:lipstick: Cleanup.

:lipstick: Cleanup.

- Cleanup some spec tests descriptions.

--- spec/jumpy-spec.coffee
+++ spec/jumpy-spec.coffee
@@ -37,24 +37,28 @@
             editorView.trigger 'jumpy:clear'
             expect(editorView.find('.jumpy')).not.toExist()

-    describe "when the jumpy:toggle event is triggered a mousedown event is fired", ->
+    describe "when the jumpy:toggle event is triggered
+        and a mousedown event is fired", ->
         it "jumpy is cleared", ->
             editorView.trigger 'mousedown'
             expect(editorView.find('.jumpy')).not.toExist()

-    describe "when the jumpy:toggle event is triggered a scroll event is fired", ->
+    describe "when the jumpy:toggle event is triggered
+        and a scroll event is fired", ->
         it "jumpy is cleared", ->
             editorView.trigger 'scroll'
             expect(editorView.find('.jumpy')).not.toExist()

-    describe "when the jumpy:toggle event is triggered and hotkeys are entered", ->
+    describe "when the jumpy:toggle event is triggered
+        and hotkeys are entered", ->
         it "jumpy is cleared", ->
             editor.setCursorBufferPosition [1,1]
             editorView.trigger 'jumpy:a'
             editorView.trigger 'jumpy:c'
             expect(editorView.find('.jumpy')).not.toExist()

-    describe "when the jumpy:toggle event is triggered and hotkeys are entered", ->
+    describe "when the jumpy:toggle event is triggered
+        and hotkeys are entered", ->
         it "jumps the cursor", ->
             editor.setCursorBufferPosition [1,1]
             editorView.trigger 'jumpy:a'
@@ -63,8 +67,8 @@
             expect(cursorPosition.row).toBe 0
             expect(cursorPosition.column).toBe 10

-    describe "when the jumpy:toggle event is triggered and hotkeys are entered
-        in succession", ->
+    describe "when the jumpy:toggle event is triggered
+        and hotkeys are entered in succession", ->
         it "jumps the cursor twice", ->
             editor.setCursorBufferPosition [1,1]
             editorView.trigger 'jumpy:a'
@@ -76,7 +80,8 @@
             expect(cursorPosition.row).toBe 1
             expect(cursorPosition.column).toBe 5

-    describe "when the jumpy:toggle event is triggered and hotkeys are entered", ->
+    describe "when the jumpy:toggle event is triggered
+        and hotkeys are entered", ->
         it "the ripple animation class is added", ->
             editorView.trigger 'jumpy:a'
             editorView.trigger 'jumpy:c'


:white_check_mark: Added spec for traversalFrom of Point class

--- spec/point-spec.coffee
+++ spec/point-spec.coffee
@@ -159,3 +159,37 @@

       expect(Point(1, 3).traverse(Point(4, 2)).isEqual([5, 2]))
       expect(Point(1, 3).traverse([5, 4]).isEqual([6, 4]))
+
+  describe "::traversalFrom(other)", ->
+    it "returns a point that other has to traverse to get to given point", ->
+      expect(Point(2, 5).traversalFrom(Point(2, 3)).isEqual(Point(0, 2)))
+      expect(Point(2, 3).traversalFrom(Point(2, 5)).isEqual(Point(0, -2)))
+      expect(Point(2, 3).traversalFrom(Point(2, 3)).isEqual(Point(0, 0)))
+
+      expect(Point(3, 4).traversalFrom(Point(2, 3)).isEqual(Point(1, 4)))
+      expect(Point(2, 3).traversalFrom(Point(3, 5)).isEqual(Point(-1, 3)))
+
+      expect(Point(2, 5).traversalFrom([2, 3]).isEqual([0, 2]))
+      expect(Point(2, 3).traversalFrom([2, 5]).isEqual([0, -2]))
+      expect(Point(2, 3).traversalFrom([2, 3]).isEqual([0, 0]))
+
+      expect(Point(3, 4).traversalFrom([2, 3]).isEqual([1, 4]))
+      expect(Point(2, 3).traversalFrom([3, 5]).isEqual([-1, 3]))
+
+  describe "::toArray()", ->
+    it "returns an array of row and column", ->
+      expect(Point(1, 3).toArray() is [1, 3])
+      expect(Point.zero().toArray() is [0, 0])
+      expect(Point.infinity().toArray() is [Infinity, Infinity])
+
+  describe "::serialize()", ->
+    it "returns an array of row and column", ->
+      expect(Point(1, 3).serialize()  is [1, 3])
+      expect(Point.zero().serialize() is [0, 0])
+      expect(Point.infinity().serialize() is [Infinity, Infinity])
+
+  describe "::toString()", ->
+    it "returns string representation of Point", ->
+      expect(Point(4, 5).toString()  is "(4, 5)")
+      expect(Point.zero().toString() is "(0, 0)")
+      expect(Point.infinity().toString() is "(Infinity, Infinity)")


don't need this

don't need this

--- lib/atom-ternjs-reference-view.coffee
+++ lib/atom-ternjs-reference-view.coffee
@@ -1,12 +1,6 @@
 Helper = require './atom-ternjs-helper'

 class ReferenceView extends HTMLElement
-
-  content: null
-  close: null
-  data: null
-  helper: null
-
   createdCallback: ->
     @classList.add('atom-ternjs-reference')
     container = document.createElement('div')


neaten html

neaten html

--- shared/src/components/question/answer.cjsx
+++ shared/src/components/question/answer.cjsx
@@ -139,18 +139,12 @@
       accessbilityProps =
         tabIndex: 0

-

-

+

+

         {selectedCount}
         {radioBox}
         <label
           {...accessbilityProps}
-
           onKeyPress={_.partial(@onKeyPress, _, answer)}
           htmlFor="#{qid}-option-#{iter}"
           className='answer-label'


go/worker: remove follow feed notifier

go/worker: remove follow feed notifier

--- workers/social/lib/social/followfeed.coffee
+++ workers/social/lib/social/followfeed.coffee
@@ -1,38 +0,0 @@
-JUser     = require './models/user'
-JAccount  = require './models/account'
-
-amqp = require 'amqp'
-
-USER_EXCHANGE_OPTIONS =
-  type        : 'direct'
-  autoDelete  : no
-  durable     : yes
-
-followMQ = amqp.createConnection KONFIG.followfeed
-
-fetchExchangePair = (followerName, followeeName, callback) ->
-  followMQ.exchange followerName, USER_EXCHANGE_OPTIONS, (follower) ->
-    followMQ.exchange followeeName, USER_EXCHANGE_OPTIONS, (followee) ->
-      callback follower, followee
-
-followHelper = (follower, followee, method)->
-  followerName  = follower.profile.nickname
-  followeeName  = followee.profile.nickname
-  fetchExchangePair followerName, followeeName, (follower, followee) ->
-    follower[method] followee, followeeName
-    follower.close()
-    followee.close()
-
-followMQ.on 'ready', ->
-
-  JUser.on 'UserCreated',  (user) ->
-    console.log 'a user is created'
-    followMQ.exchange user.username, USER_EXCHANGE_OPTIONS, (exchange) ->
-      # don't leak a channel; gotta love this driver!
-      exchange.close()
-
-  JAccount.on 'FollowHappened', ({ followee, follower })->
-    followHelper follower, followee, 'bind'
-
-  JAccount.on 'UnfollowHappened', ({ followee, follower })->
-    followHelper follower, followee, 'unbind'


JNewApp: Re-enable bypass app validations for super-admins

--- workers/social/lib/social/models/app.coffee
+++ workers/social/lib/social/models/app.coffee
@@ -185,8 +185,8 @@
       style  : "#{url}/resources/style.css"

     # If user is admin
-    # if account.can 'bypass-validations'
-    #   return callback null, urls, no
+    if account.can 'bypass-validations'
+      return callback null, urls, no

     # If url points to a vm url
     if (/^\[([^\]]+)\]/g.exec url)?[1]


use maps for the dictionaries in DesugarClasses.prototype.gather_members

--- lib/closure-conversion.coffee
+++ lib/closure-conversion.coffee
@@ -223,10 +223,10 @@
                 class_init

         gather_members: (ast_class) ->
-                methods     = Object.create null
-                smethods    = Object.create null
-                properties  = Object.create null
-                sproperties = Object.create null
+                methods     = new Map
+                smethods    = new Map
+                properties  = new Map
+                sproperties = new Map

                 for class_element in ast_class.body.body
                         if class_element.static and class_element.key.name is "prototype"
@@ -234,17 +234,17 @@

                         if class_element.kind is ""
                                 # a method
-                                method_hash = if class_element.static then smethods else methods
-                                throw new SyntaxError "method '#{class_element.key.name}' has already been defined." if method_hash[class_element.key.name]?
-                                method_hash[class_element.key.name] = class_element
+                                method_map = if class_element.static then smethods else methods
+                                throw new SyntaxError "method '#{class_element.key.name}' has already been defined." if method_map.has(class_element.key.name)
+                                method_map.set(class_element.key.name, class_element)
                         else
                                 # a property
-                                property_hash = if class_element.static then sproperties else properties
+                                property_map = if class_element.static then sproperties else properties

-                                property_hash[class_element.key.name] = Object.create(null) if not property_hash[class_element.key.name]?
-
-                                throw new SyntaxError "a '#{class_element.kind}' method for '#{class_element.key.name}' has already been defined." if property_hash[class_element.key.name][class_element.kind]?
-                                property_hash[class_element.key.name][class_element.kind] = class_element
+                                property_map.set(class_element.key.name, new Map) if not property_map.has(class_element.key.name)
+
+                                throw new SyntaxError "a '#{class_element.kind}' method for '#{class_element.key.name}' has already been defined." if property_map.get(class_element.key.name).has(class_element.kind)
+                                property_map.get(class_element.key.name).set(class_element.kind, class_element)

                 [properties, methods, sproperties, smethods]


Center editor after hyperclick

Center editor after hyperclick

--- lib/hyperclickProvider.coffee
+++ lib/hyperclickProvider.coffee
@@ -42,7 +42,10 @@
     if(highlightedKeywords.length==1)
       keyword = highlightedKeywords[0]
       callback = ->
-        atom.workspace.open(keyword.resource.path, {initialLine: keyword.startRowNo, initialColumn: keyword.startColNo}).catch (error) ->
+        atom.workspace.open(keyword.resource.path, {initialLine: keyword.startRowNo, initialColumn: keyword.startColNo})
+        .then (editor) ->
+          editor.scrollToCursorPosition()
+        .catch (error) ->
           console.log "Error opening editor: #{error}"
     else
       callback = []


Don't need this. refs #61

Don't need this. refs #61

--- lib/atom-ternjs-reference.coffee
+++ lib/atom-ternjs-reference.coffee
@@ -41,8 +41,6 @@

   findReference: ->
     return unless @manager.client
-    dir = @manager.server.rootPath
-    return unless dir
     editor = atom.workspace.getActiveTextEditor()
     return unless editor
     cursor = editor.getLastCursor()


Added go-plus

Added go-plus

--- .atom/packages.cson
+++ .atom/packages.cson
@@ -20,6 +20,7 @@
   "git-log"
   "git-time-machine"
   "glowing-cursor"
+  "go-plus"
   "highlight-selected"
   "hyperclick"
   "js-hyperclick"


:art: Remove uncessary fat arrow

:art: Remove uncessary fat arrow

--- lib/color-buffer.coffee
+++ lib/color-buffer.coffee
@@ -45,7 +45,7 @@
       @emitter.emit 'did-update-color-markers', {created: [], destroyed: []}

     # Needed to clean the serialized markers from previous versions
-    @editor.findMarkers(type: 'pigments-variable').forEach (m) => m.destroy()
+    @editor.findMarkers(type: 'pigments-variable').forEach (m) -> m.destroy()

     if colorMarkers?
       @restoreMarkersState(colorMarkers)


fix parser

fix parser

--- lib/linter-js-standard.coffee
+++ lib/linter-js-standard.coffee
@@ -74,9 +74,13 @@
     if config.honorStyleSettings
       # If linter is standard and parser is present
       # else if linter is semiStandard and parser is present
-      if @linterName == 'js-standard' and styleSettings.standard.parser
+      if @linterName == 'js-standard' and
+      styleSettings.standard and
+      styleSettings.standard.parser
         @cmd.push '--parser', styleSettings.standard.parser
-      else if @linterName == 'js-standard' and styleSettings.semistandard.parser
+      else if @linterName == 'js-standard' and
+      styleSettings.semistandard and
+      styleSettings.semistandard.parser
         @cmd.push '--parser', styleSettings.semistandard.parser

     # Check if executablePath is defined,


Remove redundant null guard

Remove redundant null guard

--- lib/load-paths-handler.coffee
+++ lib/load-paths-handler.coffee
@@ -80,7 +80,7 @@
       if err
         done(false)
       else
-        done(realPath?.search(@rootPath) is 0)
+        done(realPath.search(@rootPath) is 0)

 module.exports = (rootPaths, followSymlinks, ignoreVcsIgnores, ignores=[]) ->
   ignoredNames = []


Switch to using native version of `size`

Change-type: patch

--- lib/models/application.coffee
+++ lib/models/application.coffee
@@ -18,7 +18,6 @@
 Promise = require('bluebird')
 once = require('lodash/once')
 isEmpty = require('lodash/isEmpty')
-size = require('lodash/size')
 errors = require('balena-errors')

 {
@@ -254,7 +253,7 @@
 					if isEmpty(applications)
 						throw new errors.BalenaApplicationNotFound(nameOrSlugOrId)

-					if size(applications) > 1
+					if applications.length > 1
 						throw new errors.BalenaAmbiguousApplication(nameOrSlugOrId)
 				.get(0)
 		.tap(normalizeApplication)
@@ -347,7 +346,7 @@
 		.tap (applications) ->
 			if isEmpty(applications)
 				throw new errors.BalenaApplicationNotFound("#{owner}/#{appName}")
-			if size(applications) > 1
+			if applications.length > 1
 				throw new errors.BalenaAmbiguousApplication("#{owner}/#{appName}")
 		.get(0)
 		.tap(normalizeApplication)


fix hideHighlightOnSelectedWord sometimes not work

--- lib/highlighted-area-view.coffee
+++ lib/highlighted-area-view.coffee
@@ -74,7 +74,7 @@
       'highlight-selected.hideHighlightOnSelectedWord')
     outcome = false
     for selection in selections
-      selectionRange = selection.getScreenRange()
+      selectionRange = selection.getBufferRange()
       outcome = (range.start.column is selectionRange.start.column) and
                 (range.start.row is selectionRange.start.row) and
                 (range.end.column is selectionRange.end.column) and


:bomb: Remove input-history feature

:bomb: Remove input-history feature

--- lib/input-history-manager.coffee
+++ lib/input-history-manager.coffee
@@ -1,36 +0,0 @@
-_ = require 'underscore-plus'
-{getValidIndexForList} = require './utils'
-
-class Entry
-  constructor: (@text, @useRegex) ->
-
-  # use to eliminate duplicate entries by _.uniq()
-  toString: ->
-    @text + ':' + String(@useRegex)
-
-module.exports =
-new class InputHistoryManager
-  maxSize: 100
-  constructor: ->
-    @index = -1
-    @entries = []
-
-  get: (direction) ->
-    delta = switch direction
-      when 'previous' then +1
-      when 'next' then -1
-    @index = getValidIndexForList(@entries, @index + delta)
-    @entries[@index]
-
-  save: (text, useRegex) ->
-    return unless text
-    entry = new Entry(text, useRegex)
-    @entries.unshift(entry)
-    @entries = _.uniq(@entries, (entry) -> entry.toString())
-    @entries.splice(@maxSize) if @entries.length > @maxSize
-
-  reset: ->
-    @index = -1
-
-  destroy: ->
-    @entries = null


Using edn_reader to parse ns declarations

--- lib/editor-utils.coffee
+++ lib/editor-utils.coffee
@@ -1,4 +1,5 @@
 {CompositeDisposable, Range, Point} = require 'atom'
+edn_reader = require './proto_repl/proto_repl/edn_reader.js'

 module.exports = EditorUtils =

@@ -10,11 +11,12 @@
   # Finds a Clojure Namespace declaration in the editor and returns the name
   # of the namespace.
   findNsDeclaration: (editor)->
-    nsName = null
-    editor.scan /\(ns ([^\s\)]+)/, ({match, stop})->
-      nsName = match[1]
-      stop()
-    nsName
+    for range in @getTopLevelRanges(editor)
+      txt = editor.getTextInBufferRange(range)
+      try
+        return edn_reader.parse(txt)[1] if txt.match(/^\(ns /)
+      catch e
+        return null

   # Returns true if the position in the text editor is in a Markdown file in a
   # code block that contains Clojure.


Better autocomplete prefix provider!

Better autocomplete prefix provider!

--- lib/slime-autocomplete.coffee
+++ lib/slime-autocomplete.coffee
@@ -1,4 +1,5 @@
 # Implements Slime autocompletion!
+utils = require './utils'

 module.exports =
   selector: '.source.lisp-repl, .source.lisp'
@@ -12,13 +13,25 @@
   excludeLowerPriority: true

   # Required: Return a promise, an array of suggestions, or null.
-  getSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) ->
+  getSuggestions: ({editor, bufferPosition}) ->
+    prefix = @getPrefix(editor, bufferPosition)
+    console.log prefix
     if @swank?.connected and !@disabled and prefix != ""
       return @swank.autocomplete(prefix, @repl.pkg).then (acs) =>
-        return (text:ac for ac in acs)
+        return ({text: ac, replacementPrefix: prefix} for ac in acs)
     else
       return new Promise (resolve) ->
         resolve([])
+
+
+  # A better prefix for Lisp
+  getPrefix: (editor, bufferPosition) ->
+    # Get the text for the line up to the triggered buffer position
+    line = editor.getTextInRange([[bufferPosition.row, @repl.prompt.length], bufferPosition])
+    # Match the regex to the line, and return the match
+    matches = line.match(utils.lispWordRegex)
+    console.log matches
+    return matches?[matches?.length - 1] or ''

   # (optional): called _after_ the suggestion `replacementPrefix` is replaced
   # by the suggestion `text` in the buffer


tasks: clicking a checkbox inside a task description should make that task the currently selected one.

--- salvus/page/tasks.coffee
+++ salvus/page/tasks.coffee
@@ -505,6 +505,7 @@
                         set   : {desc  : task.desc, last_edited : new Date() - 0}
                         where : {task_id : task.task_id}
                     @set_dirty()
+                    @set_current_task(task)
                 @display_desc(task)
                 return false


:memo: Add a note on linter-views::scope

--- lib/linter-views.coffee
+++ lib/linter-views.coffee
@@ -20,7 +20,7 @@
     @panelWorkspace = atom.workspace.addBottomPanel item: @panel, visible: false

     # Set default tab to File
-    @scope = 'file'
+    @scope = 'file' # the value of @scope is changed from views/bottom-tab-{file, project}
     @bottomTabFile.active = true

     # Bubble


:gift: experimental inserted text register.

--- lib/mode-manager.coffee
+++ lib/mode-manager.coffee
@@ -96,8 +96,7 @@
         changes = getChangesSinceCheckpoint(@editor, checkpoint)
         text = @editor.getTextInBufferRange(getNewTextRangeFromChanges(changes) ? [])
         item.confirmChanges(text)
-
-      # @vimState.register.set('.', {text})
+        @vimState.register.set('.', {text})

       replaceModeDeactivator?.dispose()
       replaceModeDeactivator = null


Fixes enable/disable setting.

Fixes enable/disable setting.

--- lib/multi-wrap-guide-view.coffee
+++ lib/multi-wrap-guide-view.coffee
@@ -61,6 +61,10 @@
       scope: @editor.getRootScopeDescriptor(),
       updateGuidesCallback
     )
+    subscriptions.add atom.config.onDidChange(
+      'multi-wrap-guide.enabled',
+      updateGuidesCallback
+    )
     subscriptions

   getDefaultColumns: (scopeName) ->
@@ -71,7 +75,10 @@
     return if customColumns.length > 0 then customColumns else @getDefaultColumns(scopeName)

   isEnabled: ->
-    atom.config.get('wrap-guide.enabled', scope: @editor.getRootScopeDescriptor()) ? true
+    wrap_enabled = atom.config.get('wrap-guide.enabled', scope: @editor.getRootScopeDescriptor())
+    if wrap_enabled? and not wrap_enabled
+      return false
+    return atom.config.get('multi-wrap-guide.enabled')

   createElement: (type, classes...) ->
     element = $(document.createElement(type))
@@ -148,9 +155,10 @@
         scopeSelector: ".#{scope}"

   updateGuides: =>
+    @empty()
+    return unless @isEnabled()
     columns = @getColumns(@editor.getPath(), @editor.getGrammar().scopeName)
-    if columns.length > 0 and @isEnabled()
-      @empty()
+    if columns.length > 0
       for column in columns
         columnWidth = @editorElement.getDefaultCharacterWidth() * column
         guide = @createElement 'div', 'multi-wrap-guide'


unessential code changes in the plumbs.send

--- library/nucleus/plumbs.coffee
+++ library/nucleus/plumbs.coffee
@@ -130,17 +130,23 @@
 # based on the content that is being sent away. Use this method
 # rather than writing and ending the request in a direct way.
 # Is implemented using the external `response-send` library.
-module.exports.send = (kernel) ->
-    (request, response, next) ->
-        ack = "could not attach a response sender"
-        noLibrary = "could not load sender library"
-        terrible = "no valid response object found"
-        assert _.isFunction(send?.json), noLibrary
-        assert _.isObject(response or 0), terrible
-        assert response.send = send, ack.toString()
-        assert response.json = send.json spaces: 4
-        response.req = request unless response.req
-        return next() unless request.headersSent
+module.exports.send = (kernel) -> (request, response) ->
+    ack = "could not attach response sender method"
+    noLibrary = "could not load the sender library"
+    terrible = "got no valid response object found"
+    noHeaders = "unable to locate request headers"
+    assert _.isObject(kernel), "no kernel supplied"
+    assert _.isFunction(try (send.json)), noLibrary
+    assert _.isObject(response or null), terrible
+    assert _.isObject(request?.headers), noHeaders
+    assert (try response.send = send), ack.toString()
+    assert response.json = try (send.json spaces: 4)
+    try response.req = request unless response.req
+    assert unix = moment().unix().toString().bold
+    message = "Running sending middleware at U=%s"
+    logger.debug message.toString(), unix.toString()
+    assert _.isFunction next = _.last arguments
+    return next() unless request.headersSent

 # This middleware is a wrapper around the `toobusy` module providing
 # the functinality that helps to prevent the server shutting down due


unessential code changes in the plumbs.threshold

--- library/nucleus/plumbs.coffee
+++ library/nucleus/plumbs.coffee
@@ -80,6 +80,32 @@
     logger.info useRedis.toString().blue if redis
     return connect.session shallow or new Object

+# This middleware is a wrapper around the `toobusy` module providing
+# the functinality that helps to prevent the server shutting down due
+# to the excessive load. This is done via monitoring of the event loop
+# polling and rating the loop lag time. If it's too big, the request
+# will not be processed, but simply dropped. This is a config wrapper.
+module.exports.threshold = (kernel) ->
+    wrongReason = "no threshold reason supplied"
+    wrongLagTime = "no valid lag time specified"
+    noHeaders = "unable to locate request headers"
+    options = nconf.get("threshold") or Object()
+    assert _.isNumber(options?.lag), wrongLagTime
+    assert _.isString(options?.reason), wrongReason
+    assert _.isObject(kernel), "no kernel supplied"
+    (busy = require "toobusy").maxLag try options.lag
+    message = "Setting threshold maximum lag to %s ms"
+    logger.info message.red, options.lag.toString().bold
+    return (request, response) -> # middleware itself
+        assert unix = moment().unix().toString().bold
+        assert _.isObject(request?.headers), noHeaders
+        message = "Running threshold middleware at U=%s"
+        logger.debug message.toString(), unix.toString()
+        assert _.isFunction next = _.last arguments
+        return next undefined unless busy() is yes
+        response.writeHead 503, options.reason
+        return response.end options.reason
+
 # This middleware uses an external library to parse the incoming
 # user agent identification string into a platform description
 # object. If the user agent string is absent from the requesting
@@ -148,25 +174,6 @@
     assert _.isFunction next = _.last arguments
     return next() unless request.headersSent

-# This middleware is a wrapper around the `toobusy` module providing
-# the functinality that helps to prevent the server shutting down due
-# to the excessive load. This is done via monitoring of the event loop
-# polling and rating the loop lag time. If it's too big, the request
-# will not be processed, but simply dropped. This is a config wrapper.
-module.exports.threshold = (kernel) ->
-    wrongReason = "no threshold reason supplied"
-    wrongLagTime = "no valid lag time specified"
-    options = nconf.get("threshold") or Object()
-    assert _.isNumber(options?.lag), wrongLagTime
-    assert _.isString(options?.reason), wrongReason
-    (busy = require "toobusy").maxLag options.lag
-    message = "Setting threshold maximum lag to %s ms"
-    logger.info message.magenta, "#{options.lag}".bold
-    return (request, response, next, additional) ->
-        return next undefined unless busy() is yes
-        response.writeHead 503, options.reason
-        return response.end options.reason
-
 # This plumbing add an `accepts` method onto the HTTP resonse object
 # which check if the request/response pair has an HTTP accept header
 # set to any of the values supplied when invoking this method. It is


when 500 from API, always render error view, but only show error data in development

--- server/router.coffee
+++ server/router.coffee
@@ -44,10 +44,14 @@
   if err.statusCode && err.statusCode is 401
     res.redirect('/login')
   else
-    if (env.name == 'development')
-      throw err
-    else
-      res.render('error_view', app: req.rendrApp, req: req)
+    data =
+      app: req.rendrApp
+      req: req
+    if env.name == 'development'
+      data.locals =
+        message: err.message
+        stack: err.stack
+    res.render('error_view', data)

 # define routes
 exports.routes = () ->


fix pool with idle to 0

fix pool with idle to 0

--- src/AdapterPool.coffee
+++ src/AdapterPool.coffee
@@ -37,6 +37,7 @@
     info: 'debug'
     verbose: 'trace'

+clientId = 0
 class SemaphorePool extends semLib.Semaphore
     constructor: (options = {})->
         @_factory = {}
@@ -59,63 +60,81 @@
     getName: ->
         @_factory.name
     acquire: (callback, opts = {})->
-        @semTake
+        self = @
+        self.semTake
             priority: opts.priority
             num: 1
             timeOut: opts.timeOut
             onTimeOut: opts.onTimeOut
-            onTake: =>
-                if @_avalaible.length is 0
-                    @_factory.create (err, client)=>
+            onTake: ->
+                logger.debug "[#{self._factory.name}] [#{self.id}] acquire", self._avalaible.length
+                if self._avalaible.length is 0
+                    self._factory.create (err, client)->
                         return callback err if err
-                        @_created.push client
-                        @_removeIdle @_created.length - 1, client
+                        self._created.push client
+                        self._removeIdle client
                         callback null, client
                         return
                     return
-                client = @_avalaible.shift()
-                @_removeIdle @_created.indexOf(client), client
+                client = self._avalaible.shift()
+                self._removeIdle client
                 callback null, client
                 return
     release: (client)->
         index = @_created.indexOf client
         if ~index
+            logger.debug "[#{this._factory.name}] [#{this.id}] release '#{client.id}'. Avalaible #{this._avalaible.length}"
             @_avalaible.push client
-            @_idle index, client
+            @_idle client
             return @semGive()
         return false
-    _idle: (index, client)->
+    _idle: (client)->
+        self = @
         if @_factory.idle > 0
-            return if @_factory.min is @_created.length
-            @_timers[index] = setTimeout =>
-                return if @_factory.min is @_created.length
-                @destroy client
-                return
-            , @_factory.idle
-        return
-    _removeIdle: (index, client)->
-        clearTimeout @_timers[index]
-        return
-    destroy: (client)->
+            self._removeIdle client
+            self._timers[client.id] = setTimeout ->
+                self.destroy client
+                return
+            , self._factory.idle
+        return
+    _removeIdle: (client)->
+        clearTimeout @_timers[client.id]
+        delete @_timers[client.id]
+        return
+    destroy: (client, force)->
         index = @_created.indexOf client
+
+        # ensure minimum
         if ~index
-            @_created.splice index, 1
-            @_factory.destroy client
-            @_ensureMinimum()
-            return true
+            if force or @_factory.min < @_created.length
+                logger.debug "[#{this._factory.name}] [#{this.id}] destroying '#{client.id}'. #{this._avalaible.length}/#{this._created.length}"
+                @_created.splice index, 1
+
+                # remove it from available since it will be destroyed
+                index = @_avalaible.indexOf client
+                @_avalaible.splice index, 1 if ~index
+
+                @_factory.destroy client
+                logger.debug "[#{this._factory.name}] [#{this.id}] destroyed '#{client.id}'. #{this._avalaible.length}/#{this._created.length}"
+
+                @_ensureMinimum() if force
+                return true
+            else
+                @_idle client
         return false
     _superDestroy: (safe, _onDestroy)->
         SemaphorePool.__super__.destroy.call @, safe, _onDestroy

     destroyAll: (safe, _onDestroy)->
+        self = @
         if safe isnt false
-            @_superDestroy true, =>
-                @_onDestroy()
+            self._superDestroy true, ->
+                self._onDestroy()
                 _onDestroy() if 'function' is typeof _onDestroy
                 return
         else
-            @_superDestroy false, =>
-                @_onDestroy()
+            self._superDestroy false, ->
+                self._onDestroy()
                 _onDestroy() if 'function' is typeof _onDestroy
                 return
         return
@@ -130,11 +149,13 @@
         @_avalaible.splice 0, @_avalaible.length
         return
     _ensureMinimum: ->
-        if @_factory.min > @_created.length
-            @acquire (err, client)=>
-                return @emit 'error', err if err
-                @release client
-                @_ensureMinimum()
+        self = @
+        if self._factory.min > self._created.length
+            logger.debug "[#{self._factory.name}] [#{self.id}] _ensureMinimum. #{self._created.length}/#{self._factory.min}"
+            self.acquire (err, client)->
+                return self.emit 'error', err if err
+                self.release client
+                self._ensureMinimum()
                 return
         return

@@ -223,23 +244,26 @@

         @adapter = internal.getAdapter @options

+        self = @
         super
-            name: @options.name
-
-            create: (callback)=>
-                logger.debug "#{@_factory.name} create", @id
-                @adapter.createConnection @options, (err, client)=>
+            name: self.options.name
+
+            create: (callback)->
+                logger.debug "[#{self._factory.name}] [#{self.id}] create"
+                self.adapter.createConnection self.options, (err, client)->
                     return callback(err, null) if err
-
-                    client.on 'error', (err)=>
-                        # @destroy client
-                        @emit 'error', err
+                    client.id = ++clientId
+
+                    client.on 'error', (err)->
+                        # TODO: write why destruction should not be done on error
+                        # self.destroy client
+                        self.emit 'error', err
                         return

-                    # Remove connection from pool on disconnect
-                    client.on 'end', (err)=>
+                    # Remove connection from pool on disconnect because it is no more usable
+                    client.on 'end', (err)->
                         return if client._destroying
-                        @destroy client
+                        self.destroy client, true
                         return

                     callback null, client
@@ -247,18 +271,18 @@
                     return
                 return

-            destroy: (client)=>
+            destroy: (client)->
                 return if client._destroying
-                logger.debug "#{@_factory.name} destroy", @id
+                logger.debug "[#{self._factory.name}] [#{self.id}] destroy"
                 client._destroying = true
                 client.end()
                 return

-            max: @options.maxConnection
-            min: @options.minConnection
-            idle: @options.idleTimeout * 1000
-
-        @check(next) if typeof next is 'function'
+            max: self.options.maxConnection
+            min: self.options.minConnection
+            idle: self.options.idleTimeout * 1000
+
+        self.check(next) if typeof next is 'function'
         return

     check: (next)->


git-svn-id: http://127.0.0.1:20080/svn/buma/branches/tutorial/library/sm-store@469 b2333513-9f29-43eb-bce0-aa7d4792c706

--- src/AdapterPool.coffee
+++ src/AdapterPool.coffee
@@ -226,7 +226,7 @@
             name: @options.name

             create: (callback)=>
-                logger.info "#{@_factory.name} create", @id
+                logger.debug "#{@_factory.name} create", @id
                 @adapter.createConnection @options, (err, client)=>
                     return callback(err, null) if err

@@ -248,7 +248,7 @@

             destroy: (client)=>
                 return if client._destroying
-                logger.info "#{@_factory.name} destroy", @id
+                logger.debug "#{@_factory.name} destroy", @id
                 client._destroying = true
                 client.end()
                 return


JDomain: {activate,deactivate} Domain instance method added.

--- workers/social/lib/social/models/domain.coffee
+++ workers/social/lib/social/models/domain.coffee
@@ -53,6 +53,10 @@
           (signature String, Function)
         unbindMachine :
           (signature String, Function)
+        activateDomain:
+          (signature Function)
+        deactivateDomain:
+          (signature Function)
         remove        :
           (signature Function)

@@ -273,6 +277,37 @@
         if err? then console.error err  unless err.code is 11000

+  updateState: (state, callback)->
+
+    if state
+
+      domain = @proposedDomain
+
+      checkExistence domain, (err)=>
+        return callback err  if err
+        @update $set: { domain }, callback
+
+    else
+
+      @update $unset: domain: 1, callback
+
+
+  activateDomain: permit
+    advanced    : [
+      { permission: "edit own domains", validateWith: Validators.own }
+    ]
+    success: (client, callback)->
+      @updateState yes, callback
+
+
+  deactivateDomain: permit
+    advanced    : [
+      { permission: "edit own domains", validateWith: Validators.own }
+    ]
+    success: (client, callback)->
+      @updateState no, callback
+
+
   bindMachine: (target, callback)->
     @update $addToSet: machines: target, callback


cleaner syntax in 'syncer'

cleaner syntax in 'syncer'

--- shared/syncer.coffee
+++ shared/syncer.coffee
@@ -1,5 +1,6 @@
 qs = require('qs') if global.isServer
 modelUtils = null
+server = null

 methodMap =
   'create': 'POST'
@@ -33,8 +34,7 @@
   else
     _.extend req.query, data

-  if !server?
-    server = require('../server/server') if global.isServer
+  server ?= require('../server/server')

   server.dataAdapter.makeRequest req, (err, response, body) ->
     if err


Room toString and getArea now handle orphaned rooms

--- plugins/coffeekeep.model/room.coffee
+++ plugins/coffeekeep.model/room.coffee
@@ -15,10 +15,10 @@
         @items = new ItemCollection @

     getArea: ->
-        @collection.parent
+        @collection?.parent

     getLocationId: ->
-        "#{@getArea().id}##{@id}"
+        "#{@getArea()?.id}##{@id}"

     toString: ->
         "[Room #{@getLocationId()}]"
@@ -40,4 +40,4 @@
     urlPart: 'rooms'

 exports.Room = Room
-exports.RoomCollection = RoomCollection+exports.RoomCollection = RoomCollection


Tell Aloha where to get ghbook plugins.

Tell Aloha where to get ghbook plugins.

--- scripts/configs/aloha.coffee
+++ scripts/configs/aloha.coffee
@@ -36,6 +36,9 @@
       map:
         '*':
           'semanticblock/semanticblock-plugin': 'semanticblock/lib/semanticblock-plugin'
+
+    bundles:
+      ghbook: '../../../../scripts/aloha'

     plugins:
       # All the plugins we use in Aloha


Keep images below Slack's 2MB limit

Keep images below Slack's 2MB limit

--- scripts/google-images.coffee
+++ scripts/google-images.coffee
@@ -30,7 +30,7 @@
 imageMe = (msg, query, animated, faces, cb) ->
   cb = animated if typeof animated == 'function'
   cb = faces if typeof faces == 'function'
-  q = v: '1.0', rsz: '8', q: query, safe: 'active'
+  q = v: '1.0', rsz: '8', q: query, safe: 'active', imgsz: 'medium'
   q.imgtype = 'animated' if typeof animated is 'boolean' and animated is true
   q.imgtype = 'face' if typeof faces is 'boolean' and faces is true
   msg.http('http://ajax.googleapis.com/ajax/services/search/images')


greatly simplified logging mechanics

greatly simplified logging mechanics

--- platform.coffee
+++ platform.coffee
@@ -47,6 +47,32 @@
 	else
 		require path.resolve(wd, what)

+decolorize_stream = ->
+	through (d) ->
+		if typeof d is 'string'
+			try
+				d = JSON.parse d
+			catch e
+				undefined
+
+		if Array.isArray(d)
+			out = d.map((x) -> x.toString().stripColors).toString()
+		else if typeof d is 'object'
+			out = {}
+			for own k, v of d
+				out[k] = v.toString().stripColors
+
+			out = JSON.stringify out
+		else
+			out = d
+
+		out = out.toString().stripColors
+
+		unless out.lastIndexOf('\n') is out.length - 1
+			out += '\n'
+
+		@queue out
+
 class Platform
 	platform = undefined
 	ctx = {}
@@ -116,43 +142,18 @@

 				@locals.pretty = not cfg.compile?.minify

+				@use express.logger()
+
+				if @get('env') is 'production'
+					winston.remove winston.transports.Console
+
+					grey_stdout = decolorize_stream()
+					grey_stdout.pipe process.stdout
+					winston.add winston.transports.File, {stream: grey_stdout}
+
 				@use express.compress() if cfg.server.compress
 				@use express.favicon() unless cfg.app.favicon?
 				@use express.favicon(path.join(wd, cfg.app.favicon)) if cfg.app.favicon?
-
-				winston.add winston.transports.File,
-					stream: (through (d) -> @queue(d.toString().stripColors)).pipe(fs.createWriteStream(path.join(ctx.logs, 'app.log')))
-
-				switch @get 'env'
-					when 'development'
-						winston.info 'env = development'
-
-						greystream = through (d) ->
-							d = d.toString().stripColors
-							d = d.split ' '
-
-							d[0] = d[0].green
-							d[1] = d[1].cyan
-
-							d[2] = d[2].white.bold.redBG unless 200 <= parseInt(d[2]) < 300
-
-							d = d.join ' '
-
-							@queue d
-
-						greystream.pipe process.stdout
-
-						@use express.errorHandler()
-						@use express.logger
-							format: 'tiny'
-							stream: greystream
-
-					when 'production'
-						winston.remove winston.transports.Console
-
-				@use express.logger
-					format: 'default'
-					stream: (through (d) -> @queue(d.toString().stripColors)).pipe(fs.createWriteStream(path.join(ctx.logs, 'express.log')))

 				compiled_models = compiled_routes = undefined
 				models_ok = mws_ok = routes_ok = false


Unnecessary.

Unnecessary.

--- tests.coffee
+++ tests.coffee
@@ -20,7 +20,7 @@
         @added 'testCollection', id, fields
       changed: (id, fields) =>
         @changed 'testCollection', id, fields
-      removed: (id, fields) =>
+      removed: (id) =>
         @removed 'testCollection', id

     @onStop =>


Better tests

Better tests

--- tests.coffee
+++ tests.coffee
@@ -2392,7 +2392,7 @@
     type: 'UNIQUENESS'
     property_keys: [ 'uuid' ]

-  node = db.nodes({uuid: "#{Math.floor(Math.random()*(999999999-1+1)+1)}"}).labels.set('Special')
+  node = db.nodes({uuid: "#{Math.floor(Math.random()*999999998)}"}).labels.set('Special')
   test.equal db.constraint.create('Special', ['uuid']), res
   test.equal db.constraint.get(), [res]
   test.equal db.constraint.get('Special'), [res]
@@ -2414,7 +2414,7 @@
     label: 'Special'
     property_keys: [ 'uuid' ]

-  node = db.nodes({uuid: "#{Math.floor(Math.random()*(999999999-1+1)+1)}"}).labels.set('Special')
+  node = db.nodes({uuid: "#{Math.floor(Math.random()*999999998)}"}).labels.set('Special')
   test.equal _.pick((db.index.create('Special', ['uuid']) || {}), 'label', 'property_keys'), res
   test.equal [_.pick((db.index.get('Special')[0] || {}), 'label', 'property_keys')], [res]
   test.equal db.index.drop('Special', 'uuid'), []
@@ -2429,7 +2429,7 @@
 ###
 Tinytest.add 'Neo4jNode - n.index - create() / get() / drop() (you will see `UnknownFailure` error in console)', (test) ->

-  node = db.nodes({uuid: "#{Math.floor(Math.random()*(999999999-1+1)+1)}"}).labels.set('Special')
+  node = db.nodes({uuid: "#{Math.floor(Math.random()*999999998)}"}).labels.set('Special')
   index = node.index.create('Special', 'uuid')
   test.isTrue _.isObject index
   uri = index.indexed
@@ -2450,7 +2450,7 @@

   n1 = db.nodes()
   n2 = db.nodes()
-  r = n1.to n2, 'Special', {uuid: "#{Math.floor(Math.random()*(999999999-1+1)+1)}"}
+  r = n1.to n2, 'Special', {uuid: "#{Math.floor(Math.random()*999999998)}"}
   index = r.index.create('Special', 'uuid')
   test.isTrue _.isObject index
   uri = index.indexed


Fix models (kinda)

Fix models (kinda)

--- src/coffee/server/models.coffee
+++ src/coffee/server/models.coffee
@@ -54,27 +54,15 @@

 # callback (err, doc) -> ...
 School.get = (school_id, callback) ->
-  db.collection('schools').findOne school_id, (err, doc) ->
-    async.map doc.plans, (plan_id, callbackOnComplete) ->
-      db.collection('plans').findOne(plan_id, callbackOnComplete)
-    ,(err, plans) ->
-      doc.plans = plans
-      callback err, doc
-
-
-###
   db.collection('schools').findById(school_id, (err, doc)->
     async.map(doc.plans, (plan_id, cb2) ->
-        db.collection('plans').findById(plan_id, (err, transformed)->
-          cb2 null, transformed
-        )
+        db.collection('plans').findById(plan_id, cb2)
     ,(err, array_of_plans)->
       console.log array_of_plans
       doc.plans = array_of_plans
       callback(null, doc)
     )
   )
-###
 # callback : (plan_id) -> ...
 School.addPlan = (school_id, plan, files, callback) ->
   createDocWithParent 'plan', school_id, plan, 'plans', 'schools', callback


clarification example about why we can't let lists to run by themselves without using "eval"

--- tests.coffee
+++ tests.coffee
@@ -198,6 +198,24 @@
   # ---------------------------------------------------------------------------
   "console print '(1+1)eval"
   "2"
+
+  # ---------------------------------------------------------------------------
+  # here is why we need eval to run a list,
+  # and we can't just drop the list without eval
+  # and hope that the "empty message" causes
+  # it to run: because we want these two
+  # statements to work the same.
+  # Another way of saying it is that if a list
+  # could run on its own without eval,
+  # any *quoted* list would run on its own too!
+  # *even if it's quoted*!
+  """
+  codeToBeRun = '(console print 1+2)
+  console print codeToBeRun
+  console print " - "
+  console print '(console print 1+2)
+  """
+  "( Console print 1 + 2 ) - ( Console print 1 + 2 )"

   # ---------------------------------------------------------------------------
   "'a←5;'b←'a;console print b;console print a"


step 1

step 1

--- tree2.coffee
+++ tree2.coffee
@@ -188,6 +188,7 @@
     renderShape(ctx, @shape, .75)
     # fix line width
     ctx.scale(1/@bbox.len.x, 1/@bbox.len.y)
+    ctx.fill()
     ctx.stroke()
     ctx.restore()

@@ -247,6 +248,7 @@
     for n in @contains
       n.forAll(fn)
     fn(this)
+    @update()

   tryAll: (fn) ->
     for n in @contains
@@ -259,9 +261,6 @@
   move: (newx, newy) ->
     super(newx, newy)
     @update()
-
-  clickend: (pos, t) ->
-    console.log("SF clicked")

 collection_dropdown_menu = [
   {
@@ -307,17 +306,6 @@

 shannon_fano_dropdown_menu = [
   {
-    name: 'sort by weight',
-    action: (c, t) ->
-      console.log('sort by weight')
-      c.sortNodes( ((n1, n2) -> n2.value - n1.value), t)
-  },
-  {
-    name: 'sort alpha',
-    action: (c, t) ->
-      console.log('sort alpha')
-  },
-  {
     name: 'automatic Shannon-Fano step',
     action: (c, t) ->
       console.log('automatic Shannon-Fano step')
@@ -326,7 +314,7 @@
     name: 'finish Shannon-Fano',
     action: (c, t) ->
       console.log('finish Shannon-Fano')
-      # TODO: this definitely isn't goingt to work without the construction
+      # TODO: this definitely isn't going to work without the construction
       # finished
       c.reconstruct_as = NodeCollection
   }
@@ -481,7 +469,7 @@
         return this

     if @selected?
-      if t - @selected.t < 200
+      if t - @selected.t < .2
         mdx = pos.x - @selected.mx
         mdy = pos.y - @selected.my
         if mdx*mdx+mdy*mdy < 10*10
@@ -556,16 +544,15 @@
     super shape

   mousedown: (pos, idx, t) ->
-    for n in @nodes
-      if n.isHit(pos)
-        if @merging?
+    if @merging?
+      for n in @nodes
+        if n.isHit(pos)
           if n isnt @merging.node
             @mergeNodes(@merging.node, n)
             @merging = null

             return true

-    if @merging?
       @merging = null

     super pos, idx, t
@@ -576,6 +563,7 @@
     if @merging?
       @merging.x = pos.x
       @merging.y = pos.y
+
     return

   collection_dropdown_menu: huffman_collection_dropdown_menu
@@ -614,15 +602,101 @@
   constructor: (shape) ->
     super shape

-  default_bbox: (min: (x: 100, y: 100), len: (x: 100, y: 100))
+  defaultBBoxAt: (pos) ->
+    min: (x: pos.x - default_node_radius*2, y: pos.y - default_node_radius*2)
+    len: (x: default_node_radius*4, y: default_node_radius*4)

   copyNodesFrom: (nc) ->
-    @nodes = [ new ShannonFanoNode(nc.nodes, @shape, @default_bbox, "") ]
+    @nodes = [
+      new ShannonFanoNode(nc.nodes, @shape, @defaultBBoxAt((x:100,y:100)), "") ]

   render: (ctx, idx, t) ->
+    if @splitting?
+      ctx.save()
+      setStyle(ctx, light_link_style)
+      ctx.beginPath()
+      ctx.moveTo(@splitting.node.x, @splitting.node.y)
+      ctx.lineTo(@splitting.pos0.x, @splitting.pos0.y)
+      ctx.moveTo(@splitting.node.x, @splitting.node.y)
+      ctx.lineTo(@splitting.pos1.x, @splitting.pos1.y)
+
+      ctx.stroke()
+      ctx.restore()
+
     super ctx, idx, t

   collection_dropdown_menu: shannon_fano_dropdown_menu
+
+  mousedown: (pos, idx, t) ->
+    if @splitting?
+      hit_another = false
+      for n in @nodes
+        if n.isHit(pos)
+          hit_another = true
+          break
+
+      if not hit_another
+        @splitNode(@splitting.node, @splitting.pos0, @splitting.pos1)
+        @splitting = null
+        return true
+
+      @splitting = null
+
+    super pos, idx, t
+
+  mousemove: (pos) ->
+    super pos
+
+    if @splitting?
+      @splitting.pos0 = (x: pos.x, y: pos.y)
+      @splitting.pos1 = (x: 2*@splitting.node.x - pos.x, y: pos.y)
+
+    return
+
+  mouseup: (pos, t) ->
+    retval = super(pos,t)
+    for n in @nodes
+      if n.update?
+        n.update()
+
+    return retval
+
+  clickend: (pos, t) ->
+    if @selected.node in @nodes
+      @splitting = node: @selected.node, pos0: pos, pos1: pos
+    else
+      super pos, t
+
+  splitNode: (node, pos0, pos1) ->
+    new0 = new ShannonFanoNode(node.contains, @shape, @defaultBBoxAt(pos0), 0)
+    new1 = new ShannonFanoNode([], @shape, @defaultBBoxAt(pos1), 1)
+
+    newnode = new Inner(node.value, new0, new1, @shape)
+
+    new0.move(pos0.x, pos0.y)
+    newnode.x = node.x
+    newnode.y = node.y
+    @replaceNode(node, newnode)
+
+  replaceNode: (oldnode, newnode) ->
+    # replace in any inner node
+    replaceFn = (node) ->
+      if node.child0 == oldnode
+        node.child0 = newnode
+      if node.child1 == oldnode
+        node.child1 = newnode
+
+    for n in @nodes
+      n.forAll(replaceFn)
+
+    # replace a root
+    @nodes = for n in @nodes
+      if n is oldnode
+        newnode
+      else
+        n
+
+    return

 lerp2d = (t, p0, p1) ->
   if t < 0


Get default Prometheus metrics working

Get default Prometheus metrics working

--- libraries/metrics/metrics.coffee
+++ libraries/metrics/metrics.coffee
@@ -1,5 +1,9 @@
 StatsD = require('lynx')
 statsd = new StatsD(process.env["STATSD_HOST"] or "localhost", 8125, {on_error:->})
+
+prom = require('prom-client')
+Register = require('prom-client').register
+collectDefaultMetrics = prom.collectDefaultMetrics

 name = "unknown"
 hostname = require('os').hostname()
@@ -14,9 +18,16 @@
 module.exports = Metrics =
 	initialize: (_name) ->
 		name = _name
+		collectDefaultMetrics({ timeout: 5000, prefix: name })

 	registerDestructor: (func) ->
 		destructors.push func
+
+	injectMetricsRoute: (app) ->
+		app.get('/metrics', (req, res) ->
+			res.set('Content-Type', Register.contentType)
+			res.end(Register.metrics())
+		)

 	set : (key, value, sampleRate = 1)->
 		statsd.set buildKey(key), value, sampleRate


policies are hooked up with access

policies are hooked up with access

--- library/exposure/policies.coffee
+++ library/exposure/policies.coffee
@@ -35,6 +35,7 @@
 http = require "http"
 util = require "util"

+{Access} = require "./access"
 {Archetype} = require "../nucleus/archetype"
 {Barebones} = require "../membrane/skeleton"

@@ -43,7 +44,7 @@
 # the authenticated (or anonymous) entities. The compound is basically
 # ACL solution that functions on top of (but does not depend on) the
 # authentication facilities crafter and provided within a framework.
-module.exports.Policies = class Policies extends Barebones
+module.exports.Policies = class Policies extends Access

     # This is a marker that indicates to some internal subsystems
     # that this class has to be considered abstract and therefore


Pasan tests en Firefox y Chromium

Pasan tests en Firefox y Chromium

Firefox y Chromium llenan la etiqueta <time> con mierda de CSS, y por
eso los tests no pasaban.

--- spec/javascripts/autosave_form_spec.coffee
+++ spec/javascripts/autosave_form_spec.coffee
@@ -29,8 +29,7 @@
       request.response
         status: 200
         responseText:
-          '
Actualizado' +
-          '<time>1998-07-01</time>
'
+          '
Actualizado
'

     it 'actualiza con la respuesta', ->
       expect($('#actualizado')).toHaveHtml request.responseText


update users store to handle password changes

--- src/node_modules/users-store/users-store.coffee
+++ src/node_modules/users-store/users-store.coffee
@@ -1,11 +1,32 @@
+recase = require('recase').create({})
+debug = require('debug')("users-store")
+
 # ridiculous hack for @ngInject due to code at top from coffee class extension
 null
 ### @ngInject ###
-module.exports = (RestfulStore, UserModel) ->
+module.exports = ($http, RestfulStore, UserModel) ->
   class UsersStore extends RestfulStore
+
+    path: (params = {}, op) ->
+      debug("path", params, op)
+      if op == "changePassword"
+        "#{@baseUrl}/users/#{params.id}/change_password"
+      else
+        super(params, op)
+
+    changePassword: (object) ->
+      path = @path(object, "changePassword")
+      debug("changePassword", path, object)
+      self = @
+      object = {
+        user: recase.snakeCopy(object)
+      }
+      $http.post(path, object).then null, (err) ->
+        debug("changePassword error", err)
+        throw recase.camelCopy(err.data)

   new UsersStore(
     singular: "user"
     plural: "users"
     Model: UserModel
-  )+  )


You can now keep adding new list items

You can now keep adding new list items

-new item is added to dom and a form is added
following it.

--- listrr/static/coffee/list.coffee
+++ listrr/static/coffee/list.coffee
@@ -1,13 +1,23 @@
+initialize_form = (form) ->
+    form_action = form.attr 'action'
+    new_li_title = undefined
+    ajax_callback = (newitem_url) ->
+        data =
+            content: new_li_title
+        li = form.parent()
+        form.replaceWith from_template 'list_item', data
+        data =
+            action: form_action
+        new_elem = from_template 'li_form', data
+        li.after new_elem
+        new_form = $ 'form', new_elem
+        initialize_form new_form
+    submit_callback = (title) ->
+        new_li_title = title
+    init_miniform form, ajax_callback, submit_callback
+
 $ ->
     forms = $ "form.new_item_form"
     forms.each ->
         form = $ @
-        form_action = form.attr 'action'
-        new_li_title = undefined
-        ajax_callback = (newitem_url) ->
-            data =
-                content: new_li_title
-            form.replaceWith from_template 'list_item', data
-        submit_callback = (title) ->
-            new_li_title = title
-        init_miniform form, ajax_callback, submit_callback
+        initialize_form form


Close active tab, not last-right-clicked on tab

--- lib/tab-bar-view.coffee
+++ lib/tab-bar-view.coffee
@@ -18,9 +18,9 @@
       'tabs:keep-preview-tab': => @clearPreviewTabs()

     @subscriptions.add atom.commands.add 'atom-workspace',
-      'tabs:close-tab': => @closeTab()
-      'tabs:close-other-tabs': => @closeOtherTabs()
-      'tabs:close-tabs-to-right': => @closeTabsToRight()
+      'tabs:close-tab': => @closeTab(@getActiveTab())
+      'tabs:close-other-tabs': => @closeOtherTabs(@getActiveTab())
+      'tabs:close-tabs-to-right': => @closeTabsToRight(@getActiveTab())
       'tabs:close-saved-tabs': => @closeSavedTabs()
       'tabs:close-all-tabs': => @closeAllTabs()

@@ -166,12 +166,14 @@
       @element.querySelector('.tab.active')?.classList.remove('active')
       tabView.classList.add('active')

+  getActiveTab: ->
+    @tabForItem(@pane.getActiveItem())
+
   updateActiveTab: ->
     @setActiveTab(@tabForItem(@pane.getActiveItem()))

   closeTab: (tab) ->
     tab ?= @children('.right-clicked')[0]
-    tab ?= @tabForItem(@pane.getActiveItem())
     @pane.destroyItem(tab.item) if tab?

   splitTab: (fn) ->
@@ -182,17 +184,15 @@
   copyItem: (item) ->
     item.copy?() ? atom.deserializers.deserialize(item.serialize())

-  closeOtherTabs: ->
+  closeOtherTabs: (active) ->
     tabs = @getTabs()
-    active = @children('.right-clicked')[0]
-    active ?= @tabForItem(@pane.getActiveItem())
+    active ?= @children('.right-clicked')[0]
     return unless active?
     @closeTab tab for tab in tabs when tab isnt active

-  closeTabsToRight: ->
+  closeTabsToRight: (active) ->
     tabs = @getTabs()
-    active = @children('.right-clicked')[0]
-    active ?= @tabForItem(@pane.getActiveItem())
+    active ?= @children('.right-clicked')[0]
     index = tabs.indexOf(active)
     return if index is -1
     @closeTab tab for tab, i in tabs when i > index


Typecast rank to a number

Typecast rank to a number

--- server/queues/scoring.coffee
+++ server/queues/scoring.coffee
@@ -106,7 +106,7 @@
     matchObject.opponents[sessionID].sessionID = sessionID
     matchObject.opponents[sessionID].userID = session.creator
     matchObject.opponents[sessionID].metrics = {}
-    matchObject.opponents[sessionID].metrics.rank = newScoreObject[sessionID].gameRanking
+    matchObject.opponents[sessionID].metrics.rank = Number(newScoreObject[sessionID].gameRanking)

   log.info "Match object computed, result: #{matchObject}"
   log.info "Writing match object to database..."


re-add the display = 'none' stuff because we don't want the tooltip in the dom initially.

--- lib/util/tooltip.coffee
+++ lib/util/tooltip.coffee
@@ -25,8 +25,11 @@

   hide: ->
     @view.classList.add 'dontshow'
+    setTimeout (=> @view.style.display = 'none'), 100

-  show: -> @view.classList.remove 'dontshow'
+  show: ->
+    @view.classList.remove 'dontshow'
+    setTimeout (=> @view.style.display = 'block'), 100

   destroy: ->
     document.body.removeChild @view
@@ -34,6 +37,7 @@
   tooltipView: (content) ->
     tt = document.createElement 'div'
     tt.classList.add 'ink-tooltip', 'dontshow'
+    tt.style.display = 'none'
     tt.appendChild content
     tt

@@ -43,9 +47,9 @@
       @positionOverlay()
       clearTimeout timer
       if @cond() then @show()
-    @parent.onmouseout = => timer = setTimeout (=> @hide()), 150
+    @parent.onmouseout = => timer = setTimeout (=> @hide()), 100
     @view.onmouseover  = => clearTimeout timer
-    @view.onmouseout   = => timer = setTimeout (=> @hide()), 150
+    @view.onmouseout   = => timer = setTimeout (=> @hide()), 100

   positionOverlay: ->
     bounding = @parent.getBoundingClientRect()


Add autocomplete settings

Add autocomplete settings

--- settings/language-jison.cson
+++ settings/language-jison.cson
@@ -1,3 +1,18 @@
 ".source.jison, .source.jisonlex":
   editor:
     commentStart: "// "
+
+".rule-components.jison":
+  autocomplete:
+    symbols:
+      constant:
+        selector: ".token, .rule-result"
+        typePriority: 2
+      "":
+        selector: ".source"
+        typePriority: 1
+      builtin:
+        suggestions: [
+          "EOF"
+          "error"
+        ]


making extra input for tutor date picker disabled

--- src/components/tutor-input.cjsx
+++ src/components/tutor-input.cjsx
@@ -37,7 +37,7 @@
     classes = ['form-control']
     unless @props.value then classes.push('empty')

-      <input type='text' className={classes.join(' ')} />
+      <input type='text' disabled className={classes.join(' ')} />

{@props.label}

       <DateTimePicker {...@props}/>


Make sure we are always using setTool to switch tools

--- src/core/LiterallyCanvas.coffee
+++ src/core/LiterallyCanvas.coffee
@@ -60,7 +60,7 @@
     @scale = 1.0
     # GUI immediately replaces this value, but it's initialized so you can have
     # something really simple
-    @tool = new @opts.tools[0](this)
+    @setTool(new @opts.tools[0](this))

     @width = opts.imageSize.width or INFINITE
     @height = opts.imageSize.height or INFINITE
@@ -118,7 +118,7 @@
     @trigger('imageSizeChange', {@width, @height})

   setTool: (tool) ->
-    @tool.willBecomeInactive(this)
+    @tool?.willBecomeInactive(this)
     @tool = tool
     @trigger('toolChange', {tool})
     tool.didBecomeActive(this)


開栓情報をリフレッシュする機能実装

開栓情報をリフレッシュする機能実装

--- app/controllers/onTapInfo.coffee
+++ app/controllers/onTapInfo.coffee
@@ -6,6 +6,15 @@

 $.activityIndicator.style = style
 $.RightNavButton.text = String.fromCharCode("0xe10e")
+$.RightNavButton.addEventListener 'click', (e) ->
+  dummyRow = $.UI.create 'TableViewRow',
+    classes:'dummyRow'
+  $.tableview.setData [dummyRow]
+
+  statusesQuery (statuses) ->
+    createOnTapInfo statuses
+
+
 $.tableview.addEventListener 'click', (e) ->
   shopData = e.row.shopData
   shopDataDetailController = Alloy.createController('shopDataDetail')
@@ -49,16 +58,23 @@

   return $.tableview.setData rows

-exports.move = (_tab) ->
+
+statusesQuery = (callback)->
   $.activityIndicator.show()
-
   Cloud.Statuses.query
     page: 1
     per_page: 20
   , (e) ->
     $.activityIndicator.hide()
     if e.success
-      createOnTapInfo e.statuses
-
+      callback e.statuses
+    else
+      alert "開栓情報が取得できませんでした"
+
+exports.move = (_tab) ->
   _tab.open $.onTapWindow
+  statusesQuery (statuses) ->
+    createOnTapInfo statuses
+
+


Ignore stream tag cutscene warning (#350)

--- app/core/application.ozar.coffee
+++ app/core/application.ozar.coffee
@@ -52,6 +52,7 @@
     Router = require('core/Router')
     @isProduction = -> document.location.href.search('https?://localhost') is -1
     Vue.config.devtools = not @isProduction()
+    Vue.config.ignoredElements = ['stream'] # Used for Cloudflare Cutscene Player and would throw Vue warnings

     # propagate changes from global 'me' User to 'me' vuex module
     store = require('core/store')


Fixed the goal manager from polluting the goals in the level with the isPositive property.

--- app/lib/world/GoalManager.coffee
+++ app/lib/world/GoalManager.coffee
@@ -90,6 +90,7 @@
   # IMPLEMENTATION DETAILS

   addGoal: (goal) ->
+    goal = _.cloneDeep(goal)
     goal.id = @nextGoalID++ if not goal.id
     return if @goalStates[goal.id]?
     @goals.push(goal)


add assert import

add assert import

--- snippets/import.cson
+++ snippets/import.cson
@@ -2,3 +2,7 @@
     'go import bolddb':
         'prefix':'g-im-boltdb'
         'body':'${1:import} "github.com/boltdb/bolt"'
+
+    'go import assert':
+        'prefix':'g-im-assert'
+        'body':'${1:import} "github.com/stretchr/testify/assert"'


Removes stray console.log

Removes stray console.log

--- source/module.coffee
+++ source/module.coffee
@@ -116,7 +116,6 @@
   # Invokes the lifecycle action on all required modules, then on itself,
   # calling the instance hooks before, on, and after
   _runLifeCycleAction: (action, func) ->
-    console.log(func.toString())

     @_invokeActionOnRequiredModules action
     this["before#{@_capitalize(action)}"]?()


Set baseDir correctly

Set baseDir correctly

--- spec/Subgraph.coffee
+++ spec/Subgraph.coffee
@@ -209,6 +209,7 @@
     gr.addOutport 'out', 'Split', 'out'
     it 'should emit a ready event after network has been loaded', (done) ->
       @timeout 6000
+      c.baseDir = root
       c.once 'ready', ->
         chai.expect(c.network).not.to.be.null
         chai.expect(c.ready).to.be.true
@@ -264,6 +265,7 @@
     file = "#{urlPrefix}spec/fixtures/subgraph.fbp"
     it 'should emit a ready event after network has been loaded', (done) ->
       @timeout 6000
+      c.baseDir = root
       c.once 'ready', ->
         chai.expect(c.network).not.to.be.null
         chai.expect(c.ready).to.be.true
@@ -277,8 +279,8 @@
       g.send file
       chai.expect(c.ready).to.be.false
     it 'should expose available ports', (done) ->
-      c.baseDir = root
-      @timeout 6000
+      @timeout 6000
+      c.baseDir = root
       c.once 'ready', ->
         chai.expect(c.inPorts.ports).to.have.keys [
           'graph'
@@ -323,6 +325,7 @@
     file = "#{urlPrefix}spec/fixtures/subgraph_legacy.fbp"
     it 'should emit a ready event after network has been loaded', (done) ->
       @timeout 6000
+      c.baseDir = root
       c.once 'ready', ->
         chai.expect(c.network).not.to.be.null
         chai.expect(c.ready).to.be.true


v4 needs to use a subtree

v4 needs to use a subtree

--- spec/Thenable.coffee
+++ spec/Thenable.coffee
@@ -1447,17 +1447,19 @@

         test splitter, [1,2], done

-      it.skip 'v4', (done) ->
+      it 'v4', (done) ->
         splitter = (choice,data) ->
           choice.tree()
           .deliver({})
           .then (choice,data) ->
-            choice.branch "0"
-            .then (choice,data) ->
-              choice.branch "1", (b) ->
-                1
-              choice.branch "2",  (b) ->
-                2
+            choice.branch "0", (b) ->
+              b.tree()
+              .deliver({})
+              .then (choice,data) ->
+                choice.branch "1", (b) ->
+                  1
+                choice.branch "2",  (b) ->
+                  2

         test splitter, [1,2], done


organized spec & added parans tests

organized spec & added parans tests


Fix indentation error

Fix indentation error

--- spec/editor-view-spec.coffee
+++ spec/editor-view-spec.coffee
@@ -432,9 +432,9 @@
           editorView.setFontFamily('sans-serif')

       it "positions the cursor to the clicked row and column", ->
-          {top, left} = editorView.pixelOffsetForScreenPosition([3, 30])
-          editorView.renderedLines.trigger mousedownEvent(pageX: left, pageY: top)
-          expect(editor.getCursorScreenPosition()).toEqual [3, 30]
+        {top, left} = editorView.pixelOffsetForScreenPosition([3, 30])
+        editorView.renderedLines.trigger mousedownEvent(pageX: left, pageY: top)
+        expect(editor.getCursorScreenPosition()).toEqual [3, 30]

     describe "double-click", ->
       it "selects the word under the cursor, and expands the selection wordwise in either direction on a subsequent shift-click", ->


Remove unused require

Remove unused require

--- spec/github-file-spec.coffee
+++ spec/github-file-spec.coffee
@@ -1,5 +1,4 @@
 GitHubFile = require '../lib/github-file'
-Project = require 'project'
 fsUtils = require 'fs-utils'
 path = require 'path'


move multipart-form uploaded data into fixture file

--- spec/http-helper-spec.coffee
+++ spec/http-helper-spec.coffee
@@ -17,38 +17,7 @@

 requestUploadMultipart = vh.buildRequest "localhost:#{testPort}",
   headers: {'Content-Type': "multipart/form-data; boundary=#{mimeBoundary}"}
-  body: [
-    "--#{mimeBoundary}"
-    'Content-Disposition: form-data; name="foo"'
-    ''
-    'forty-two'
-
-    "--#{mimeBoundary}"
-    'Content-Disposition: form-data; name="bar"; filename="my-novel.txt"'
-    'Content-Type: text/plain'
-    ''
-    'Call me Ishmael. Some years ago--never mind how long precisely--'
-    'having little or no money in my purse, and nothing particular to '
-    'interest me on shore, I thought I would sail about a little and '
-    'see the watery part of the world. It is a way I have of driving '
-    'off the spleen and regulating the circulation. Whenever I find '
-    'myself growing grim about the mouth; whenever it is a damp, '
-    'drizzly November in my soul; whenever I find myself involuntarily '
-    'pausing before coffin warehouses, and bringing up the rear of '
-    'every funeral I meet; and especially whenever my hypos get such an '
-    'upper hand of me, that it requires a strong moral principle to '
-    'prevent me from deliberately stepping into the street, and '
-    'methodically knocking people\'s hats off--then, I account it high '
-    'time to get to sea as soon as I can. This is my substitute for '
-    'pistol and ball. With a philosophical flourish Cato throws himself '
-    'upon his sword; I quietly take to the ship. There is nothing '
-    'surprising in this. If they but knew it, almost all men in their '
-    'degree, some time or other, cherish very nearly the same feelings '
-    'towards the ocean with me.'
-
-    "--#{mimeBoundary}--"
-    ''
-  ].join '\r\n'
+  bodyFromFile: "./fixtures/multipart-form-upload"

 vows.describe('Vows HTTP macros')

@@ -67,6 +36,7 @@
       'should have byte count': (res) -> assert.include res.body, '278055'

     'POST /upload': requestUploadMultipart.respondsWith 200
+      'should have antagonist': (res) -> assert.include res.json.antagonist, 'whale'

   .addBatch
     'stop test server':
@@ -94,7 +64,7 @@
           if err?
             puts inspect err
             respond 500, err.toString()
-          else respond 200, JSON.stringify [fields, files]
+          else respond 200, JSON.stringify {fields, files}
     when 'PUT'  then countRequestBodyBytes (err, count) ->
       if err? then respond 500, err.toString()
       else respond 201, "Sweet. You uploaded #{count} bytes"


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/sprite.coffee
+++ src/sprite.coffee
@@ -114,9 +114,7 @@
     proxy = LoaderProxy()

     img.onload = ->
-      tile = Sprite(this)
-
-      spriteCache[url] = Object.extend(proxy, tile)
+      spriteCache[url] = Object.extend(proxy, Sprite(this))

       if loadedCallback
         loadedCallback(proxy)


:art:

:art:

--- spec/specificity-spec.coffee
+++ spec/specificity-spec.coffee
@@ -7,63 +7,64 @@

 {specificity, isValidSelector} = require '../index'

-describe "specificity", ->
-  it "computes the specificity of a selector", ->
-    expect(specificity('#an-id')).toBe 100
-    expect(specificity('#an-id {')).toBe 100
-    expect(specificity('#an-id, #another-id')).toBe 100
+describe "clear-cut", ->
+  describe "specificity(selector)", ->
+    it "computes the specificity of a selector", ->
+      expect(specificity('#an-id')).toBe 100
+      expect(specificity('#an-id {')).toBe 100
+      expect(specificity('#an-id, #another-id')).toBe 100

-    expect(specificity('#an-id:after')).toBe 101
-    expect(specificity('body#an-id')).toBe 101
-    expect(specificity('body #an-id')).toBe 101
+      expect(specificity('#an-id:after')).toBe 101
+      expect(specificity('body#an-id')).toBe 101
+      expect(specificity('body #an-id')).toBe 101

-    expect(specificity('#an-id[foo]')).toBe 110
-    expect(specificity('#an-id[foo=bar]')).toBe 110
-    expect(specificity('#an-id.a-class')).toBe 110
-    expect(specificity('#an-id:not(.a-class)')).toBe 110
-    expect(specificity('#an-id .a-class')).toBe 110
-    expect(specificity('.a-class #an-id')).toBe 110
-    expect(specificity('.a-class#an-id')).toBe 110
+      expect(specificity('#an-id[foo]')).toBe 110
+      expect(specificity('#an-id[foo=bar]')).toBe 110
+      expect(specificity('#an-id.a-class')).toBe 110
+      expect(specificity('#an-id:not(.a-class)')).toBe 110
+      expect(specificity('#an-id .a-class')).toBe 110
+      expect(specificity('.a-class #an-id')).toBe 110
+      expect(specificity('.a-class#an-id')).toBe 110

-    expect(specificity('#an-id #another-id')).toBe 200
-    expect(specificity('#an-id > #another-id')).toBe 200
+      expect(specificity('#an-id #another-id')).toBe 200
+      expect(specificity('#an-id > #another-id')).toBe 200

-    expect(specificity('.a-class')).toBe 10
-    expect(specificity('.a-class {')).toBe 10
-    expect(specificity('.a-class, .b-class')).toBe 10
+      expect(specificity('.a-class')).toBe 10
+      expect(specificity('.a-class {')).toBe 10
+      expect(specificity('.a-class, .b-class')).toBe 10

-    expect(specificity('.a-class:after')).toBe 11
-    expect(specificity('body.a-class')).toBe 11
-    expect(specificity('body .a-class')).toBe 11
+      expect(specificity('.a-class:after')).toBe 11
+      expect(specificity('body.a-class')).toBe 11
+      expect(specificity('body .a-class')).toBe 11

-    expect(specificity('.a-class.b-class')).toBe 20
-    expect(specificity('.a-class:not(.b-class)')).toBe 20
-    expect(specificity('.a-class .b-class')).toBe 20
-    expect(specificity('.a-class > .b-class')).toBe 20
-    expect(specificity('.a-class[foo]')).toBe 20
-    expect(specificity('.a-class[foo=bar]')).toBe 20
+      expect(specificity('.a-class.b-class')).toBe 20
+      expect(specificity('.a-class:not(.b-class)')).toBe 20
+      expect(specificity('.a-class .b-class')).toBe 20
+      expect(specificity('.a-class > .b-class')).toBe 20
+      expect(specificity('.a-class[foo]')).toBe 20
+      expect(specificity('.a-class[foo=bar]')).toBe 20

-    expect(specificity('body')).toBe 1
-    expect(specificity('body {')).toBe 1
+      expect(specificity('body')).toBe 1
+      expect(specificity('body {')).toBe 1

-    expect(specificity('body:after')).toBe 2
-    expect(specificity('body div')).toBe 2
+      expect(specificity('body:after')).toBe 2
+      expect(specificity('body div')).toBe 2

-    expect(specificity('body div > span')).toBe 3
-    expect(specificity('body div > span')).toBe 3
+      expect(specificity('body div > span')).toBe 3
+      expect(specificity('body div > span')).toBe 3

-    expect(specificity('body[foo]')).toBe 11
-    expect(specificity('body[foo=bar]')).toBe 11
-    expect(specificity('body:not(.a-class)')).toBe 11
+      expect(specificity('body[foo]')).toBe 11
+      expect(specificity('body[foo=bar]')).toBe 11
+      expect(specificity('body:not(.a-class)')).toBe 11

-    expect(specificity('#id')).toBeGreaterThan(specificity('body'))
-    expect(specificity('#id')).toBeGreaterThan(specificity('.class'))
-    expect(specificity('.a.b')).toBeGreaterThan(specificity('.a'))
-    expect(specificity('body div')).toBeGreaterThan(specificity('body'))
+      expect(specificity('#id')).toBeGreaterThan(specificity('body'))
+      expect(specificity('#id')).toBeGreaterThan(specificity('.class'))
+      expect(specificity('.a.b')).toBeGreaterThan(specificity('.a'))
+      expect(specificity('body div')).toBeGreaterThan(specificity('body'))

-    expect(specificity('<>')).toBe 1
+      expect(specificity('<>')).toBe 1

-  describe "isValidSelector", ->
+  describe "isValidSelector(selector)", ->
     it "returns true if the selector is valid, false otherwise", ->
       expect(isValidSelector('body')).toBe true
       expect(isValidSelector('body')).toBe true


john is cleaning.

john is cleaning.

--- src/public/javascripts/lib/models/worker.coffee
+++ src/public/javascripts/lib/models/worker.coffee
@@ -234,6 +234,16 @@
       index: @index
       job_id: @job_id

+  clean_reduce: () ->
+    @reduce_code = null
+    @reduce_data = null
+    @job_id = start_reduce_message.job_id
+    @number_of_mappers = start_reduce_message.number_of_mappers
+    @reduce_data = {}
+    @num_done = 0
+    @mapper_done
+    @_status.state = 'IDLE'
+
   hashval: (s) ->
     hash = 0
     chr = null


Figures with one nonsub file get a single message #109

--- client/import.iced
+++ client/import.iced
@@ -296,7 +296,7 @@
               [labels, subbody] = findLabels subbody
               subcaption = /\\caption\s*{((?:[^{}]|{[^{}]*})*)}/.exec(subbody)?[1]
               findIncludeGraphics subbody, subcaption, labels
-          findIncludeGraphics body
+          findIncludeGraphics body, false  ## subcaption = false for root images
           ## Search for correct extensions
           for graphic in graphics
             for extension in ['', '.svg', '.png', '.jpg', '.pdf']
@@ -326,6 +326,9 @@
           for graphic in figure.graphics
             for label in graphic.labels
               figures[label] = figure
+
+  formatLabel = (label) ->
+    "``#{label.replace /^fig:/, ''}''"

   ## Extract sections.
   for filename, content of zip.files
@@ -377,8 +380,8 @@
             labels[ref]
           else if ref of figures
             attach.push figures[ref]
-            #"``#{figures[ref].labels[figures[p1].labels.length-1]}''"
-            "``#{ref}''"
+            #formatLabel figures[ref].labels[figures[p1].labels.length-1]
+            formatLabel ref
           else
             console.warn "Unresolved #{match}"
             match
@@ -389,21 +392,7 @@
         delete revision.creator
         await Meteor.call 'messageImport', group, null, message, [revision], defer error, message._id

-        attach = _.unique attach  ## upload \ref'd figures only once each
-        for figure in attach
-          figure.message =
-            title: "Figure " +
-              ("``#{label}''" for label in figure.labels).join " / "
-            body: figure.caption
-            published: now
-            format: 'latex'
-          frevision = _.clone figure.message
-          figure.message.creator = me
-          figure.message.created = now
-          frevision.updators = [me]
-          frevision.updated = now
-          await Meteor.call 'messageImport', group, message._id, figure.message, [frevision], defer error, figure.message._id
-
+        attach = _.uniq attach  ## upload \ref'd figures only once each
         await
           for figure in attach
             figure.files = []
@@ -423,6 +412,23 @@
                 file.callback = (file2) ->
                   d file2.uniqueIdentifier
               Files.resumable.addFile file
+        for figure in attach
+          figure.message =
+            title: "Figure " +
+              (formatLabel label for label in figure.labels).join " / "
+            body: figure.caption
+            format: 'latex'
+            published: now
+          rootFiles = (file for file in figure.files when file.graphic.subcaption == false)
+          if rootFiles.length == 1
+            figure.message.file = rootFiles[0].file2id
+            figure.files = _.without figures.files, rootFiles[0]
+          frevision = _.clone figure.message
+          figure.message.creator = me
+          figure.message.created = now
+          frevision.updators = [me]
+          frevision.updated = now
+          await Meteor.call 'messageImport', group, message._id, figure.message, [frevision], defer error, figure.message._id
         await
           for figure in attach
             for file in figure.files
@@ -432,7 +438,7 @@
                 published: now
               if file.graphic.labels.length > 0
                 filerev.title = "Figure " +
-                  ("``#{label}''" for label in file.graphic.labels).join " / "
+                  (formatLabel label for label in file.graphic.labels).join " / "
               if file.graphic.subcaption
                 filerev.body = file.graphic.subcaption
               filemsg = _.clone filerev


fix initial map

fix initial map

--- frontend/coffee/directives/bestMap.coffee
+++ frontend/coffee/directives/bestMap.coffee
@@ -96,7 +96,7 @@
 		L.tileLayer 'https://{s}.tiles.mapbox.com/v3/thymajesty.kbp9jbaa/{z}/{x}/{y}.png', configureMap
 		.addTo(map)
 		markers = for i in [0...2]
-					{likes_count: i*100, long_latt: [49, 32], description:'description: Lorem ipsum Sed tempor Ut minim fugiat ea sed quis ad eiusmod sed proident non Duis'}
+					{likes_count: i*100, long_latt: [50.438265372826606, 30.517272949218754], description:'description: Lorem ipsum Sed tempor Ut minim fugiat ea sed quis ad eiusmod sed proident non Duis'}

 		markers.isAddPosible = true


Coco ce2d08a: removed _argN temporaries for readability/consistency

--- nodes.coffee
+++ nodes.coffee
@@ -1056,7 +1056,12 @@

   asReference: (o) ->
     return @reference if @reference
-    node = if @isComplex() then new Literal o.scope.freeVariable 'arg' else @name
+    node = @name
+    if node.this
+      node = node.properties[0].name
+      node = new Literal '_' + node.value if node.value.reserved
+    else if node.isComplex()
+      node = new Literal o.scope.freeVariable 'arg'
     node = new Value node
     node = new Splat node if @splat
     @reference = node


Fixing #600. JS statements that must be expressions but contain pure statements cause syntax errors.

--- nodes.coffee
+++ nodes.coffee
@@ -44,8 +44,7 @@
     @tab     = o.indent
     top     = if @topSensitive() then @options.top else del @options, 'top'
     closure = @isStatement(o) and not @isPureStatement() and not top and
-              not @options.asStatement and this not instanceof Comment and
-              not @containsPureStatement()
+              not @options.asStatement and this not instanceof Comment
     code = if closure then @compileClosure(@options) else @compileNode(@options)
     code

@@ -53,7 +52,7 @@
   # object with their parent closure, to preserve the expected lexical scope.
   compileClosure: (o) ->
     o.sharedScope = o.scope
-    Closure.wrap(this).compile o
+    Closure.wrap(this, no, yes).compile o

   # If the code generation wishes to use the result of a complex expression
   # in multiple places, ensure that the expression is only ever evaluated once,
@@ -447,7 +446,7 @@
   # Grab the reference to the superclass' implementation of the current method.
   superReference: (o) ->
     {method} = o.scope
-    throw Error "cannot call super outside of a function" unless method
+    throw Error "cannot call super outside of a function." unless method
     {name} = method
     throw Error "cannot call super on an anonymous function." unless name
     if method.klass
@@ -1617,7 +1616,10 @@
   # Wrap the expressions body, unless it contains a pure statement,
   # in which case, no dice. If the body mentions `this` or `arguments`,
   # then make sure that the closure wrapper preserves the original values.
-  wrap: (expressions, statement) ->
+  wrap: (expressions, statement, force) ->
+    if expressions.containsPureStatement()
+      return expressions unless force
+      throw new Error 'cannot include a pure statement in an expression.'
     return expressions if expressions.containsPureStatement()
     func = new Parens new Code [], Expressions.wrap [expressions]
     args = []


full revert of 4ce374be25f9d4c0698288bd65f46d24ad707712

--- nodes.coffee
+++ nodes.coffee
@@ -991,7 +991,7 @@
         val = new Value new Literal(vvar), [new (if acc then Access else Index) idx]
       assigns.push new Assign(obj, val, null, param: @param).compile o, LEVEL_TOP
     assigns.push vvar unless top
-    code = (compact assigns).join ', '
+    code = assigns.join ', '
     if o.level < LEVEL_LIST then code else "(#{code})"

   # When compiling a conditional assignment, take care to ensure that the


Fixed port in console.log

Fixed port in console.log

--- prviewer.coffee
+++ prviewer.coffee
@@ -359,5 +359,5 @@
 # -------------------------------------------------------------------------

 http.createServer(app).listen port, ->
-  console.log "Pull Request Viewer listening on http://localhost:#{ argv.port }"
-
+  console.log "Pull Request Viewer listening on http://localhost:#{ port }"
+


OOP love for nodes.coffee

OOP love for nodes.coffee

--- nodes.coffee
+++ nodes.coffee
@@ -15,18 +15,6 @@

 # Import the helpers we plan to use.
 {compact, flatten, merge, del, index_of}: helpers
-
-# Helper function that registers a node class. If `is_statement` is passed,
-# marks the node as a JavaScript *statement*, or as a *pure_statement*.
-# Statements must be wrapped in a closure when used as an expression, and
-# nodes tagged as *pure_statement* cannot be closure-wrapped without losing
-# their meaning.
-register: (klass, o) ->
-  o: or {}
-  klass::constructor_name: o.name
-  (klass::is_statement: -> true) if o.is_statement
-  (klass::is_pure_statement: -> true) if o.is_pure_statement
-  (klass::children_attributes: o.children) if o.children

 #### BaseNode

@@ -40,6 +28,17 @@
 # being requested by the surrounding function), information about the current
 # scope, and indentation level.
 exports.BaseNode: class BaseNode
+
+  # If `is_statement` is passed, marks the node as a JavaScript *statement*,
+  # or as a *pure_statement* if `is_pure_statement`. Statements must be
+  # wrapped in a closure when used as an expression, and nodes tagged as
+  # *pure_statement* cannot be closure-wrapped without losing their meaning.
+  constructor: (o) ->
+    o: or {}
+    @constructor_name:        o.name     if o.name
+    @children_attributes:     o.children if o.children
+    (@is_statement:        -> true)      if o.is_statement
+    (@is_pure_statement:   -> true)      if o.is_pure_statement

   # Common logic for determining whether to wrap this node in a closure before
   # compiling it, or to compile directly. We need to wrap if this node is a
@@ -147,8 +146,6 @@
   is_pure_statement:    -> false
   top_sensitive:        -> false

-register BaseNode, {name: 'BaseNode'}
-
 #### Expressions

 # The expressions body is the list of expressions that forms the body of an
@@ -157,6 +154,7 @@
 exports.Expressions: class Expressions extends BaseNode

   constructor: (nodes) ->
+    super {name: 'Expressions', is_statement: yes, children: ['expressions']}
     @expressions: compact flatten nodes or []

   # Tack an expression on to the end of this expression list.
@@ -227,8 +225,6 @@
   return nodes[0] if nodes.length is 1 and nodes[0] instanceof Expressions
   new Expressions(nodes)

-register Expressions, {name: 'Expressions', is_statement: yes, children: ['expressions']}
-
 #### LiteralNode

 # Literals are static values that can be passed through directly into
@@ -237,6 +233,7 @@
 exports.LiteralNode: class LiteralNode extends BaseNode

   constructor: (value) ->
+    super {name: 'LiteralNode'}
     @value: value

   # Break and continue must be treated as pure statements -- they lose their
@@ -253,8 +250,6 @@
   toString: (idt) ->
     " \"$@value\""

-register LiteralNode, {name: 'LiteralNode'}
-
 #### ReturnNode

 # A `return` is a *pure_statement* -- wrapping it in a closure wouldn't
@@ -262,6 +257,7 @@
 exports.ReturnNode: class ReturnNode extends BaseNode

   constructor: (expression) ->
+    super {name: 'ReturnNode', is_statement: yes, is_pure_statement: yes, children: ['expression']}
     @expression: expression

   top_sensitive: ->
@@ -274,8 +270,6 @@
     o.as_statement: true if @expression.is_statement()
     "${@tab}return ${@expression.compile(o)};"

-register ReturnNode, {name: 'ReturnNode', is_statement: yes, is_pure_statement: yes, children: ['expression']}
-
 #### ValueNode

 # A value, variable or literal or parenthesized, indexed or dotted into,
@@ -286,6 +280,7 @@

   # A **ValueNode** has a base and a list of property accesses.
   constructor: (base, properties) ->
+    super {name: 'ValueNode', children: ['base', 'properties']}
     @base: base
     @properties: (properties or [])

@@ -356,8 +351,6 @@

     if op and @wrapped then "($complete)" else complete

-register ValueNode, {name: 'ValueNode', children: ['base', 'properties']}
-
 #### CommentNode

 # CoffeeScript passes through comments as JavaScript comments at the
@@ -365,9 +358,9 @@
 exports.CommentNode: class CommentNode extends BaseNode

   constructor: (lines, type) ->
+    super {name: 'CommentNode', is_statement: yes}
     @lines: lines
     @type: type
-    this

   make_return: ->
     this
@@ -379,8 +372,6 @@
     else
       "$@tab//" + @lines.join("\n$@tab//")

-register CommentNode, {name: 'CommentNode', is_statement: yes}
-
 #### CallNode

 # Node for a function invocation. Takes care of converting `super()` calls into
@@ -388,6 +379,7 @@
 exports.CallNode: class CallNode extends BaseNode

   constructor: (variable, args) ->
+    super {name: 'CallNode', children: ['variable', 'args']}
     @is_new:   false
     @is_super: variable is 'super'
     @variable: if @is_super then null else variable
@@ -438,8 +430,6 @@
       meth: "($temp = ${ @variable.source })${ @variable.last }"
     "${@prefix()}${meth}.apply($obj, ${ @compile_splat_arguments(o) })"

-register CallNode, {name: 'CallNode', children: ['variable', 'args']}
-
 #### CurryNode

 # Binds a context object and a list of arguments to a function,
@@ -448,6 +438,7 @@
 exports.CurryNode: class CurryNode extends CallNode

   constructor: (meth, args) ->
+    super {name: 'CurryNode', children: ['meth', 'context', 'args']}
     @meth: meth
     @context: args[0]
     @args: (args.slice(1) or [])
@@ -463,8 +454,6 @@
     ref: new ValueNode literal utility 'bind'
     (new CallNode(ref, [@meth, @context, literal(@arguments(o))])).compile o

-register CurryNode, {name: 'CurryNode', children: ['meth', 'context', 'args']}
-
 #### ExtendsNode

 # Node to extend an object's prototype with an ancestor object.
@@ -473,6 +462,7 @@
 exports.ExtendsNode: class ExtendsNode extends BaseNode

   constructor: (child, parent) ->
+    super {name: 'ExtendsNode', children: ['child', 'parent']}
     @child: child
     @parent: parent

@@ -480,8 +470,6 @@
   compile_node: (o) ->
     ref:  new ValueNode literal utility 'extends'
     (new CallNode ref, [@child, @parent]).compile o
-
-register ExtendsNode, {name: 'ExtendsNode', children: ['child', 'parent']}

 #### AccessorNode

@@ -490,24 +478,23 @@
 exports.AccessorNode: class AccessorNode extends BaseNode

   constructor: (name, tag) ->
+    super {name: 'AccessorNode', children: ['name']}
     @name: name
     @prototype: tag is 'prototype'
     @soak_node: tag is 'soak'
-    this

   compile_node: (o) ->
     o.chain_root.wrapped: or @soak_node
     proto_part: if @prototype then 'prototype.' else ''
     ".$proto_part${@name.compile(o)}"

-register AccessorNode, {name: 'AccessorNode', children: ['name']}
-
 #### IndexNode

 # A `[ ... ]` indexed accessor into an array or object.
 exports.IndexNode: class IndexNode extends BaseNode

   constructor: (index, tag) ->
+    super {name: 'IndexNode', children: ['index']}
     @index: index
     @soak_node: tag is 'soak'

@@ -515,8 +502,6 @@
     o.chain_root.wrapped: or @soak_node
     idx: @index.compile o
     "[$idx]"
-
-register IndexNode, {name: 'IndexNode', children: ['index']}

 #### RangeNode

@@ -526,6 +511,7 @@
 exports.RangeNode: class RangeNode extends BaseNode

   constructor: (from, to, exclusive) ->
+    super {name: 'RangeNode', children: ['from', 'to']}
     @from: from
     @to: to
     @exclusive: !!exclusive
@@ -560,8 +546,6 @@
     arr:  Expressions.wrap([new ForNode(body, {source: (new ValueNode(this))}, literal(name))])
     (new ParentheticalNode(new CallNode(new CodeNode([], arr.make_return())))).compile(o)

-register RangeNode, {name: 'RangeNode', children: ['from', 'to']}
-
 #### SliceNode

 # An array slice literal. Unlike JavaScript's `Array#slice`, the second parameter
@@ -570,8 +554,8 @@
 exports.SliceNode: class SliceNode extends BaseNode

   constructor: (range) ->
+    super {name: 'SliceNode', children: ['range']}
     @range: range
-    this

   compile_node: (o) ->
     from:       @range.from.compile(o)
@@ -579,15 +563,14 @@
     plus_part:  if @range.exclusive then '' else ' + 1'
     ".slice($from, $to$plus_part)"

-register SliceNode, {name: 'SliceNode', children: ['range']}
-
 #### ObjectNode

 # An object literal, nothing fancy.
 exports.ObjectNode: class ObjectNode extends BaseNode

   constructor: (props) ->
-   @objects: @properties: props or []
+    super {name: 'ObjectNode', children: ['properties']}
+    @objects: @properties: props or []

   # All the mucking about with commas is to make sure that CommentNodes and
   # AssignNodes get interleaved correctly, with no trailing commas or
@@ -607,14 +590,13 @@
     inner: if props then '\n' + props + '\n' + @idt() else ''
     "{$inner}"

-register ObjectNode, {name: 'ObjectNode', children: ['properties']}
-
 #### ArrayNode

 # An array literal.
 exports.ArrayNode: class ArrayNode extends BaseNode

   constructor: (objects) ->
+    super {name: 'ArrayNode', children: ['objects']}
     @objects: objects or []
     @compile_splat_literal: SplatNode.compile_mixed_array <- @, @objects

@@ -637,8 +619,6 @@
     else
       "[$objects]"

-register ArrayNode, {name: 'ArrayNode', children: ['objects']}
-
 #### ClassNode

 # The CoffeeScript class definition.
@@ -647,6 +627,7 @@
   # Initialize a **ClassNode** with its name, an optional superclass, and a
   # list of prototype property assignments.
   constructor: (variable, parent, props) ->
+    super {name: 'ClassNode', is_statement: yes, children: ['variable', 'parent', 'properties']}
     @variable: variable
     @parent: parent
     @properties: props or []
@@ -692,8 +673,6 @@
     returns:   if @returns      then new ReturnNode(@variable).compile(o)  else ''
     "$construct$extension$props$returns"

-register ClassNode, {name: 'ClassNode', is_statement: yes, children: ['variable', 'parent', 'properties']}
-
 #### AssignNode

 # The **AssignNode** is used to assign a local variable to value, or to set the
@@ -705,6 +684,7 @@
   LEADING_DOT:  /^\.(prototype\.)?/

   constructor: (variable, value, context) ->
+    super {name: 'AssignNode', children: ['variable', 'value']}
     @variable: variable
     @value: value
     @context: context
@@ -793,8 +773,6 @@
     val:    @value.compile(o)
     "${name}.splice.apply($name, [$from, $to].concat($val))"

-register AssignNode, {name: 'AssignNode', children: ['variable', 'value']}
-
 #### CodeNode

 # A function definition. This is the only node that creates a new Scope.
@@ -803,6 +781,7 @@
 exports.CodeNode: class CodeNode extends BaseNode

   constructor: (params, body, tag) ->
+    super {name: 'CodeNode', children: ['params', 'body']}
     @params:  params or []
     @body:    body or new Expressions()
     @bound:   tag is 'boundfunc'
@@ -858,8 +837,6 @@
     children: (child.toString(idt + TAB) for child in @children()).join('')
     "\n$idt$children"

-register CodeNode, {name: 'CodeNode', children: ['params', 'body']}
-
 #### SplatNode

 # A splat, either as a parameter to a function, an argument to a call,
@@ -867,6 +844,7 @@
 exports.SplatNode: class SplatNode extends BaseNode

   constructor: (name) ->
+    super {name: 'SplatNode', children: ['name']}
     name: literal(name) unless name.compile
     @name: name

@@ -914,8 +892,6 @@
       i: + 1
     args.join('')

-register SplatNode, {name: 'SplatNode', children: ['name']}
-
 #### WhileNode

 # A while loop, the only sort of low-level loop exposed by CoffeeScript. From
@@ -924,6 +900,7 @@
 exports.WhileNode: class WhileNode extends BaseNode

   constructor: (condition, opts) ->
+    super {name: 'WhileNode', is_statement: yes, children: ['condition', 'guard', 'body']}
     if opts and opts.invert
       condition: new ParentheticalNode condition if condition instanceof OpNode
       condition: new OpNode('!', condition)
@@ -962,8 +939,6 @@
       post: ''
     "$pre {\n${ @body.compile(o) }\n$@tab}$post"

-register WhileNode, {name: 'WhileNode', is_statement: yes, children: ['condition', 'guard', 'body']}
-
 #### OpNode

 # Simple Arithmetic and logical operations. Performs some conversion from
@@ -987,7 +962,7 @@
   PREFIX_OPERATORS: ['typeof', 'delete']

   constructor: (operator, first, second, flip) ->
-    @constructor_name: + ' ' + operator
+    super {name: "OpNode $operator", children: ['first', 'second']}
     @first: first
     @second: second
     @operator: @CONVERSIONS[operator] or operator
@@ -1041,19 +1016,17 @@
     parts: parts.reverse() if @flip
     parts.join('')

-register OpNode, {name: 'OpNode', children: ['first', 'second']}
-
 #### TryNode

 # A classic *try/catch/finally* block.
 exports.TryNode: class TryNode extends BaseNode

   constructor: (attempt, error, recovery, ensure) ->
+    super {name: 'TryNode', is_statement: true, children: ['attempt', 'recovery', 'ensure']}
     @attempt: attempt
     @recovery: recovery
     @ensure: ensure
     @error: error
-    this

   make_return: ->
     @attempt: @attempt.make_return() if @attempt
@@ -1071,14 +1044,13 @@
     finally_part: (@ensure or '') and ' finally {\n' + @ensure.compile(merge(o)) + "\n$@tab}"
     "${@tab}try {\n$attempt_part\n$@tab}$catch_part$finally_part"

-register TryNode, {name: 'TryNode', is_statement: true, children: ['attempt', 'recovery', 'ensure']}
-
 #### ThrowNode

 # Simple node to throw an exception.
 exports.ThrowNode: class ThrowNode extends BaseNode

   constructor: (expression) ->
+    super {name: 'ThrowNode', is_statement: true, children: ['expression']}
     @expression: expression

   # A **ThrowNode** is already a return, of sorts...
@@ -1087,8 +1059,6 @@

   compile_node: (o) ->
     "${@tab}throw ${@expression.compile(o)};"
-
-register ThrowNode, {name: 'ThrowNode', is_statement: true, children: ['expression']}

 #### ExistenceNode

@@ -1098,6 +1068,7 @@
 exports.ExistenceNode: class ExistenceNode extends BaseNode

   constructor: (expression) ->
+    super {name: 'ExistenceNode', children: ['expression']}
     @expression: expression

   compile_node: (o) ->
@@ -1113,8 +1084,6 @@
     [first, second]: [first.compile(o), second.compile(o)]
     "(typeof $first !== \"undefined\" && $second !== null)"

-register ExistenceNode, {name: 'ExistenceNode', children: ['expression']}
-
 #### ParentheticalNode

 # An extra set of parentheses, specified explicitly in the source. At one time
@@ -1125,6 +1094,7 @@
 exports.ParentheticalNode: class ParentheticalNode extends BaseNode

   constructor: (expression) ->
+    super {name: 'ParentheticalNode', children: ['expression']}
     @expression: expression

   is_statement: ->
@@ -1139,8 +1109,6 @@
     l:    code.length
     code: code.substr(o, l-1) if code.substr(l-1, 1) is ';'
     if @expression instanceof AssignNode then code else "($code)"
-
-register ParentheticalNode, {name: 'ParentheticalNode', children: ['expression']}

 #### ForNode

@@ -1154,6 +1122,7 @@
 exports.ForNode: class ForNode extends BaseNode

   constructor: (body, source, name, index) ->
+    super {name: 'ForNode', is_statement: yes, children: ['body', 'source', 'guard']}
     @body:    body
     @name:    name
     @index:   index or null
@@ -1224,8 +1193,6 @@
     close:          if @object then '}}' else '}'
     "$set_result${source_part}for ($for_part) {\n$var_part$body\n$@tab$close$return_result"

-register ForNode, {name: 'ForNode', is_statement: yes, children: ['body', 'source', 'guard']}
-
 #### IfNode

 # *If/else* statements. Our *switch/when* will be compiled into this. Acts as an
@@ -1236,6 +1203,7 @@
 exports.IfNode: class IfNode extends BaseNode

   constructor: (condition, body, tags) ->
+    super {name: 'IfNode', children: ['condition', 'body', 'else_body', 'assigner']}
     @condition: condition
     @body:      body
     @else_body: null
@@ -1327,8 +1295,6 @@
     else_part:  if @else_body then @else_body_node().compile(o) else 'null'
     "$if_part : $else_part"

-register IfNode, {name: 'IfNode', children: ['condition', 'body', 'else_body', 'assigner']}
-
 # Faux-Nodes
 # ----------