modify

modify

--- test/test-npm-module.spec.coffee
+++ test/test-npm-module.spec.coffee
@@ -11,4 +11,4 @@
 describe 'test-npm-module', ->
   it 'works', ->
     actual = testNpmModule 'World2'
-    expect(actual).to.eql 'Hello World3'
+    expect(actual).to.eql 'Hello World4'


change update checking interval to 12hours

--- libs/updateSelf.coffee
+++ libs/updateSelf.coffee
@@ -10,7 +10,7 @@
 home = if process.platform is 'win32' then process.env.USERPROFILE else process.env.HOME
 rcFile = path.join home, '.npmuprc.json'

-interval = 8 * 3600 * 1000
+interval = 12 * 3600 * 1000

 readRc = ->
     try


Tweak modifier order in spec

Tweak modifier order in spec

--- spec/keymap-spec.coffee
+++ spec/keymap-spec.coffee
@@ -296,7 +296,7 @@
         expect(keymap.keystrokeStringForEvent(keydownEvent('a', altKey: true))).toBe 'alt-a'
         expect(keymap.keystrokeStringForEvent(keydownEvent('[', metaKey: true))).toBe 'cmd-['
         expect(keymap.keystrokeStringForEvent(keydownEvent('*', ctrlKey: true))).toBe 'ctrl-*'
-        expect(keymap.keystrokeStringForEvent(keydownEvent('left', ctrlKey: true, metaKey: true, altKey: true))).toBe 'alt-ctrl-cmd-left'
+        expect(keymap.keystrokeStringForEvent(keydownEvent('left', ctrlKey: true, metaKey: true, altKey: true))).toBe 'alt-cmd-ctrl-left'

     describe "when shift is pressed when a non-modifer key", ->
       it "returns a string that identifies the key pressed", ->


Destroy rootView/project on both window and atom globals

--- spec/spec-helper.coffee
+++ spec/spec-helper.coffee
@@ -82,12 +82,17 @@
   keymap.bindingSets = bindingSetsToRestore
   keymap.bindingSetsByFirstKeystroke = bindingSetsByFirstKeystrokeToRestore
   atom.deactivatePackages()
-  if rootView?
-    rootView.remove?()
-    window.rootView = null
-  if project?
-    project.destroy()
-    window.project = null
+
+  window.rootView?.remove?()
+  atom.rootView?.remove?() if atom.rootView isnt window.rootView
+  window.rootView = null
+  atom.rootView = null
+
+  window.project?.destroy?()
+  atom.project?.destroy?() if atom.project isnt window.project
+  window.project = null
+  atom.project = null
+
   $('#jasmine-content').empty() unless window.debugContent
   delete atom.windowState
   jasmine.unspy(atom, 'saveWindowState')


JGroup::fetchData: correct check on provided path with . suffix

--- workers/social/lib/social/models/group/index.coffee
+++ workers/social/lib/social/models/group/index.coffee
@@ -572,7 +572,7 @@

       return callback err  if err

-      path = "data.#{path}"  if path.indexOf 'data' isnt 0
+      path = "data.#{path}"  if path.indexOf 'data.' isnt 0
       callback null, data.getAt path


[LoadingFree] inject bongoModels from server/index

--- workers/social/lib/social/render/scriptblock.coffee
+++ workers/social/lib/social/render/scriptblock.coffee
@@ -8,14 +8,13 @@
   options.client or= null

   prefetchedFeeds = {}
-
-  {client, intro} = options
+  {bongoModels, client, intro} = options

   fetchMembersFromGraph = (cb)->
-    JGroup    = require '../models/group'
-    {Member}  = require '../models/graph'
+    return cb null, [] unless bongoModels
+    {JGroup}  = bongoModels
     groupName = client?.context?.group or 'koding'
-    JGroup.one slug : groupName, (err, group)->
+    JGroup.one slug: groupName, (err, group)->
       return cb null, [] if err
       group._fetchMembersFromGraph client, {}, cb


slightly varied random color for users w/o avatars; css transition for some changes

--- @client/avatar.coffee
+++ @client/avatar.coffee
@@ -114,7 +114,7 @@
       fetch user
       return SPAN null

-  attrs.style ||= {}
+  attrs.style ?= {}
   style = attrs.style

   # Setting avatar image
@@ -135,6 +135,9 @@
     # isn't set to transparent because a transparent icon in many cases
     # will reveal content behind it that is undesirable to show.
     style.backgroundColor = 'white'
+  else if props.set_bg_color
+    user.bg_color ?= hsv2rgb(Math.random() / 5 + .6, Math.random() / 8 + .025, Math.random() / 4 + .4)
+    style.backgroundColor = user.bg_color

   id = if anonymous
          "avatar-hidden"
@@ -170,7 +173,6 @@
   else
     # IE9 gets confused if there is an image without a src
     # Chrome puts a weird gray border around IMGs without a src
-    attrs.style.backgroundColor ?= 'transparent'
     SPAN attrs

@@ -196,6 +198,7 @@
   border-radius: 50%;
   background-size: cover;
   background-color: #{default_avatar_in_histogram_color};
+  transition: width 1s, height 1s, top 1s, left 1s, background-color 1s, opacity 100ms;
   user-select: none;
   -moz-user-select: none;
   -webkit-user-select: none;


Delete selection with delete key

Delete selection with delete key

--- app/controllers/marking_surface.coffee
+++ app/controllers/marking_surface.coffee
@@ -14,9 +14,12 @@

   events:
     'mousedown': 'onMouseDown'
+    'keydown': 'onKeyDown'

   constructor: ->
     super
+
+    @el.attr tabindex: 0

     @stage = new Kinetic.Stage
       container: @el.get 0
@@ -35,6 +38,7 @@
     return if e.isDefaultPrevented()
     return unless @tool?
     e.preventDefault()
+    @el.focus()

     {x, y} = @mouseOffset e

@@ -52,8 +56,12 @@
         @selection = tool
         t.deselect() for t in @tools when t isnt tool

+      tool.bind 'deselect', =>
+        @selection = null if @selection is tool
+
       tool.bind 'remove', =>
         @tools.splice i, 1 for t, i in @tools when t is tool
+        @tools[@tools.length - 1]?.select() if @selection is tool

       tool.select()

@@ -69,6 +77,11 @@
     {x, y}  = @mouseOffset e
     @selection.onFirstDrag [x, y]

+  onKeyDown: (e) ->
+    if e.which in [8, 46] # Backspace and delete
+      e.preventDefault()
+      @selection?.mark.destroy()
+
   mouseOffset: (e) ->
     {left, top} = @el.offset()
     x: (e.pageX - left) / @el.width()


Don't embed fonts that haven't been referenced

--- lib/font.coffee
+++ lib/font.coffee
@@ -58,7 +58,7 @@
     @subset?.use characters

   embed: ->
-    return if @embedded
+    return if @embedded or not @dictionary?

     if @isAFM
       @embedAFM()


Use `isBrowser` check for tests

Use `isBrowser` check for tests

--- spec/Parse.coffee
+++ spec/Parse.coffee
@@ -1,5 +1,5 @@
 noflo = require 'noflo'
-if typeof process is 'object' and process.title is 'node'
+unless noflo.isBrowser()
   chai = require 'chai' unless chai
   Parse = require '../components/Parse.coffee'
 else


Fix #60

Fix #60

--- lib/init.coffee
+++ lib/init.coffee
@@ -1,14 +1,32 @@
 module.exports =
-  configDefaults:
-    clangCommand: 'clang'
-    clangIncludePaths: '.'
-    clangSuppressWarnings: false
-    clangDefaultCFlags: '-Wall'
-    clangDefaultCppFlags: '-Wall -std=c++11'
-    clangDefaultObjCFlags: ' '
-    clangDefaultObjCppFlags: ' '
-    clangErrorLimit: 0
-    verboseDebug: false
+  config:
+    clangCommand:
+      type: 'string'
+      default: 'clang'
+    clangIncludePaths:
+      type: 'string'
+      default: '.'
+    clangSuppressWarnings:
+      type: 'boolean'
+      default: false
+    clangDefaultCFlags:
+      type: 'string'
+      default: '-Wall'
+    clangDefaultCppFlags:
+      type: 'string'
+      default: '-Wall -std=c++11'
+    clangDefaultObjCFlags:
+      type: 'string'
+      default: ' '
+    clangDefaultObjCppFlags:
+      type: 'string'
+      default: ' '
+    clangErrorLimit:
+      type: 'integer'
+      default: 0
+    verboseDebug:
+      type: 'boolean'
+      default: false

   activate: ->
     console.log 'activate linter-clang' if atom.inDevMode()


remove old code

remove old code

--- lib/init.coffee
+++ lib/init.coffee
@@ -253,89 +253,6 @@

     el = document.getElementById('moje')

-    print = {
-        'imported by': (entry) ->
-            console.log("EEEEEE", entry)
-            "
#{entry.name}
" +
-            (entry.data || []).map (importer) ->
-                "
 #{path_.basename(importer.path)}
"
-            .join("
")
-        # import: (entry) ->
-        #     "
#{entry.type}
" +
-        #         "
 #{entry.name} from
-        #         #{entry.originalSource}
-        #
"
-        # label: (entry) ->
-        #     "
-        #      #{entry.data} "
-
-
-        # class: (entry) ->
-        #     cls = entry
-        #     cls.loc = cls.loc || defaultLoc
-        #     "
class #{cls.name}
" + cls.methods.join('
')
-        # '@mixin': (entry) ->
-        #     "
#{entry.name}
" +
-        #             "
#{entry.data}
" +
-        #         '
'
-        #
-        symbol: (entry) ->
-            "
#{entry.name}
" +
-                entry.data.map(
-                    (n) -> "
#{n}
"
-                    #(n) -> "
#{n}
"
-                ).join('
') +
-                '
'
-        declaration: (entry) ->
-            "
#{entry.name}
" +
-                    "
#{entry.data}
" +
-                '
'
-        # lines: (entry) ->
-        #     loc = entry.data[0]
-        #     if loc < 150
-        #         color = 'green'
-        #     else if loc < 300
-        #         color = '#bb5'
-        #     else if loc < 1000
-        #         color = '#fa8'
-        #     else
-        #         color = 'red'
-        #     if entry.data.length
-        #         "
#{entry.name}
" +
-        #             entry.data.map(
-        #                 (n) -> "
#{n}
"
-        #             ).join('
') +
-        #             '
'
-        default: (entry, description) ->
-            if entry.data && entry.data.length && entry.data.map
-                "
#{entry.name}
" +
-                    entry.data.map(
-                        (n) -> "
#{n}
"
-                    ).join('
') +
-                    '
'
-            else
-                entry.loc = entry.loc || defaultLoc
-                "
#{entry.type}

-
#{entry.name}
"
-
-    }
     if editor.buffer.file
         filename = editor.buffer.file.path
     else
@@ -403,14 +320,6 @@
                     data: importers #.map((f) => path_.basename(f.path))

                 metadata = getMetadata(found[0]).concat(fixture)
-                html = metadata.map (entry) ->
-                    try
-                        render = print[entry.type || entry.name] || print.default
-                        render.call(print, entry)
-                    catch e
-                        console.error(e);
-                        console.log('error', entry)
-                .join('')
                 ReactDOM.render(
                     React.createElement(Structure,{metadata: metadata}),
                     document.getElementById('lupa-structure')


tests: Fix invalid chai assert

tests: Fix invalid chai assert

--- spec/setup.coffee
+++ spec/setup.coffee
@@ -114,7 +114,7 @@
         client.subscribeToQueue 's_worker.OUT', onMessage, (err) ->
           chai.expect(err).to.not.exist
           client.sendTo 'inqueue', 's_api.IN', input, (err) ->
-            chai.expect(err).to.not.exit
+            chai.expect(err).to.not.exist

     it 'should have set up deadlettering'


Use dispatch instead of trigger

Use dispatch instead of trigger

--- lib/main.coffee
+++ lib/main.coffee
@@ -29,8 +29,8 @@

       'incompatible-packages:reload-atom-and-recheck-packages': ->
         workspaceView = atom.views.getView(atom.workspace)
-        workspaceView.trigger 'incompatible-packages:clear-cache'
-        workspaceView.trigger 'window:reload'
+        atom.commands.dispatch(workspaceView, 'incompatible-packages:clear-cache')
+        atom.commands.dispatch(workspaceView, 'window:reload')

     if statusBar = document.querySelector('status-bar')
       createStatusBarView(statusBar)


destroy log on deactivate

destroy log on deactivate

--- lib/main.coffee
+++ lib/main.coffee
@@ -59,13 +59,14 @@
           atom.notifications.addFatalError("Uncaught #{error.stack.split('\n')[0]}", options)

     @notificationsLog = new NotificationsLog
-    @notificationsLog.onItemClick @logItemClick.bind(this)
+    @subscriptions.add @notificationsLog.onItemClick @logItemClick.bind(this)
     @subscriptions.add atom.commands.add 'atom-workspace', 'notifications:toggle-logs', => @notificationsLog.toggle()

   deactivate: ->
     @subscriptions.dispose()
     @notificationsElement?.remove()
     @notificationsPanel?.destroy()
+    @notificationsLog?.destroy()

     @subscriptions = null
     @notificationsElement = null


hack to allow easy closing

hack to allow easy closing

--- lib/main.coffee
+++ lib/main.coffee
@@ -1,4 +1,5 @@
 {CompositeDisposable} = require 'atom'
+{$} = require 'atom-space-pen-views'

 fs = require 'fs'
 zmq = require 'zmq'
@@ -409,13 +410,16 @@
             found = result['found']
             if found is true
                 data = result['data']
-                atom.workspace.open('Hydrogen Inspector', {split:'down'}).then (editor) ->
+                atom.workspace.open('.Hydrogen Inspector', {split:'down'}).then (editor) ->
                     if editor.isEmpty() is false
                         buffer = editor.getBuffer()
                         buffer.deleteRows(0, buffer.getLineCount())
-
+                    editor.setSoftWrapped(true)
+                    editor.setLineNumberGutterVisible(false)
                     editor.insertText(stripAnsi(data['text/plain']))
                     editor.moveToTop()
                     atom.workspace.activatePreviousPane()
+                    # Hack for easy closing
+                    editor.save()
             else
                 atom.notifications.addInfo("No introspection available!")


Fixed bug where main.coffee tried t read from a null TextEditor buffer on package activate.

--- lib/main.coffee
+++ lib/main.coffee
@@ -27,13 +27,6 @@
             model.resetView()

     chart_server.activate()
-
-    isChartFileRegex = /\/lib\/.*\.js|\/views\//
-    atom.workspace.observeTextEditors (editor) ->
-      filePath = editor.buffer.file.path;
-      if isChartFileRegex.test(filePath)
-        editor.onDidSave (event) ->
-          chart_client.write('chart file saved to ' + filePath)

     for key, constructor of ChartModelFactory.constructors
       atom.views.addViewProvider


Working on sign in tests

Working on sign in tests

--- tests/mocha/client/signin.coffee
+++ tests/mocha/client/signin.coffee
@@ -1,153 +1,144 @@
 if not Meteor.isClient
   return

-Meteor.setTimeout ->
-  MochaWeb?.testOnly ->
+MochaWeb?.testOnly ->

-    assert = chai.assert
+  assert = chai.assert

-    # Because I keep typing equals
-    assert.equals = assert.equal
+  # Because I keep typing equals
+  assert.equals = assert.equal

-    _createdUserEmail = null
+  _createdUserEmail = null

-    _submitSignIn = (email, password, callback) ->
-      _assertSignInVisible()
+  _submitSignIn = (email, password, callback) ->
+    _assertSignInVisible()
+    _getEmailInput().val email
+    _getPasswordInput().val password
+    _getSubmitButton().click()
+    _wait callback
+
+  _submitSignUp = (email, password, accepted, callback) ->
+    _goToSignUp ->
       _getEmailInput().val email
       _getPasswordInput().val password
+      _getTermsInput().prop "checked", accepted
       _getSubmitButton().click()
       _wait callback

-    _submitSignUp = (email, password, accepted, callback) ->
+  _goToSignUp = (callback) ->
+    _assertSignInVisible()
+    email = _generateRandomEmail()
+    _getEmailInput().val email
+    _getEmailInput().blur()
+    _wait ->
+      _assertSignUpVisible()
+      callback email
+
+  _getVisibleForm = ->
+    return $("form:visible")
+
+  _getEmailInput = ->
+    return _getVisibleForm().find "input[name=email]"
+
+  _getPasswordInput = ->
+    return _getVisibleForm().find "input[name=password]"
+
+  _getTermsInput = ->
+    return _getVisibleForm().find "input#terms"
+
+  _getSubmitButton = ->
+    return _getVisibleForm().find "button"
+
+  _assertSignInVisible = ->
+    assert.equal _getVisibleForm().attr("id"), "signin"
+
+  _assertSignUpVisible = ->
+    assert.equal _getVisibleForm().attr("id"), "signup"
+
+  _wait = (func) ->
+    Meteor.setTimeout func, 250
+
+  _getRandomNumber = ->
+    return Math.floor((Math.random() * 100000) + 1)
+
+  _generateRandomEmail = ->
+    return "newguy#{_getRandomNumber()}@gmail.com"
+
+  _getCurrentUserEmail = ->
+    return Meteor.user().emails[0].address
+
+  _getErrorMessage = (name) ->
+    return $("input[name=#{name}] ~ .input__status").text()
+
+  _logout = (callback) ->
+    Meteor.logout()
+    Meteor.autorun ->
+      if not Meteor.userId()
+        callback()
+
+  describe "Signin", ->
+
+    beforeEach ->
+      Meteor.flush()
+
+    it "should start with the signin form", ->
+      _assertSignInVisible()
+
+    it "should have two inputs", ->
+      _assertSignInVisible()
+      assert.equal _getVisibleForm().find("input").length, 2
+      assert.equal _getEmailInput().attr("name"), "email"
+      assert.equal _getPasswordInput().attr("name"), "password"
+
+    it "should deny signin submit if email is malformed", ->
+      _submitSignIn "joe@joe", "password123", ->
+        error = _getErrorMessage "email"
+        assert.equal "Please enter a valid email", error
+        done()
+
+    it "should deny signin submit if password is empty", ->
+      _submitSignIn "joe@joe.com", "", ->
+        error = _getErrorMessage "email"
+        assert.equal "Please enter a password", error
+        done()
+
+    it "should detect new email and present signup form", (done) ->
       _goToSignUp ->
-        _getEmailInput().val email
-        _getPasswordInput().val password
-        _getTermsInput().prop "checked", accepted
-        _getSubmitButton().click()
-        _wait callback
+        done()

-    _goToSignUp = (callback) ->
-      _assertSignInVisible()
-      email = _generateRandomEmail()
-      _getEmailInput().val email
-      _getEmailInput().blur()
-      _wait ->
-        _assertSignUpVisible()
-        callback email
-
-    _getVisibleForm = ->
-      return $("form:visible")
-
-    _getEmailInput = ->
-      return _getVisibleForm().find "input[name=email]"
-
-    _getPasswordInput = ->
-      return _getVisibleForm().find "input[name=password]"
-
-    _getTermsInput = ->
-      return _getVisibleForm().find "input#terms"
-
-    _getSubmitButton = ->
-      return _getVisibleForm().find "button"
-
-    _assertSignInVisible = ->
-      assert.equal _getVisibleForm().attr("id"), "signin"
-
-    _assertSignUpVisible = ->
-      assert.equal _getVisibleForm().attr("id"), "signup"
-
-    _wait = (func) ->
-      Meteor.setTimeout func, 250
-
-    _getRandomNumber = ->
-      return Math.floor((Math.random() * 100000) + 1)
-
-    _generateRandomEmail = ->
-      return "newguy#{_getRandomNumber()}@gmail.com"
-
-    _getCurrentUserEmail = ->
-      return Meteor.user().emails[0].address
-
-    _getErrorMessage = (name) ->
-      return $("input[name=#{name}] ~ .input__status").text()
-
-
-    describe "Signin", ->
-
-      beforeEach (done) ->
-        _getEmailInput().val null
-        _getPasswordInput().val null
-        _getEmailInput().blur()
-        Meteor.flush()
-        _wait done
-
-      before (done) ->
-        Meteor.logout()
-        Meteor.autorun ->
-          user = Apollos.user()
-          done() if Object.keys(user).length is 0
-
-      it "should start with the signin form", ->
-        _assertSignInVisible()
-
-      it "should have two inputs", ->
-        _assertSignInVisible()
-        assert.equal _getVisibleForm().find("input").length, 2
+    it "should show the terms checkbox on the signup form", (done) ->
+      _goToSignUp ->
+        assert.equal _getVisibleForm().find("input").length, 3
         assert.equal _getEmailInput().attr("name"), "email"
         assert.equal _getPasswordInput().attr("name"), "password"
+        assert.equal _getTermsInput().attr("id"), "terms"
+        done()

-      it "should deny signin submit if email is malformed", ->
-        _submitSignIn "joe@joe", "password123", ->
-          error = _getErrorMessage "email"
-          assert.equal "Please enter a valid email", error
-          done()
+    it "should deny signup submit if email is malformed", (done) ->
+      _submitSignUp "joe@joe", "password123", true, ->
+        error = _getErrorMessage "email"
+        assert.equal "Please enter a valid email", error
+        done()

-      it "should deny signin submit if password is empty", ->
-        _submitSignIn "joe@joe.com", "", ->
-          error = _getErrorMessage "email"
-          assert.equal "Please enter a password", error
-          done()
+    it "should deny signup submit if password is empty", (done) ->
+      _submitSignUp "joe@joe.com", "", true, ->
+        error = _getErrorMessage "password"
+        assert.equal "Password may not be empty", error
+        done()

-      it "should detect new email and present signup form", (done) ->
-        _goToSignUp ->
-          done()
+    it "should deny signup submit if terms are not accepted", (done) ->
+      _submitSignUp "joe@joe.com", "password123", false, ->
+        error = _getErrorMessage "terms"
+        assert.equal "Please accept the terms", error
+        done()

-      it "should show the terms checkbox on the signup form", (done) ->
-        _goToSignUp ->
-          assert.equal _getVisibleForm().find("input").length, 3
-          assert.equal _getEmailInput().attr("name"), "email"
-          assert.equal _getPasswordInput().attr("name"), "password"
-          assert.equal _getTermsInput().attr("id"), "terms"
-          done()
+    it "should create a new user from the signup form", (done) ->
+      _createdUserEmail = _generateRandomEmail()
+      _submitSignUp _createdUserEmail, "password123", true, ->
+        assert.equal Meteor.user().emails[0].address, _createdUserEmail
+        _logout done

-      it "should deny signup submit if email is malformed", (done) ->
-        _submitSignUp "joe@joe", "password123", true, ->
-          error = _getErrorMessage "email"
-          assert.equal "Please enter a valid email", error
-          done()
-
-      it "should deny signup submit if password is empty", (done) ->
-        _submitSignUp "joe@joe.com", "", true, ->
-          error = _getErrorMessage "password"
-          assert.equal "Password may not be empty", error
-          done()
-
-      it "should deny signup submit if terms are not accepted", (done) ->
-        _submitSignUp "joe@joe.com", "password123", false, ->
-          error = _getErrorMessage "terms"
-          assert.equal "Please accept the terms", error
-          done()
-
-      it "should create a new user from the signup form", (done) ->
-        _createdUserEmail = _generateRandomEmail()
-        _submitSignUp _createdUserEmail, "password123", true, ->
-          assert.equal Meteor.user().emails[0].address, _createdUserEmail
-          Meteor.logout()
-          done()
-
-      it "should allow sign in of created user", (done) ->
-        _submitSignIn _createdUserEmail, "password123", ->
-          assert.equal Meteor.user().emails[0].address, _createdUserEmail
-          Meteor.logout()
-          done()
-, 1000
+    it "should allow sign in of created user", (done) ->
+      _submitSignIn _createdUserEmail, "password123", ->
+        assert.equal Meteor.user().emails[0].address, _createdUserEmail
+        _logout done


:art: adding support for style.constants

--- src/Styler.coffee
+++ src/Styler.coffee
@@ -19,6 +19,8 @@
     json = JSON.parse fs.readFileSync(file).toString()
     @styleName = json.name

+    @_replaceConstants json.constants, json.layers if json.constants
+
     for style in json.layers
       continue if style.ref

@@ -37,7 +39,19 @@
       if style.appliesTo(feature) and ((layer is "road") or (not style.minzoom) or style.minzoom <= zoom)
           return style

-    false
+    return false
+
+  _replaceConstants: (constants, tree) ->
+    for id, node of tree
+      switch typeof node
+        when 'object'
+          continue if node.constructor.name.match /Stream/
+          @_replaceConstants constants, node
+
+        when 'string'
+          if node.charAt(0) is '@'
+            tree[id] = constants[node]
+    null

   _compileFilter: (filter) ->
     switch filter?[0]


removes log

removes log

--- src/bebopt.coffee
+++ src/bebopt.coffee
@@ -16,12 +16,6 @@
 limitations under the License.
 ###
 'use strict'
-
-if 0 # FIXME remove me
-  log = require './log'
-else
-  log = require './log'
-  log = log.fake()

 class Bebopt
   constructor: (@app) ->


Show spinner in search tabs by default

Show spinner in search tabs by default

--- app/scripts/directives.coffee
+++ app/scripts/directives.coffee
@@ -481,8 +481,8 @@
     replace : true
     template : """

-

-                <span ng-if="spinner" class="preloader_spinner"
+

+

     """


restore go to page in pager

restore go to page in pager

git-svn-id: 00f01ee4f729ccc52c3e336777c3e81b5adc88db@248942 ef392f78-ee14-0410-84e5-9db46baae5bb

--- app/scripts/directives.coffee
+++ app/scripts/directives.coffee
@@ -485,7 +485,7 @@
          previous-text="‹" next-text="›" first-text="«" last-text="»"
          boundary-links="true"
          rotate="false"
-         num-pages="$parent.numPages"> </pagination>
+         num-pages="$parent.numPages"> </uib-pagination>
       <div class="page_input"><span>{{'goto_page' | loc:lang}} </span>
         <input ng-model="gotoPage" ng-keyup="onPageInput($event, gotoPage, numPages)"
             ng-click="$event.stopPropagation()" />


Pass down the websocket error

Pass down the websocket error

--- src/client.coffee
+++ src/client.coffee
@@ -120,9 +120,9 @@
         # flags.masked will be set if the data was masked
         @onMessage JSON.parse(data)

-      @ws.on 'error', =>
+      @ws.on 'error', (error)=>
         # TODO: Reconnect?
-        @emit 'error'
+        @emit 'error', error

       @ws.on 'close', =>
         @emit 'close'


Remove logging line

Remove logging line

--- app/scripts/controllers/domains.coffee
+++ app/scripts/controllers/domains.coffee
@@ -22,7 +22,6 @@
     @mostFrequentVisible = false

   showMostFrequent: ->
-    console.log(@mostFrequentVisible)
     @groupsVisible = false
     @mostFrequentVisible = true


Ensure ID is numeric

Ensure ID is numeric

--- app._coffee
+++ app._coffee
@@ -108,7 +108,9 @@
     destination = processor.process source, _

 app.get '/:id', (req, res, _) ->
-    id = req.param 'id'
+    id = parseInt req.params.id, 10
+    if not id? or isNaN id
+        return res.send 404
     res.render 'view', {id}


build sass and concat scripts on starting gulp

--- app/templates/gulpfile.coffee
+++ app/templates/gulpfile.coffee
@@ -156,7 +156,6 @@
     .pipe concat('built.js')
     .pipe gulp.dest(conf.path.dev.js)
     .pipe reload({stream: true})
-  return

@@ -350,5 +349,5 @@
 #*------------------------------------*\
 #     $TASKS
 #*------------------------------------*/
-gulp.task 'default', ['watch']
+gulp.task 'default', ['sass', 'concat', 'watch']
 gulp.task "build", ["uglify-vendors", "rev_replace"]


Use paths

Use paths

--- app/templates/gulpfile.coffee
+++ app/templates/gulpfile.coffee
@@ -6,14 +6,14 @@
 es = require 'event-stream'

 paths = {
-  lint: ['./gulpfile.js', './src/**/*.js'],
-  watch: ['./gulpfile.js', './src/**/*.js', './test/**/*.js', '!test/{temp,temp/**}'],
-  tests: ['./test/**/*.js', '!test/{temp,temp/**}'],
-  source: ['./src/**/*.js']
+  lint: ['./gulpfile.coffee', './src/**/*.coffee'],
+  watch: ['./gulpfile.coffee', './src/**/*.coffee', './test/**/*.coffee', '!test/{temp,temp/**}'],
+  tests: ['./test/**/*.coffee', '!test/{temp,temp/**}'],
+  source: ['./src/**/*.coffee']
 }

 gulp.task 'lint', ->
-  gulp.src(['./src/**/*.coffee'])
+  gulp.src(paths.lint)
     .pipe($.coffeelint('./coffeelint.json'))
     .pipe($.coffeelint.reporter())

@@ -22,12 +22,12 @@

 gulp.task 'compile', ['lint'], ->
   es.merge(
-    gulp.src('./src/**/*.coffee')
+    gulp.src(paths.source)
       .pipe($.sourcemaps.init())
       .pipe($.coffee({ bare: true }).on('error', $.util.log))
       .pipe($.sourcemaps.write())
       .pipe(gulp.dest('./compile/src'))
-    gulp.src('./test/**/*.coffee')
+    gulp.src(paths.tests)
       .pipe($.sourcemaps.init())
       .pipe($.coffee({ bare: true }).on('error', $.util.log))
       .pipe($.sourcemaps.write())


Content edits

Content edits

--- app/translations/en_us.coffee
+++ app/translations/en_us.coffee
@@ -46,17 +46,16 @@
       content: "Welcome to Chicago Wildlife Watch! In this project, we are asking you to identify animals found in camera trap photos from around the city. Your help allows us to continue our research into urban biodiversity, making the city a better home for both humans and animals."
     s2:
       title: "Identifying animals"
-      content: "Welcome to Chicago Wildlife Watch! In this project, we are asking you to identify animals found in camera trap photos from around the city. Your help allows us to continue our research into urban biodiversity, making the city a better home for both humans and animals."
+      content: "You are presented with a photo on the left and a list of potential animals on the right. When you spot an animal within a photo, simply click the corresponding animal on the right. You will be asked to supply some followup info, such as the number of animals found in the image."
     s3:
       title: "Filtering by characteristics"
-      content: "You are presented with a photo on the left and a list of potential animals on the right. When you spot an animal within a photo, simply click the corresponding animal on the right. You will be asked to supply some followup info, such as number of animals found in the image."
+      content: "Occasionally it is difficult to determine which animal is in the photo. Just above the list of animals is a variety of methods to filter the animal list. Selecting different options reduces the number of choices, making your job easier."
     s4:
       title: "Made a mistake?"
       content: "If you need to delete any of your existing marks while viewing an image, simply find your animal in the list below the image and click the 'X' icon"
     s5:
       title: "That's it!"
-      content: "If you need to delete any of your existing marks while viewing an image, simply find your animal in the list below the image and click the 'X' icon"
-
+      content: "That's it! You may catchup on project updates either on the blog or through speaking with other volunteers. If you need additional help using the interface, click Tutorial within the list of links below the Finish button."

   classify:
     deleteAnnotation: 'Delete'
@@ -209,170 +208,170 @@
     beaver:
       label: 'Beaver'
       description: '''
-        Usually brown with large sharp front teeth, front feet has claws, back feet are webbed, round body, and a large flat tail. As the second largest rodent in the world, beavers are semi-aquatic and primarily nocturnal. They are known for building dams, canals, and lodges out of tree limbs and branches for homes.
-    '''
+        Usually brown with large sharp front teeth, front feet has claws, back feet are webbed, round body, and a large flat tail. As the second largest rodent in the world, beavers are semi-aquatic and primarily nocturnal.
+      '''

     bike:
       label: 'Bike'
       description: '''
-        Describe Bike
-    '''
+        N/A
+      '''

     car:
       label: 'Car'
       description: '''
-        Describe Car
-    '''
+        N/A
+      '''

     chipmunk:
       label: 'Chipmunk'
       description: '''
         A small member of the squirrel family that use their cheek pouches to carry various foods of nuts, berries, seeds, etc into their burrow or nest to store their food. These have smaller less bushy tails than a typical squirrel and sport signature white and brown stripes around their eye and going down their back. Typically brown or tan in color.
-    '''
+      '''

     coyote:
       label: 'Coyote'
       description: '''
-        Commonly brown and tan in color, this member of the dog family is smaller than wolves and have occasionally been called prairie wolves or brush wolves due to their wolf like appearance. Although, they differ from wolves with their pointed snouts, slender body, and tall, pointy ears. In the fall and winter they form packs for more effective hunting. Humans should not fear urban coyotes, but take precautions not to feed the animals and keep small pets indoors.
-    '''
+        Commonly brown and tan in color, this member of the dog family is smaller than wolves and have occasionally been called prairie wolves or brush wolves due to their wolf like appearance. Although, they differ from wolves with their pointed snouts, slender body, and tall, pointy ears. In the fall and winter they form packs for more effective hunting.
+      '''

     deer:
       label: 'Deer'
       description: '''
         A fairly big, tan animal that is in the same family as moose and reindeer. They have large ears and short fluffy tails. The males are called bucks while females are called does and the young are called fawns. Some males may have antlers depending on the time of the year.
-    '''
+      '''

     domCat:
       label: 'Cat, Domestic'
       description: '''
         A common household pet with a small build, long tails, and small pointed ears. This animals come in a variety of color, patterns (spotted or stripes), and breeds.
-    '''
+      '''

     domDog:
       label: 'Dog, Domestic'
       description: '''
-        A common household pet related to wolves, foxes, and jackals, but come in a variety of colors, sizes, patterns, and breeds. Dogs communicate in many different ways; such as scent, facial expressions, body position, and movement.
-    '''
+        A common household pet related to wolves, foxes, and jackals, but come in a variety of colors, sizes, patterns, and breeds.
+      '''

     flyingSquirrel:
       label: 'Squirrel, Flying'
       description: '''
         A member of the squirrel family, this animal has large, dark eyes, loose skin between the front and back legs, clawed feet, and a long, bushy tail with dark fur on top and light below. It travels from tree to tree by launching itself and spreading its limbs so it can glide, while using its legs to steer and its tail to brake when it reaches its destination.
-    '''
+      '''

     foxSquirrel:
       label: 'Squirrel, Fox'
       description: '''
-        The largest species of tree squirrel in North America, they look like a larger typical squirrel with an orange tone to their fur. They can be around 2 feet in length and weight 1 - 2 lbs. It spends most time in forging food  and roaming around fields.
-    '''
+        The largest species of tree squirrel in North America, they look like a larger typical squirrel with an orange tone to their fur. They can be around 2 feet in length and weight 1 - 2 lbs. It spends most time in forging food and roaming around fields.
+      '''

     grayFox:
       label: 'Fox, Gray'
       description: '''
         A small to medium sized animal with gray coat on its back and head with light fur on its chest and belly. They have a signature black tipped, bushy tail and band of black fur from the inner corner of their eyes around the bottom of their snout. They are tree climbers, typically sleep during the day at very rocky locations, and are active during the night.
-    '''
+      '''

     graySquirrel:
       label: 'Squirrel, Gray'
       description: '''
         Small squirrels that are gray-brown all over their bodies except the white patch along their belly. This animals diet consists of acorns, hickory nuts, maple, walnuts, etc.
-    '''
+      '''

     horse:
       label: 'Horse'
       description: '''
-        A large, muscular animal with a mane of hair along the back of the head and neck. These animals have hooves and a long tail of hair. Males are called stallions, females are mares, and young ones are called foals. Domesticated these animals are used for transportation and competitive sports.
-    '''
+        A large, muscular animal with a mane of hair along the back of the head and neck. These animals have hooves and a long tail of hair.
+      '''

     human:
       label: 'Human'
       description: '''
-        Describe Human
-    '''
+        Identifying a human within a photo flags this image. We generally remove images of humans very quickly from the dataset.
+      '''

     livestock:
       label: 'Livestock'
       description: '''
-        Describe Livestock
-    '''
+        N/A
+      '''

     mink:
       label: 'Mink'
       description: '''
         Related to otters, weasels, and ferrets, Minks have pointed snouts with dark eyes. They also have clawed feet with webbing and bodies covered in thick brown fur that sheds water quickly. These animals live in semi aquatic habitats.
-    '''
+      '''

     mouse:
       label: 'Mouse'
       description: '''
         A very small animal with a long snout, big round ears, and a long hairless tail. This animal can be found in and around grasslands and homes.
-    '''
+      '''

     mower:
       label: 'Mower'
       description: '''
-        Describe Mower
+        N/A
     '''

     muskrat:
       label: 'Muskrat'
       description: '''
         This animal normally has brown fur and a black tail with small dark eyes. The furless, scaly tail is half of its total length. When it walks, the tail creates a trail. Their front paws are clawed while in the rear they are semi-webbed clawed feet.
-    '''
+      '''

     opossum:
       label: 'Opossum'
       description: '''
         Adept tree climbers with their clawed feet and opposable big toe on rear paw. This animal known for playing dead has a white face with a pink nose,  black eyes, black ears, long tail and long, gray fur on their body.
-    '''
+      '''

     otherVehicle:
       label: 'Vehicle, Other'
       description: '''
-        Describe Vehicle, Other
-    '''
+        N/A
+      '''

     rabbit:
       label: 'Rabbit'
       description: '''
         Rabbits we see in urban settings have large, dark eyes, long ears pointed upwards, and brown fur with a short, fluffy, white tail.
-    '''
+      '''

     raccoon:
       label: 'Raccoon'
       description: '''
         This brown-gray furry animal looks like it’s wearing a bandit mask with the black patches around its eyes on its white face. Another notable features are its finger like clawed toes and its black-ringed tail with long fur. Raccoons are nocturnal and often seen foraging for food in trash bins.
-    '''
+      '''

     rat:
       label: 'Rat'
       description: '''
         A large mouse like critter with a pointy nose, round ears, and a long, hairless, pinkish tail. Many people think they only roam the subway and sewers, but its not uncommon to see then running across the street above ground.
-    '''
+      '''

     redFox:
       label: 'Fox, Red'
       description: '''
-        An animal with orange brownish fur along its head and back with and white fur on its belly and typically at the tip of its bushy tail. It’s tail helps it balance just like a cat’s. Some have black fur on the back of its ears and legs. They often hunt for food but can adapt to eating garbage and pet food.
-    '''
+        An animal with orange brownish fur along its head and back with and white fur on its belly and typically at the tip of its bushy tail. It’s tail helps it balance just like a cat’s. Some have black fur on the back of its ears and legs.
+      '''

     skunk:
       label: 'Skunk'
       description: '''
         An animal, well known for its stinky spray towards predators, has black with white stripe patterns. It has a single thin, white stripe down the middle of its face and two, thicker white stripes originated at the top of its head running down either side of the back into its thick, bushy tail.
-    '''
+      '''

     woodChuck:
       label: 'Woodchuck'
       description: '''
         This animal is also known as a groundhog. It has a brown furry coat, clawed feets, small eyes and ears, and a short, flat tail.
-    '''
+      '''

     melanisticGraySquirrel:
       label: 'Squirrel, Melanistic Gray'
       description: '''
         Is an animal that has a color morph to change its fur from gray to black. You may occasionally see them during the color changing process, they will appear to be brown and black patchy.
-    '''
+      '''

   differences:
     beaver:


- Allow actionDispatchers to return null

--- src/DefaultMixin.coffee
+++ src/DefaultMixin.coffee
@@ -9,7 +9,8 @@

 # Bind an action to automatically dispatch to the right store.
 dispatchAction = (actionCreator, self) -> ->
-	self.store.dispatch(actionCreator.apply(self, arguments))
+	action = actionCreator.apply(self, arguments)
+	if action? then self.store.dispatch(action)

 # DefaultMixin is mixed into all component specs automatically by createClass.
 export default DefaultMixin = {


fix alarms model

fix alarms model

--- app/CNeditor/alarm.coffee
+++ app/CNeditor/alarm.coffee
@@ -4,7 +4,7 @@
 module.exports = class Alarm extends Backbone.Model

     # A remplir par Joseph
-    urlRoot: "/apps/alarms/alarms"
+    urlRoot: "/apps/agenda/alarms"

     @dateFormat = "{Dow} {Mon} {dd} {yyyy} {HH}:{mm}:00"

@@ -45,7 +45,7 @@

     # Private static methods
     noCallback = ->
-    isAlarm = (app) -> app.name is 'todos'
+    isAlarm = (app) -> app.name is 'agenda'
     getApps = -> request.get '/api/applications', noCallback
     checkAlarmInstalled = (apps) ->
         return true if apps.rows.some isAlarm


Update zh-HANS.coffee (#5409)

Update zh-HANS.coffee (#5409)

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -1769,11 +1769,11 @@
     unpublished: "（未发布的）"
     view_arena_ladder: "查看竞技场天梯"
     resource_hub: "资源中心"
-#    pacing_guides: "Classroom-in-a-Box Pacing Guides"
-#    pacing_guides_desc: "Learn how to incorporate all of CodeCombat's resources to plan your school year!"
-#    pacing_guides_elem: "Elementary School Pacing Guide"
-#    pacing_guides_middle: "Middle School Pacing Guide"
-#    pacing_guides_high: "High School Pacing Guide"
+    pacing_guides: "课堂进度建议指南"
+    pacing_guides_desc: "了解如何整合CodeCombat的所有资源来制定您的教学计划！"
+    pacing_guides_elem: "小学进度指南"
+    pacing_guides_middle: "中学进度指南"
+    pacing_guides_high: "高中进度指南"
     getting_started: "入门"
     educator_faq: "常见问题"
     educator_faq_desc: "在您的学校或教室使用 CodeCombat 的常见问题。"
@@ -1781,8 +1781,8 @@
     teacher_getting_started_desc: "第一次使用 CodeCombat ？下载此老师入门指南开始设置您的账号，创建第一个课堂并邀请学生参加第一门课程。"
     student_getting_started: "学生快速入门指南"
     student_getting_started_desc: "您可以在讲解 CodeCombat 之前先将此入门指南发布给学生，以便他们熟悉代码编辑器。本指南可用于 Python 和 JavaScript 语言教室。"
-#    ap_cs_principles: "AP Computer Science Principles"
-#    ap_cs_principles_desc: "AP Computer Science Principles gives students a broad introduction to the power, impact, and possibilities of Computer Science. The course emphasizes computational thinking and problem solving while also teaching the basics of programming."
+    ap_cs_principles: "AP 计算机科学原理"
+    ap_cs_principles_desc: "AP计算机科学原理为学生提供了计算机科学的应用，影响和可能性的广泛介绍。本课程在教授编程基础知识的同时，还强调了计算思维和解决问题的能力。"
     cs1: "计算机科学的介绍"
     cs2: "计算机科学 2"
     cs3: "计算机科学 3"
@@ -1793,8 +1793,8 @@
     cs1_syntax_javascript: "课程 1 JavaScript 语法指南"
     cs1_syntax_javascript_desc: "参考常见的 JavaScript 语法，学生将学习计算科学的介绍。"
     coming_soon: "更多指南即将推出！"
-    engineering_cycle_worksheet: "培训周期表"
-    engineering_cycle_worksheet_desc: "基于此表来管理学生的培训周期、评估、计划、进度和调试。请参考完整的示例表作为指南。"
+    engineering_cycle_worksheet: "程序设计流程表"
+    engineering_cycle_worksheet_desc: "基于此表来管理学生的程序设计流程、评估、计划、进度和调试。请参考完整的示例表作为指南。"
     engineering_cycle_worksheet_link: "查看示例"
     progress_journal: "学习进度"
     progress_journal_desc: "鼓励学生通过进度日志跟踪他们的进步。"
@@ -1802,8 +1802,8 @@
     cs1_curriculum_desc: "课程 1 的范围、顺序、课程计划和活动等。"
     arenas_curriculum: "竞技场 － 老师指导"
     arenas_curriculum_desc: "如何在班级中使用多人竞技场《Wakka Maul》《Cross Bones》和《Power Peak》"
-#    assessments_curriculum: "Assessment Levels - Teacher Guide"
-#    assessments_curriculum_desc: "Learn how to use Challenge Levels and Combo Challenge levels to assess students' learning outcomes."
+    assessments_curriculum: "评估级别 - 教师指南"
+    assessments_curriculum_desc: "了解如何使用挑战级别和组合挑战级别来评估学生的学习成果。"
     cs2_curriculum: "计算机科学 2 - 课程指南"
     cs2_curriculum_desc: "课程 2 的范围、顺序、课程计划和活动等。"
     cs3_curriculum: "计算机科学 3 - 课程指南"
@@ -1811,38 +1811,38 @@
     cs4_curriculum: "计算机科学 4 - 课程指南"
     cs4_curriculum_desc: "课程 4 的范围、顺序、课程计划和活动等。"
     cs5_curriculum_js: "计算机科学 5 - 课程指南 (JavaScript)"
-#    cs5_curriculum_desc_js: "Scope and sequence, lesson plans, activities and more for Course 5 classes using JavaScript."
+    cs5_curriculum_desc_js: "使用JavaScript的课程5的范围，顺序，课程计划和活动等。"
     cs5_curriculum_py: "计算机科学 5 - 课程指南 (Python)"
-#    cs5_curriculum_desc_py: "Scope and sequence, lesson plans, activities and more for Course 5 classes using Python."
+    cs5_curriculum_desc_py: "使用Python的课程5的范围，顺序，课程计划和活动等。"
     cs1_pairprogramming: "组队编程活动"
     cs1_pairprogramming_desc: "让两个学生一起组队编程练习，这将有助于他们互相更好的学习与沟通。"
     gd1: "游戏开发1"
-#    gd1_guide: "Game Development 1 - Project Guide"
-#    gd1_guide_desc: "Use this to guide your students as they create their first shareable game project in 5 days."
-#    gd1_rubric: "Game Development 1 - Project Rubric"
-#    gd1_rubric_desc: "Use this rubric to assess student projects at the end of Game Development 1."
+    gd1_guide: "游戏开发1 – 课程指南"
+    gd1_guide_desc: "使用本指南来指导学生在5天内开发他们的第一个可共享游戏项目。"
+    gd1_rubric: "游戏开发1 – 课程评估"
+    gd1_rubric_desc: "使用本标准来评估学生们在游戏开发1创建的项目。"
     gd2: "游戏开发2"
-#    gd2_curriculum: "Game Development 2 - Curriculum Guide"
-#    gd2_curriculum_desc: "Lesson plans for Game Development 2."
+    gd2_curriculum: "游戏开发2 – 课程指南"
+    gd2_curriculum_desc: "游戏开发2的课程计划。"
     gd3: "游戏开发3"
-#    gd3_curriculum: "Game Development 3 - Curriculum Guide"
-#    gd3_curriculum_desc: "Lesson plans for Game Development 3."
+    gd3_curriculum: "游戏开发3 – 课程指南"
+    gd3_curriculum_desc: "游戏开发3的课程计划。"
     wd1: "网页开发1"
-#    wd1_curriculum: "Web Development 1 - Curriculum Guide"
-#    wd1_curriculum_desc: "Scope and sequence, lesson plans, activities, and more for Web Development 1."
-#    wd1_headlines: "Headlines & Headers Activity"
-#    wd1_headlines_example: "View sample solution"
-#    wd1_headlines_desc: "Why are paragraph and header tags important? Use this activity to show how well-chosen headers make web pages easier to read. There are many correct solutions to this!"
-#    wd1_html_syntax: "HTML Syntax Guide"
-#    wd1_html_syntax_desc: "One-page reference for the HTML style students will learn in Web Development 1."
-#    wd1_css_syntax: "CSS Syntax Guide"
-#    wd1_css_syntax_desc: "One-page reference for the CSS and Style syntax students will learn in Web Development 1."
+    wd1_curriculum: "网页开发1 - 课程指南"
+    wd1_curriculum_desc: "网页开发1的范围、顺序，课程计划和活动等。"
+    wd1_headlines: "标题标识活动练习"
+    wd1_headlines_example: "查看示例"
+    wd1_headlines_desc: "为什么段落和标题标签很重要？运用此练习说明精选的标题如何使网页更易于阅读。当然还有更多其他的解决方案!"
+    wd1_html_syntax: "HTML语法指南"
+    wd1_html_syntax_desc: "这是学生在网页开发1中学习HTML样式的单页参考。"
+    wd1_css_syntax: "CSS语法指南"
+    wd1_css_syntax_desc: "这是学生在网页开发1中学习CSSL风格的单页参考。"
     wd2: "网页开发2"
-#    wd2_jquery_syntax: "jQuery Functions Syntax Guide"
-#    wd2_jquery_syntax_desc: "One-page reference for the jQuery functions students will learn in Web Development 2."
-#    wd2_quizlet_worksheet: "Quizlet Planning Worksheet"
-#    wd2_quizlet_worksheet_instructions: "View instructions & examples"
-#    wd2_quizlet_worksheet_desc: "Before your students build their personality quiz project at the end of Web Development 2, they should plan out their quiz questions, outcomes and responses using this worksheet. Teachers can distribute the instructions and examples for students to refer to."
+    wd2_jquery_syntax: "jQuery函数语法指南"
+    wd2_jquery_syntax_desc: "这是学生在网页开发2中学习jQuery函数的单页参考。"
+    wd2_quizlet_worksheet: "测试规划表"
+    wd2_quizlet_worksheet_instructions: "查看说明和示例"
+    wd2_quizlet_worksheet_desc: "在学生创建他们的个性测验项目，结束网页开发2之前，他们应该使用此工作表来规划测试问题、结果和响应。教师可以分发说明和示例供学生参考。"
     student_overview: "概述"
     student_details: "学生详情"
     student_name: "学生姓名"


Removing UUID from projects

Removing UUID from projects

--- app/models/project.coffee
+++ app/models/project.coffee
@@ -5,7 +5,6 @@
   owner: DS.belongsTo 'user', async: true, inverse: 'projects'
   name: DS.attr 'string'
   packageName: DS.attr 'string'
-  uuid: DS.attr 'string'
   platform: DS.attr 'number'
   source: DS.attr 'string'
   version: DS.attr 'string'


add h2 tag

add h2 tag

--- app/scripts/editor.coffee
+++ app/scripts/editor.coffee
@@ -104,6 +104,11 @@
 					e.stopPropagation()
 			when 50
 				if @status.cmd or @status.ctrl
+					@toggleFormatBlock 'h2'
+					e.preventDefault()
+					e.stopPropagation()
+			when 51
+				if @status.cmd or @status.ctrl
 					@toggleFormatBlock 'pre'
 					e.preventDefault()
 					e.stopPropagation()


setting browser html happens in show()

setting browser html happens in show()

--- src/atom/browser.coffee
+++ src/atom/browser.coffee
@@ -12,12 +12,12 @@
     return false if not /^https?:/.test url

     @url = url
-
-    style = "width:100%;height:100%;background-color:#fff;border:none"
-    @html = ""
-
     @show()

     true

+  show: ->
+    style = "width:100%;height:100%;background-color:#fff;border:none"
+    @html = ""

+    super


Refactoring & commenting

Refactoring & commenting

--- src/Parser.coffee
+++ src/Parser.coffee
@@ -73,25 +73,17 @@

   # Parse a statement that returns true or false or calculate a value
   parseStatement: (s) ->
+    # Check for valid parentheses
     if !Util.validateParentheses(s)
       console.error "ERROR: Invalid parentheses in statement"
+    # Clean spaces
     s = s.replace(/\s+/g, '');
+    # Remove all operators and parentheses
     parsedString = s.split(/\(|\)|\+|\*|\-|\/|<=|>=|<|>|==|!=|\|\||&&/)
     parsedValues = []
+    # Parse the strings for known prefixes, and parse the values based on that.
     for val in parsedString
-      type = null
-      if val.substring(0,5) == "stat."
-        type = "stats"
-      else if val.substring(0,4) == "inv."
-        type = "item"
-      else if val.substring(0,4) == "var."
-        type = "var"
-      else if !isNaN(parseFloat(val)) && val.toString().indexOf(".") == -1
-        type = "int"
-      else if !isNaN(parseFloat(val)) && val.toString().indexOf(".") != -1
-        type = "float"
-      else
-        type = "string"
+      type = @getStatementType(val)
       switch type
         when "item"
           for i in data.game.inventory
@@ -116,15 +108,35 @@
             parsedValues.push "'" + val + "'"
           else
             parsedValues.push ""
+    # Replace all variables with their correct values
     for i in [0 .. parsedString.length-1]
       if parsedString[i] != "" && parsedValues[i] != ""
         s = s.replace(new RegExp(parsedString[i],'g'),parsedValues[i])
+    # Solve or calculate the statement
     return eval(s)
+
+  # Read a string's beginning to detect its type
+  getStatementType: (val) ->
+    type = null
+    if val.substring(0,5) == "stat."
+      type = "stats"
+    else if val.substring(0,4) == "inv."
+      type = "item"
+    else if val.substring(0,4) == "var."
+      type = "var"
+    else if !isNaN(parseFloat(val)) && val.toString().indexOf(".") == -1
+      type = "int"
+    else if !isNaN(parseFloat(val)) && val.toString().indexOf(".") != -1
+      type = "float"
+    else
+      type = "string"
+    return type

   # Find a value from the game data json
   # toPrint == true returns the value, toPrint == false returns the object
   findValue: (parsed, toPrint) ->
     splitted = parsed.split(",")
+    # Find the first object in hierarchy
     if !toPrint
       if splitted.length > 1
         variable = @findValueByName(data.game,splitted[0])[0]
@@ -132,6 +144,7 @@
         variable = @findValueByName(data.game,splitted[0])[1]
     else
       variable = @findValueByName(data.game,splitted[0])[0]
+    # Follow the path
     for i in [0 .. splitted.length - 1]
       if Util.isOdd(i)
         variable = variable[parseInt(splitted[i])]


Remove custom Windows behavior

Remove custom Windows behavior

--- src/browser/main.coffee
+++ src/browser/main.coffee
@@ -76,17 +76,6 @@

 setupAtomHome = ->
   return if process.env.ATOM_HOME
-
-  # Use relative .atom path on Windows when using the installer version
-  # in a non-default location. This enables an easy portable version.
-  if process.platform is 'win32'
-    SquirrelUpdate = require './squirrel-update'
-    if SquirrelUpdate.existsSync() and process.env.LOCALAPPDATA
-      atomPath = path.join(process.env.LOCALAPPDATA, 'atom').toLowerCase()
-      if __dirname.toLowerCase().indexOf(atomPath) isnt 0
-        rootAtomFolder = path.resolve(process.execPath, '..', '..')
-        process.env.ATOM_HOME = path.join(rootAtomFolder, '.atom')
-        return

   if process.platform is 'win32'
     home = process.env.USERPROFILE


main project file

main project file

--- src/browser/main.coffee
+++ src/browser/main.coffee
@@ -1,29 +1,26 @@
-crashReporter = require 'crash-reporter'
 app = require 'app'
-fs = require 'fs-plus'
+BrowserWindow = require 'browser-window'
+crashReport = require 'crash-reporter'
 path = require 'path'
-yargs = require 'yargs'
-console.log = require 'nslog'

+start = () ->
+    crashReport.start()

-start = ->
-  setupChizel()
-  args = parseCommandLine()
+    mainWindow = null

-  ChizelApplication = require path.join(args.resourcePath, 'src', 'browser','chizel-application')
-  ChizelApplication.open(args)
+    app.on 'ready', spawnWindow
+    app.on 'closed', destoryWindow

+    #console.log app.getLocale()

-setupChizel = ->
+destoryWindow = () ->
+    @mainWindow = null

+spawnWindow = () ->

+    path = path.dirname __dirname
+    @mainWindow = new BrowserWindow({width:800, height:600})

-parseCommandLine = ->
-
-
-
-
-
-
+    @mainWindow.loadUrl('file://' + path + '/static/index.html' );

 start()


fix(system-tray): Add sans-serif as fallback font for system tray unread count

--- src/canvas-utils.coffee
+++ src/canvas-utils.coffee
@@ -81,7 +81,7 @@
     canvas.height = h

     context = canvas.getContext('2d')
-    context.font = '14px Nylas-Pro'
+    context.font = '14px Nylas-Pro, sans-serif'
     context.fillStyle = 'black'
     context.textAlign = 'start'
     context.textBaseline = 'middle'


Fixed date in vm stat data

Fixed date in vm stat data

--- src/api/vm.coffee
+++ src/api/vm.coffee
@@ -1056,13 +1056,16 @@
   vifs = []
   xvds = []
   date = [] #TODO
+  baseDate = json.rrd.lastupdate
+  dateStep = json.rrd.step
+  numStep = json.rrd.rra[0].database.row.length - 1

   $forEach json.rrd.rra[0].database.row, (n, key) ->
     # WARNING! memoryFree is in Kb not in b, memory is in b
     memoryFree.push(n.v[memoryFreeIndex]*1024)
     memoryUsed.push(Math.round(parseInt(n.v[memoryIndex])-(n.v[memoryFreeIndex]*1024)))
     memory.push(parseInt(n.v[memoryIndex]))
-    date.push(key)
+    date.push(baseDate - (dateStep * (numStep - key)))
     # build the multi dimensional arrays
     $forEach cpusIndexes, (value, key) ->
       cpus[key] ?= []


Allow root badges route to allow a format

--- apps/badges/routes.coffee
+++ apps/badges/routes.coffee
@@ -2,27 +2,31 @@
 User = require '../../models/user'
 Organization = require '../../models/organization'
 authenticate = require '../middleware/authenticate'
+_ = require 'underscore'

 util = require 'util'
 fs = require 'fs'

 routes = (app) ->
+  #
+  #INDEX
+  app.get '/badges.:format?', authenticate, (req, res, next) ->
+    next() if !req.org
+    orgId = req.org.id
+    query = Badge.where('issuer_id', orgId)
+    if req.query.tags?
+      tags = _.flatten(Array(req.query.tags))
+      query.in('tags', tags)
+    query.exec (err, badges)->
+      if req.xhr || req.params.format == 'json'
+        formatBadgeResponse(req, res, badges)
+      else
+        res.render "#{__dirname}/views/index",
+          badges: badges
+          orgId: orgId
+          org: req.org
+
   app.namespace '/badges', authenticate, ->
-    #INDEX
-    app.get '/', (req, res) ->
-      next() if !req.org
-      orgId = req.org.id
-      query = Badge.where('issuer_id', orgId)
-      if req.query.tags?
-        query.in('tags', req.query.tags)
-      query.exec (err, badges)->
-        if req.xhr || req.params.format == 'json'
-          formatResponse(req, res, badges)
-        else
-          res.render "#{__dirname}/views/index",
-            badges: badges
-            orgId: orgId
-            org: req.org

     #NEW
     app.get '/new', (req, res) ->
@@ -122,22 +126,29 @@
               if err?
                 response = {message: "Failed to issue Badge", error: err}
                 console.error "Badge Issue Response: #{JSON.stringify(response)}"
-                return
               else
                 console.log "Badge Issue Response: #{JSON.stringify(response)}"
-                res.send JSON.stringify(response),
-                  'content-type': 'application/json'
-                return
+
+              res.send JSON.stringify(response),
+                'content-type': 'application/json'
+
+
+formatBadgeAssertionResponse = (req, res, badge) ->
+  cb = req.query.callback
+  assertionPromise = badge.toJSON()
+  if cb
+    assertionPromise.on 'complete', (assertion)->
+      res.send "#{cb}(#{JSON.stringify(assertion)})"
+  else
+    res.send assertionPromise,
+      'content-type': 'application/json'

 formatBadgeResponse = (req, res, badge) ->
   cb = req.query.callback
-  assertionPromise = badge.assertion()
   if cb
-    assertionPromise.on 'complete', (assertion)->
-      res.send "#{cb}(#{JSON.stringify(assertion)})",
-
+    res.send "#{cb}(#{JSON.stringify(badge)})"
   else
-    res.send assertionPromise,
+    res.send JSON.stringify(badge),
       'content-type': 'application/json'


show entry in function for all server functions

--- app/server.coffee
+++ app/server.coffee
@@ -27,11 +27,17 @@
   build_parameter_doc = (ctx, doc) ->
     ParameterDocsComponent {ctx, docs: docs.parameter_docs[doc.parameter_name]}

-  _.each docs.function_docs, (d, n) ->
-    Documentation.register_documentation ['server', 'functions', n], function_name: n, summary: d.signature, complete: build_function_doc
-
-  _.each docs.parameter_docs, (d, n) ->
-    Documentation.register_documentation ['server', 'parameters', n], parameter_name: n, summary: 'A server parameter', complete: build_parameter_doc
+  initDocs = ->
+    _.each _.sortBy(function_names, _.identity), (n) ->
+      d = docs.function_docs[n]
+      if d?
+        value = function_name: n, summary: d.signature, complete: build_function_doc
+      else
+        value = summary: '(undocumented)'
+      Documentation.register_documentation ['server', 'functions', n], value
+
+    _.each docs.parameter_docs, (d, n) ->
+      Documentation.register_documentation ['server', 'parameters', n], parameter_name: n, summary: 'A server parameter', complete: build_parameter_doc

   Documentation.register_documentation ['server', 'functions'], index: true
   Documentation.register_documentation ['server', 'parameters'], index: true
@@ -178,8 +184,11 @@
       unless function_names
         functions_promise = http.get(server.url 'functions').then (functions) ->
           function_names = _.filter Object.keys(functions), (f) -> f.indexOf('-') == -1
+          initDocs()
     else
       function_names = graphite_function_names
+      initDocs()
+      function_names

   MetricTreeComponent: React.createClass
     render: ->


refactoring Color2 constructor

refactoring Color2 constructor

--- src/color2.coffee
+++ src/color2.coffee
@@ -43,14 +43,14 @@
     return undefined unless channels = hslParser.exec(colorString)

     parsedColor = (parseFloat channel for channel in channels[1..3])
-    parsedColor[0] = parsedColor[0].round()
+    parsedColor[0] = parsedColor[0]
     parsedColor[3] ||= 1.0

     return hslToRgb(parsedColor)

   hslToRgb = (hsl) ->
     [h, s, l, a] = (parseFloat(channel) for channel in hsl)
-    h = h.round()
+
     h /= 360.0
     a ||= 1.0

@@ -76,37 +76,40 @@

       rgbMap = (channel * 0xFF for channel in [r, g, b])

-      rgbMap[0] = rgbMap[0].round()
-      rgbMap[1] = rgbMap[1].round()
-      rgbMap[2] = rgbMap[2].round()
-
     return rgbMap.concat(a)

   normalizeKey = (key) ->
     key.toString().toLowerCase().split(' ').join('')

+  channelize = (color, alpha) ->
+    if Object.isArray color
+      if alpha?
+        alpha = parseFloat(alpha)
+      else if color[3]?
+        alpha = parseFloat(color[3])
+      else
+        alpha = 1
+
+      result = (parseFloat(channel) for channel in color[0..2]).concat(alpha)
+    else
+      result = lookup[normalizeKey(color)] || parseHex(color) || parseRGB(color) || parseHSL(color)
+
+      if alpha?
+        result[3] = parseFloat(alpha)
+
+    return result
+
   Color2 = (args...) ->
-    switch args.length
-      when 0
-        parsedColor = [0, 0, 0, 1]
-      when 1
-        if Object.isArray(color = args.first())
-          parsedColor = (parseFloat(channel) for channel in color)
-          parsedColor[3] ||= 1.0
-        else
-          parsedColor = lookup[normalizeKey(color)] || parseHex(color) || parseRGB(color) || parseHSL(color)
-      when 2
-        alpha = parseFloat(args[1])
-
-        if Object.isArray(color = args.first())
-          parsedColor = (parseFloat(channel) for channel in color)
-          parsedColor[3] = alpha
-        else
-          parsedColor = lookup[normalizeKey(color)] || parseHex(color) || parseRGB(color) || parseHSL(color)
-          parsedColor[3] = alpha
-      else
-        parsedColor = (parseFloat(channel) for channel in args)
-        parsedColor[3] ||= 1.0
+    parsedColor =
+      switch args.length
+        when 0
+          [0, 0, 0, 1]
+        when 1
+          channelize(args.first())
+        when 2
+          channelize(args.first(), args.last())
+        else
+          channelize(args)

     throw "#{args.join(',')} is an unknown color" unless parsedColor

@@ -200,9 +203,6 @@
         (array[0] * amount) + (array[1] * (1 - amount))

       [@r, @g, @b, @a] = mixedColors
-      @r = @r.round()
-      @g = @g.round()
-      @b = @b.round()

       return this


ping: Also support specifying URL and METHOD as envvars

--- src/common.coffee
+++ src/common.coffee
@@ -74,7 +74,9 @@
   if not options.pingInterval and process.env['MSGFLO_PING_INTERVAL']
     options.pingInterval = parseInt process.env['MSGFLO_PING_INTERVAL']
   options.pingInterval = 0 if not options.pingInterval # default: never
+  options.pingMethod = process.env['MSGFLO_PING_METHOD'] if not options.pingMethod
   options.pingMethod = 'POST' if not options.pingMethod
+  options.pingUrl = process.env['MSGFLO_PING_URL'] if not options.pingUrl
   options.pingUrl = "https://api.flowhub.io/runtimes/$RUNTIME_ID" if not options.pingUrl
   options.pingUrl = options.pingUrl.replace '$RUNTIME_ID', options.runtimeId


userId in env var

userId in env var

--- src/config.coffee
+++ src/config.coffee
@@ -50,6 +50,7 @@
 	apiKey: process.env.MULTIVISOR_API_KEY
 	deviceType: process.env.MULTIVISOR_DEVICE_TYPE
 	username: process.env.MULTIVISOR_USERNAME
+	userId: process.env.MULTIVISOR_USER_ID
 	apps: _.map appIds, (appId, ind) ->
 		app = { appId }
 		if isPreloaded


remove old config.coffee

remove old config.coffee

--- src/config.coffee
+++ src/config.coffee
@@ -1,7 +0,0 @@
-angular.module('config', []).constant 'config',
-  environment: 'development'
-  apiEndpoint: 'http://localhost:3000'
-  googClient: '@@googClient'
-  googApiKey: '@@googApiKey'
-
-


Use path name in config error messsages

Use path name in config error messsages

--- src/config.coffee
+++ src/config.coffee
@@ -832,10 +832,17 @@
       @configFileHasErrors = false
     catch error
       @configFileHasErrors = true
-      if error.location?
-        @notifyFailure('Failed to load config.cson', error.stack)
+      fileName = path.basename(@configFilePath)
+      message = "Failed to load `#{fileName}`"
+
+      detail = if error.location?
+        # stack is the output from CSON in this case
+        error.stack
       else
-        @notifyFailure('Failed to load config.cson', error.message)
+        # message will be EACCES permission denied, et al
+        error.message
+
+      @notifyFailure(message, detail)

   observeUserConfig: ->
     try
@@ -843,8 +850,8 @@
         @debouncedLoad() if eventType is 'change' and @watchSubscription?
     catch error
       @notifyFailure """
-        Unable to watch path: `config.cson`. Make sure you have permissions to
-        `~/.atom/config.cson`. On linux there are currently problems with watch
+        Unable to watch path: `#{path.basename(@configFilePath)}`. Make sure you have permissions to
+        `#{@configFilePath}`. On linux there are currently problems with watch
         sizes. See [this document][watches] for more info.
         [watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path
       """


Ace: loading external script fixed

Ace: loading external script fixed

--- client/ace/lib/ace.coffee
+++ client/ace/lib/ace.coffee
@@ -10,11 +10,26 @@
 $ = require 'jquery'
 settings = require './settings'
 getscript = require 'getscript'
-
-
-aceLoaded = false
+_ = require 'underscore'
+

 module.exports = class Ace extends KDView
+
+  ACE_LOADED = false
+  @registerStaticEmitter()
+
+  getscript '/a/p/p/thirdparty/ace/_ace.js', (err) =>
+
+    throw err  if err
+
+    ace.config.set 'basePath', '/a/p/p/thirdparty/ace'
+    ace.config.set 'themePath', '/a/p/p/thirdparty/ace'
+    ace.config.set 'modePath', '/a/p/p/thirdparty/ace'
+    ace.config.set 'workerPath', '/a/p/p/thirdparty/ace'
+
+    ACE_LOADED = true
+    Ace.emit 'ScriptLoaded'
+

   constructor:(options, file)->

@@ -33,61 +48,51 @@
     @hide()
     @appStorage.fetchStorage (storage)=>

-      onAceLoad = =>
-
-        @keyHandlers = {}
-
-        @fetchContents (err, contents)=>
-          notification?.destroy()
-          id = "editor#{@getId()}"
-          return  unless @getElement().querySelector "##{id}"
-          @editor = ace.edit id
-          @prepareEditor()
-          if contents
-            @setContents contents
-            @lastSavedContents = contents
-
-          @editor.on 'change', =>
-            @emit 'FileContentChanged'  unless @suppressListeners
-            @emit 'FileContentRestored'  unless @isCurrentContentChanged()
-
-          @editor.gotoLine 0
-
-          # remove cmd+L binding. we have already defined cmd+g for this purpose
-          @editor.commands.removeCommand 'gotoline'
-
-          # we are using ctrl+alt+s for 'Save All' action
-          @editor.commands.removeCommand 'sortlines'
-
-          @focus()
-          @show()
-
-          kd.utils.defer => @emit 'ace.ready'
-
-          mixpanel 'Open Ace, success'
-
-        @once 'ace.ready', =>
-          LineWidgets = ace.require('ace/line_widgets').LineWidgets
-          @Range      = ace.require('ace/range').Range
-          @Anchor     = ace.require('ace/anchor').Anchor
-
-          @lineWidgetManager = new LineWidgets @editor.session
-          @lineWidgetManager.attach @editor
-
-
-      unless aceLoaded
-        getscript '/a/p/p/thirdparty/ace/_ace.js', (err) ->
-          throw err  if err
-
-          ace.config.set 'basePath', '/a/p/p/thirdparty/ace'
-          ace.config.set 'themePath', '/a/p/p/thirdparty/ace'
-          ace.config.set 'modePath', '/a/p/p/thirdparty/ace'
-          ace.config.set 'workerPath', '/a/p/p/thirdparty/ace'
-
-          aceLoaded = true
-          onAceLoad()
-      else
-        onAceLoad()
+    if ACE_LOADED
+    then @scriptLoaded()
+    else Ace.once 'ScriptLoaded', @bound 'scriptLoaded'
+
+
+  scriptLoaded: ->
+
+    @keyHandlers = {}
+
+    @fetchContents (err, contents)=>
+      notification?.destroy()
+      id = "editor#{@getId()}"
+      return  unless @getElement().querySelector "##{id}"
+      @editor = ace.edit id
+      @prepareEditor()
+      if contents
+        @setContents contents
+        @lastSavedContents = contents
+
+      @editor.on 'change', =>
+        @emit 'FileContentChanged'  unless @suppressListeners
+        @emit 'FileContentRestored'  unless @isCurrentContentChanged()
+
+      @editor.gotoLine 0
+
+      # remove cmd+L binding. we have already defined cmd+g for this purpose
+      @editor.commands.removeCommand 'gotoline'
+
+      # we are using ctrl+alt+s for 'Save All' action
+      @editor.commands.removeCommand 'sortlines'
+
+      @focus()
+      @show()
+
+      kd.utils.defer => @emit 'ace.ready'
+
+      mixpanel 'Open Ace, success'
+
+    @once 'ace.ready', =>
+      LineWidgets = ace.require('ace/line_widgets').LineWidgets
+      @Range      = ace.require('ace/range').Range
+      @Anchor     = ace.require('ace/anchor').Anchor
+
+      @lineWidgetManager = new LineWidgets @editor.session
+      @lineWidgetManager.attach @editor

   setContent: (content, emitFileContentChangedEvent = yes) ->


Fixed a bug where Type would fall outside a resource when using MetaData or Properties explicitly

--- src/coffin.coffee
+++ src/coffin.coffee
@@ -100,8 +100,8 @@
   _resourceByType: (type, name, props) =>
     result = {}
     if props?.Metadata? or props?.Properties?
+      props.Type = type
       result[name] = props
-      result.Type = type
     else
       result[name] =
         Type: type


make this optional

make this optional

--- lib/tree.coffee
+++ lib/tree.coffee
@@ -3,7 +3,7 @@
 {div, span} = views.tags

 module.exports =
-  treeView: (head, children, {expand}) ->
+  treeView: (head, children, {expand}={}) ->
     view = views.render div 'ink tree', [
       span 'icon icon-chevron-right open'
       div 'header gutted', head


tweaks

tweaks

--- lib/tree.coffee
+++ lib/tree.coffee
@@ -1,7 +1,7 @@
 {$, $$} = require 'atom-space-pen-views'

 module.exports =
-  treeView: (head, children) ->
+  treeView: (head, children, {expand}) ->
     view = $$ ->
       @div class: 'ink tree', =>
         @span class: 'icon icon-chevron-right'
@@ -10,16 +10,18 @@
     view.find('> .header').append head
     view.find('> .body').append child for child in children

-    view.find('> .body').hide()
+    view.find('> .body').hide() unless expand
     view.find('> .icon').click => @toggle view
     view.find('> .header').click => @toggle view

-    view
+    view[0]

   toggle: (view) ->
-    view.find('> .body').toggle()
+    view = $(view)
+    body = view.find('> .body')
     icon = view.find('> .icon')
-    if not view.visible
+    body.toggle()
+    if body.isVisible()
       view.visible = true
       icon.removeClass 'icon-chevron-right'
       icon.addClass 'icon-chevron-down'


removed subscriptions in helper

removed subscriptions in helper

--- client/views/index.coffee
+++ client/views/index.coffee
@@ -14,8 +14,3 @@

 Template.index.events
   'click .restart-tag': (e, tpl) -> Meteor.call 'restartTag', @tag
-
-Template.index.created = ->
-  Meteor.subscribe 'apps'
-  Meteor.subscribe 'applicationDefs'
-  Meteor.subscribe 'instances'


config: Set app from GUV_APP envvar by default

Documentation promised this but apparently was not done

--- src/config.coffee
+++ src/config.coffee
@@ -68,6 +68,7 @@
   # TODO: have a way of declaring these functions in JSON schema?
   c.statuspage = process.env['STATUSPAGE_ID'] if not c.statuspage
   c.broker = process.env['GUV_BROKER'] if not c.broker
+  c.app = process.env['GUV_APP'] if not c.app
   c.broker = process.env['CLOUDAMQP_URL'] if not c.broker
   c.errors = [] # avoid shared ref
   if role != '*'


thingy: Throw exception if #connect called twice; optional on_sync parameter to #connect.

--- client/thingy.coffee
+++ client/thingy.coffee
@@ -161,6 +161,7 @@
       for id, info of @_.partials
         make( id, info )
       @_.partials = {}
+      @_.on_sync()
     else if key == '%create'
       id = value
       info = @_.partials[ id ]
@@ -601,10 +602,16 @@
   #
   # @param [Heron.Dictionary] dictionary Dictionary to connect to.
   # @param [String]           domain     Domain of dictionary to use.
+  # @param [Function]         on_sync    Function to call once in sync.
+  #   Default: nop
   # @return [Heron.Thingy] this
-  connect: ( dictionary, domain ) ->
+  # @throw [String] if called a second time.
+  connect: ( dictionary, domain, on_sync = -> ) ->
+    if @_.dictionary?
+      throw "Already connected to a dictionary."
     @_.dictionary = dictionary
     @_.domain     = domain
+    @_.on_sync    = on_sync

     # Subscribe to domain.
     receiver = new Receiver( this )


Deleting CoffeeScript in favor of pure JS.

--- src/device.coffee
+++ src/device.coffee
@@ -1,203 +0,0 @@
-# Device.js
-# (c) 2014 Matthew Hudson
-# Device.js is freely distributable under the MIT license.
-# For all details and documentation:
-# http://matthewhudson.me/projects/device.js/
-
-# Save the previous value of the device variable.
-previousDevice = window.device
-
-# Add device as a global object.
-window.device = {}
-
-# The <html> element.
-_doc_element = window.document.documentElement
-
-# The client user agent string.
-# Lowercase, so we can use the more efficient indexOf(), instead of Regex
-_user_agent = window.navigator.userAgent.toLowerCase()
-
-
-# Main functions
-# --------------
-
-device.ios = ->
-  device.iphone() or device.ipod() or device.ipad()
-
-device.iphone = ->
-  _find 'iphone'
-
-device.ipod = ->
-  _find 'ipod'
-
-device.ipad = ->
-  _find 'ipad'
-
-device.android = ->
-  _find 'android'
-
-device.androidPhone = ->
-  device.android() and _find 'mobile'
-
-# See: http://android-developers.blogspot.com/2010/12/android-browser-user-agent-issues.html
-device.androidTablet = ->
-  device.android() and not _find 'mobile'
-
-device.blackberry = ->
-  _find('blackberry') or _find('bb10') or _find('rim')
-
-device.blackberryPhone = ->
-  device.blackberry() and not _find 'tablet'
-
-# See: http://supportforums.blackberry.com/t5/Web-and-WebWorks-Development/How-to-detect-the-BlackBerry-Browser/ta-p/559862
-device.blackberryTablet = ->
-  device.blackberry() and _find 'tablet'
-
-device.windows = ->
-  _find 'windows'
-
-device.windowsPhone = ->
-  device.windows() and _find 'phone'
-
-device.windowsTablet = ->
-  device.windows() and (_find('touch') and not device.windowsPhone())
-
-device.fxos = ->
-  (_find('(mobile;') or _find('(tablet;')) and _find('; rv:')
-
-device.fxosPhone = ->
-  device.fxos() and _find 'mobile'
-
-device.fxosTablet = ->
-  device.fxos() and _find 'tablet'
-
-device.meego = ->
-  _find 'meego'
-
-device.cordova = ->
-  window.cordova && location.protocol == 'file:'
-
-device.nodeWebkit = ->
-  typeof window.process == 'object'
-
-device.mobile = ->
-  device.androidPhone() or device.iphone() or device.ipod() or device.windowsPhone() or device.blackberryPhone() or device.fxosPhone() or device.meego()
-
-device.tablet = ->
-  device.ipad() or device.androidTablet() or device.blackberryTablet() or device.windowsTablet() or device.fxosTablet()
-
-device.desktop = ->
-  not device.tablet() and not device.mobile()
-
-device.portrait = ->
-  (window.innerHeight/window.innerWidth) > 1
-
-device.landscape = ->
-  (window.innerHeight/window.innerWidth) < 1
-
-# Run device.js in noConflict mode, returning the device variable to its previous owner.
-# Returns a reference to the device object.
-device.noConflict = ->
-  window.device = previousDevice
-  @
-
-# Private Utility
-# ---------------
-
-# Simple UA string search
-_find = (needle) ->
-  _user_agent.indexOf(needle) isnt -1
-
-# Check if docElement already has a given class.
-_hasClass = (class_name) ->
-  regex = new RegExp class_name, 'i'
-  _doc_element.className.match regex
-
-# Add one or more CSS classes to the <html> element.
-_addClass = (class_name) ->
-  if not _hasClass class_name
-    _doc_element.className += " " + class_name
-
-# Remove single CSS class from the <html> element.
-_removeClass = (class_name) ->
-  if _hasClass class_name
-    _doc_element.className = _doc_element.className.replace class_name, ""
-
-
-# HTML Element Handling
-# ---------------------
-
-# Insert the appropriate CSS class based on the _user_agent.
-if device.ios()
-  if device.ipad()
-    _addClass "ios ipad tablet"
-  else if device.iphone()
-    _addClass "ios iphone mobile"
-  else if device.ipod()
-    _addClass "ios ipod mobile"
-
-else if device.android()
-  if device.androidTablet()
-    _addClass "android tablet"
-  else
-    _addClass "android mobile"
-
-else if device.blackberry()
-  if device.blackberryTablet()
-    _addClass "blackberry tablet"
-  else
-    _addClass "blackberry mobile"
-
-else if device.windows()
-  if device.windowsTablet()
-    _addClass "windows tablet"
-  else if device.windowsPhone()
-    _addClass "windows mobile"
-  else
-    _addClass "desktop"
-
-else if device.fxos()
-  if device.fxosTablet()
-    _addClass "fxos tablet"
-  else
-    _addClass "fxos mobile"
-
-else if device.meego()
-  _addClass "meego mobile"
-
-else if device.nodeWebkit()
-  _addClass "node-webkit"
-
-else
-  _addClass "desktop"
-
-if device.cordova()
-  _addClass "cordova"
-
-
-# Orientation Handling
-# --------------------
-
-# Handle device orientation changes
-_handleOrientation = ->
-  if device.landscape()
-    _removeClass "portrait"
-    _addClass "landscape"
-  else
-    _removeClass "landscape"
-    _addClass "portrait"
-
-# Detect whether device supports orientationchange event,
-# otherwise fall back to the resize event.
-_supports_orientation = "onorientationchange" of window
-_orientation_event = if _supports_orientation then "orientationchange" else "resize"
-
-# Listen for changes in orientation.
-if window.addEventListener
-  window.addEventListener _orientation_event, _handleOrientation, no
-else if window.attachEvent
-  window.attachEvent _orientation_event, _handleOrientation
-else
-  window[_orientation_event] = _handleOrientation
-
-_handleOrientation()


Fix Dialog

Fix Dialog

--- src/dialog.coffee
+++ src/dialog.coffee
@@ -1,7 +1,6 @@
 # Dialog class for hubot
 #
-# dialog = new Dialog msg # set username and room
-# dialog.listen callback # callback can use dialog as @ params are [msg]
+# dialog = new Dialog msg, callback # set username and room
 # dialog.set key, value # set data to robot.brain.data.dialogs[user/room]
 # dialog.end # end dialog and destroy data
 #
@@ -22,7 +21,7 @@
     Dialog.dialogs[Dialog.getKey user, room]

   @addDialog: (dialog) ->
-    Dialog.dialogs[Dialog.getKey dialog.user, dialog.room]
+    Dialog.dialogs[Dialog.getKey dialog.user, dialog.room] = dialog

   @deleteDialog: (user, room) ->
     delete Dialog.dialogs[Dialog.getKey user, room]
@@ -38,26 +37,34 @@
     @robot = msg.robot
     @user = msg.envelope.user.name
     @room = msg.envelope.room
-    @key = Dialog.getKey user, room
+    @key = Dialog.getKey @user, @room
     @data = {}
     @listen callback
+    Dialog.addDialog @

   set: (key, value) ->
     @data[key] = value
-    @
+    @save()

   get: (key) ->
     @data[key]
+
+  reset: ->
+    @data = {}
+    @save

   listen: (callback) ->
     @callback = callback
     @

-  save: ->
+  save: ()->
     @robot.brain.data.dialogs?[@key] = @data
+    @

   end: ->
     Dialog.deleteDialog @user, @room
     @robot.brain.data.dialogs?[@key] = null
     delete @robot.brain.data.dialogs?[@key]
     @user = @room = @data = @callback = @callback = null
+
+module.exports = Dialog


add post event hook

add post event hook

--- src/editor.coffee
+++ src/editor.coffee
@@ -134,6 +134,7 @@
   @events:
     API_TEXT_CHANGE  : 'api-text-change'
     PRE_EVENT        : 'pre-event'
+    POST_EVENT       : 'post-event'
     SELECTION_CHANGE : 'selection-change'
     TEXT_CHANGE      : 'text-change'

@@ -193,6 +194,7 @@
   emit: (eventName, args...) ->
     super(Scribe.Editor.PRE_EVENT, eventName, args...)
     super(eventName, args...)
+    super(Scribe.Editor.POST_EVENT, eventName, args...)

   deleteAt: (index, length, external = true) ->
     doAt.call(this, =>


Add time stamps to all transactions which can view viewed with +history

--- bot.iced
+++ bot.iced
@@ -120,6 +120,7 @@
 					"status": "pending"
 					"tx": body.tx
 					"address": body.address
+					"time": new Date().toDateString()
 				await Users_collection.update {id: chatterID}, {$set: {lastAddFundTx: body.tx}, $push:{history: fundTx}}, {w:1}, defer(err)
 				if err
 					console.error err
@@ -204,6 +205,8 @@
 						message += "\n\tType: sent tip, Amount: #{item.amount}, Recipient: #{item.recipient}"
 					when "received tip"
 						message += "\n\tType: received tip, Amount: #{item.amount}, Sender: #{item.sender}"
+				if item.time
+					message += ", Date: #{item.time} (EST)"
 			bot.sendMessage chatterID, message
 		when "+withdraw"
 			await checkIfRegistered chatterID, defer(registered, user)
@@ -255,6 +258,7 @@
 					"amount": -amount
 					"status": "sent"
 					"address": address
+					"time": new Date().toDateString()
 				await Users_collection.update {id: chatterID}, {$inc: {funds: -(amount + 1)}, $push:{history: withdrawTx}}, {w:1}, defer(err)
 				if err
 					console.error err
@@ -312,6 +316,7 @@
 				"amount": -amount
 				"status": "sent"
 				"recipient": shibe
+				"time": new Date().toDateString()
 			await Users_collection.update {id: chatterID}, {$inc: {funds: -amount}, $push:{history: tip1Tx}}, {w:1}, defer(err)
 			if err
 				console.error err
@@ -322,6 +327,7 @@
 				"amount": amount
 				"status": "received"
 				"sender": user.name
+				"time": new Date().toDateString()
 			await Users_collection.update {id: shibeID}, {$inc: {funds: amount}, $push:{history: tip2Tx}}, {w:1}, defer(err)
 			if err
 				console.error err


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine.coffee
+++ src/engine.coffee
@@ -73,7 +73,7 @@

       if I.showFPS
         canvas.fillColor("#FFF")
-        canvas.fillText("fps: " + framerate.fps)
+        canvas.fillText("fps: " + framerate.fps, 0, 0)

       framerate.rendered()


Refactor error.coffee

Refactor error.coffee

Now follows DRY practice.

--- src/errors.coffee
+++ src/errors.coffee
@@ -1,47 +1,46 @@

 { availableKeys, availableFormats } = require './api_config'

-NoKeySuppliedError = ->
-  self = new Error "You have to supply as least one API key."
-  self.name = 'NoKeySuppliedError'
-  self.__proto__ = NoKeySuppliedError::
-  self
-NoKeySuppliedError::__proto__ = Error::
+exported = {}

-InvalidKeyNameSuppliedError = (key) ->
-  self = new Error "The supplied key '#{key}' should be one of \
-                    the following #{availableKeys.join(', ')}"
-  self.name = 'InvalidKeyNameSuppliedError'
-  self.__proto__ = InvalidKeyNameSuppliedError::
-  self
-InvalidKeyNameSuppliedError::__proto__ = Error::
+errors = [
+  {
+    error: 'NoKeySuppliedError'
+    msg: ->
+          "One of the following API keys have to be supplied \
+           upon instantiation: #{availableKeys.join(', ')}."
+  }, {
+    error: 'InvalidKeyNameSuppliedError'
+    msg: (key) ->
+          "The supplied key '#{key}' should be one of \
+          the following #{availableKeys.join(', ')}"
+  }, {
+    error: 'NoKeySuppliedForServiceError'
+    msg: (service) ->
+          "You have not supplied an API key for the #{service} \
+          service which you attempted to use."
+  }, {
+    error: 'InvalidKeyFormatSupplied'
+    msg: ->
+          "The supplied API key(s) has to be an object following the format: \
+          { realtimeInformation: <API key from trafiklab.se> }"
+  }, {
+    error: 'InvalidResponseFormatSuppliedError'
+    msg: (format) ->
+          "'#{format}' which you supplied is not supported. \
+          Use either #{availableFormats.join(', ')} or omit \
+          format completely to get the response as a JavaScript object."
+  }
+]

-NoKeySuppliedForServiceError = (service) ->
-  self = new Error "You have not supplied an API key for the #{service} service."
-  self.name = 'NoKeySuppliedForServiceError'
-  self.__proto__ = NoKeySuppliedForServiceError::
-  self
-NoKeySuppliedForServiceError::__proto__ = Error::
+alias = (obj, error) ->
+  obj[error.error] = (arg) ->
+    self = new Error error.msg(arg)
+    self.name = error.error
+    self.__proto__ = obj[error.error]::
+    self
+  obj[error.error]::__proto__ = Error::

-InvalidKeyFormatSupplied = (service) ->
-  self = new Error "..."
-  self.name = 'InvalidKeyFormatSupplied'
-  self.__proto__ = InvalidKeyFormatSupplied::
-  self
-InvalidKeyFormatSupplied::__proto__ = Error::
+alias exported, error for error in errors

-InvalidResponseFormatSuppliedError = (format) ->
-  self = new Error "#{format} which you supplied is not supported. \
-                    Use either #{availableFormats.join(', ')} or omit \
-                    format completely to get the response as a JavaScript object."
-  self.name = 'InvalidResponseFormatSuppliedError'
-  self.__proto__ = InvalidResponseFormatSuppliedError::
-  self
-InvalidResponseFormatSuppliedError::__proto__ = Error::
-
-module.exports =
-  NoKeySuppliedError: NoKeySuppliedError
-  InvalidKeyNameSuppliedError: InvalidKeyNameSuppliedError
-  NoKeySuppliedForServiceError: NoKeySuppliedForServiceError
-  InvalidResponseFormatSuppliedError: InvalidResponseFormatSuppliedError
-  InvalidKeyFormatSupplied: InvalidKeyFormatSupplied
+module.exports = exported


Add a new error for the outcomes service

--- src/errors.coffee
+++ src/errors.coffee
@@ -13,6 +13,9 @@
 class NonceError extends Error
   constructor: ->
     super
+class OutcomeResponseError extends Error
+  constructor: ->
+    super

 module.exports =
   ConsumerError: ConsumerError
@@ -20,3 +23,4 @@
   ParameterError: ParameterError
   SignatureError: SignatureError
   NonceError: NonceError
+  OutcomeResponseError: OutcomeResponseError


Use https url

Use https url

--- assets/data/constant.cson
+++ assets/data/constant.cson
@@ -9,7 +9,7 @@
   ,
     name: 'Kancolle English Wikia'
     link: 'http://kancolle.wikia.com/wiki/Kancolle_Wiki'
-    avatar: 'http://vignette4.wikia.nocookie.net/kancolle/images/8/89/Wiki-wordmark.png/revision/latest?cb=20141226051229'
+    avatar: 'https://vignette4.wikia.nocookie.net/kancolle/images/8/89/Wiki-wordmark.png/revision/latest?cb=20141226051229'
     description: 'For providing English translation.'
     extraCSS:
       WebkitClipPath: "inset(0px 74% 0px 0px)"
@@ -33,7 +33,7 @@
   ,
     name: 'Kensuke Tanaka'
     link: 'https://www.facebook.com/kensuke.tanaka.790'
-    avatar: 'http://www.famitsu.com/images/000/039/658/l_522b1c633860a.jpg'
+    avatar: 'https://www.famitsu.com/images/000/039/658/l_522b1c633860a.jpg'
     description: 'For providing development difficulties (which we enjoyed a lot, sincerely).'
     extraCSS:
       maxHeight: "200%"


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/follow.coffee
+++ src/follow.coffee
@@ -45,7 +45,8 @@
 ###
 Follow = (I={}, self) ->
   Object.reverseMerge I,
-    velocity: Point(0, 0)
+    velocity: Point(0, 0)
+    speed:

   ###*
   Set your direction to face another object.


follow docs

follow docs

--- src/follow.coffee
+++ src/follow.coffee
@@ -3,7 +3,7 @@
 velocity so that it will approach another object.

-
+#
 player = GameObject
   health: 50


fix #55 for other formats

fix #55 for other formats

--- src/format.coffee
+++ src/format.coffee
@@ -136,6 +136,9 @@
   constructor: (@root) ->
     super(@root, 'bold', 'B')

+  matchContainer: (container) ->
+    return super(container) or container.style?.fontWeight == 'bold'
+

 class ScribeItalicFormat extends ScribeTagFormat
   constructor: (@root) ->
@@ -149,6 +152,9 @@
   constructor: (@root) ->
     super(@root, 'strike', 'S')

+  matchContainer: (container) ->
+    return super(container) or container.style?.textDecoration == 'line-through'
+
   preformat: (value) ->
     @root.ownerDocument.execCommand('strikeThrough', false, value)

@@ -156,6 +162,9 @@
 class ScribeUnderlineFormat extends ScribeTagFormat
   constructor: (@root) ->
     super(@root, 'underline', 'U')
+
+  matchContainer: (container) ->
+    return super(container) or container.style?.textDecoration == 'underline'

 class ScribeLinkFormat extends ScribeTagFormat


Fix wrong indentation

Fix wrong indentation

Lines 140 to 142 were indented one more level than they should be.

--- src/gitlab.coffee
+++ src/gitlab.coffee
@@ -137,9 +137,9 @@
                 if showMergeDesc == "1"
                   text = "Merge Request #{bold(hook.object_attributes.iid)}: #{hook.user.username} #{hook.object_attributes.action}ed  #{hook.object_attributes.title} (#{hook.object_attributes.state}) between #{bold(hook.object_attributes.source_branch)} and #{bold(hook.object_attributes.target_branch)} at #{bold(hook.object_attributes.url)}\n"
                   splitted = hook.object_attributes.description.split  "\r\n"
-                    for i in [0...splitted.length]
-                      splitted[i] = "> " + splitted[i]
-                    text += "\r\n" + splitted.join "\r\n"
+                  for i in [0...splitted.length]
+                    splitted[i] = "> " + splitted[i]
+                  text += "\r\n" + splitted.join "\r\n"
                   robot.send user, text
                 else
                   robot.send user, "Merge Request #{bold(hook.object_attributes.iid)}: #{hook.user.username} #{hook.object_attributes.action}ed  #{hook.object_attributes.title} (#{hook.object_attributes.state}) between #{bold(hook.object_attributes.source_branch)} and #{bold(hook.object_attributes.target_branch)} at #{bold(hook.object_attributes.url)}"


Indentation to make clear what is happening

--- code/instance.coffee
+++ code/instance.coffee
@@ -130,10 +130,10 @@
   # or DDD_something.l.ext (locally).
   _call_hook: (hook, callback) =>
     if /^\d+_.+\.r\.\w/.test hook.file
-      console.log "Running remote #{hook.file}"
+      console.log "  Running remote #{hook.file}"
       @sh "sh /root/#{hook.file} #{@name}", callback
     else if /^\d+_.+\.l\.\w/.test hook.file
-      console.log "Running local #{hook.file}"
+      console.log "  Running local #{hook.file}"
       hooks_dir="#{settings.config_path}/#{hook.config_name}/hooks"
       oldcwd = process.cwd()
       process.chdir hooks_dir


Fixes #3089 -- don't mutate options passed in to compile()

--- coffee-script.coffee
+++ coffee-script.coffee
@@ -38,7 +38,8 @@
 # object, where sourceMap is a sourcemap.coffee#SourceMap object, handy for doing programatic
 # lookups.
 exports.compile = compile = withPrettyErrors (code, options) ->
-  {merge} = helpers
+  {merge, extend} = helpers
+  options = extend {}, options

   if options.sourceMap
     map = new SourceMap


Remove ObjectID require from server

Remove ObjectID require from server

--- coffee/server.coffee
+++ coffee/server.coffee
@@ -3,7 +3,6 @@
 pg = require 'pg'
 helpers = require './query-helpers'
 config = require './config'
-ObjectID = require('mongodb').ObjectID

 pg.connect config.conString, (err, client) ->
   return console.error err if err


Rename matchSubnet to subnetMatch.

Rename matchSubnet to subnetMatch.

--- src/ipaddr.coffee
+++ src/ipaddr.coffee
@@ -28,7 +28,7 @@
   return true

 # An utility function to ease named range matching. See examples below.
-ipaddr.matchSubnet = (address, rangeList, defaultName='unicast') ->
+ipaddr.subnetMatch = (address, rangeList, defaultName='unicast') ->
   for rangeName, rangeSubnets of rangeList
     # ECMA5 Array.isArray isn't available everywhere
     if toString.call(rangeSubnets[0]) != '[object Array]'
@@ -98,7 +98,7 @@

   # Checks if the address corresponds to one of the special ranges.
   range: ->
-    return ipaddr.matchSubnet(this, @SpecialRanges)
+    return ipaddr.subnetMatch(this, @SpecialRanges)

   # Convrets this IPv4 address to an IPv4-mapped IPv6 address.
   toIPv4MappedAddress: ->
@@ -201,7 +201,7 @@

   # Checks if the address corresponds to one of the special ranges.
   range: ->
-    return ipaddr.matchSubnet(this, @SpecialRanges)
+    return ipaddr.subnetMatch(this, @SpecialRanges)

   # Checks if this address is an IPv4-mapped IPv6 address.
   isIPv4MappedAddress: ->


:lipstick: spelling of "key binding"

:lipstick: spelling of "key binding"

--- src/keymap.coffee
+++ src/keymap.coffee
@@ -30,7 +30,7 @@
     try
       document.body.webkitMatchesSelector(selector.replace(/!important/g, ''))
     catch
-      console.warn("Encountered an invalid selector adding keybindings from '#{source}': '#{selector}'")
+      console.warn("Encountered an invalid selector adding key bindings from '#{source}': '#{selector}'")
       return

     for keystroke, command of keyBindings
@@ -70,7 +70,7 @@
           and target.webkitMatchesSelector(binding.selector)
       .sort (a, b) -> a.compare(b)

-  # Public: Get the keybindings for a given command and optional target.
+  # Public: Get the key bindings for a given command and optional target.
   #
   # params - An {Object} whose keys constrain the binding search:
   #   :command - A {String} representing the name of a command, such as
@@ -129,7 +129,8 @@
   keystrokeStringForEvent: (event) ->
     @keystrokeForKeyboardEvent(event.originalEvent ? event)

-  # Deprecated: Use {::addKeyBindings} with a map from selectors to keybindings.
+  # Deprecated: Use {::addKeyBindings} with a map from selectors to key
+  # bindings.
   bindKeys: (source, selector, keyBindings) ->
     @addKeyBindingsForSelector(source, selector, keyBindings)


Let hubot address you better

Let hubot address you better

--- src/lieber.coffee
+++ src/lieber.coffee
@@ -21,6 +21,6 @@
     name = msg.match[2].trim()
     switch name
       when 'me'
-        msg.reply "#{msg.message.user.name}, you are #{msg.random phrases}"
+        msg.reply "You are #{msg.random phrases}"
       else
         msg.send "#{name} is #{msg.random phrases}"


introduced: log-scales

introduced: log-scales

--- src/limits.coffee
+++ src/limits.coffee
@@ -95,6 +95,16 @@
         limits.push min
         for i in [1..num-1]
             limits.push min+(i/num)*(max-min)
+        limits.push max
+
+    else if mode.substr(0,1) == 'l' # log scale
+        if min <= 0
+            throw 'Logarithmic scales are only possible for values > 0'
+        min_log = Math.LOG10E * Math.log min
+        max_log = Math.LOG10E * Math.log max
+        limits.push min
+        for i in [1..num-1]
+            limits.push Math.pow 10, min_log + (i/num) * (max_log - min_log)
         limits.push max

     else if mode.substr(0,1) == 'q' # quantile scale


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/matrix.coffee
+++ src/matrix.coffee
@@ -181,7 +181,8 @@

     ###*
     Returns a new matrix that corresponds this matrix multiplied by a
-    a skew matrix.
+    a skewing matrix.
+
     @name skew
     @methodOf Matrix#
     @see Matrix.skew


Pass compilers.

Pass compilers.

--- src/module.coffee
+++ src/module.coffee
@@ -268,11 +268,12 @@
           cached.dependents[@requireAs] = @
         return @traverse dependencies, opts, cb

+      dep.compilers   = @compilers
       dep.moduleCache = @moduleCache
+      dep.naked       = @naked
       dep.resolver    = @resolver
+      dep.strict      = @strict
       dep.urlRoot     = @urlRoot
-      dep.strict      = @strict
-      dep.naked       = @naked

       # create module and parse it
       mod = new Module dep.requiredAs, dep


Ensure a match exists before replacing text with it

Previously if a match was `undefined` (the value!), it was getting
translated to the string `undefined`, and this was causing that word to
be replaced anywhere it was found in the string.

Fixes #15.

--- src/parser.coffee
+++ src/parser.coffee
@@ -64,7 +64,7 @@
     if visibilityMatch = new RegExp(VisibilityRegex).exec(rawSummary)
       visibility = visibilityMatch[1]
       rawVisibility = visibilityMatch[0]
-      rawSummary = rawSummary.replace(rawVisibility, '')
+      rawSummary = rawSummary.replace(rawVisibility, '') if rawVisibility?

   if isReturnValue(rawSummary)
     returnValues = parseReturnValues(tokens, false)
@@ -168,10 +168,15 @@

   returnsMatches = new RegExp(ReturnsRegex).exec(firstToken.text) # there might be a `Public: ` in front of the return.
   if consumeTokensAfterReturn
-    normalizedString = generateDescription(tokens, -> true).replace(returnsMatches[1], '')
+    normalizedString = generateDescription(tokens, -> true)
+    if returnsMatches[1]?
+      normalizedString = normalizedString.replace(returnsMatches[1], '')
   else
     token = tokens.shift()
-    normalizedString = token.text.replace(returnsMatches[1], '').replace(/\s{2,}/g, ' ')
+    normalizedString = token.text
+    if returnsMatches[1]?
+      normalizedString = normalizedString.replace(returnsMatches[1], '')
+    normalizedString = normalizedString.replace(/\s{2,}/g, ' ')

   returnValues = null
@@ -264,7 +269,7 @@

   if nameMatches = new RegExp(ArgumentListItemRegex).exec(argumentString)
     name = nameMatches[1]
-    description = description.replace(nameMatches[0], '')
+    description = description.replace(nameMatches[0], '') if nameMatches[0]?
     type = getLinkMatch(description)
     isOptional = !!nameMatches[3]


Implement a basic keypath parser that tokenizes a keypath into path + interface pairs.

--- src/rivets.coffee
+++ src/rivets.coffee
@@ -348,6 +348,26 @@
     @models[key] = model for key, model of models
     binding.update models for binding in @bindings

+# Rivets.KeypathParser
+# --------------------
+
+# Parser and tokenizer for keypaths in binding declarations.
+class Rivets.KeypathParser
+  # Parses the keypath and returns a set of adapter interface + path tokens.
+  @parse: (keypath, interfaces, root) ->
+    tokens = []
+    current = {interface: root, path: ''}
+
+    for index, char of keypath
+      if char in interfaces
+        tokens.push current
+        current = {interface: char, path: ''}
+      else
+        current.path += char
+
+    tokens.push current
+    tokens
+
 # Rivets.TextTemplateParser
 # -------------------------