script block: don't print empty script tags

We were printing script tags with empty src attributes

--- workers/social/lib/social/render/scriptblock.coffee
+++ workers/social/lib/social/render/scriptblock.coffee
@@ -36,9 +36,9 @@
     <script>(function(e,b){if(!b.__SV){var a,f,i,g;window.mixpanel=b;a=e.createElement("script");a.type="text/javascript";a.async=!0;a.src=("https:"===e.location.protocol?"https:":"http:")+'//cdn.mxpnl.com/libs/mixpanel-2.2.min.js';f=e.getElementsByTagName("script")[0];f.parentNode.insertBefore(a,f);b._i=[];b.init=function(a,e,d){function f(b,h){var a=h.split(".");2==a.length&&(b=b[a[0]],h=a[1]);b[h]=function(){b.push([h].concat(Array.prototype.slice.call(arguments,0)))}}var c=b;"undefined"!==typeof d?c=b[d]=[]:d="mixpanel";c.people=c.people||[];c.toString=function(b){var a="mixpanel";"mixpanel"!==d&&(a+="."+d);b||(a+=" (stub)");return a};c.people.toString=function(){return c.toString(1)+".people (stub)"};i="disable track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.set_once people.increment people.append people.track_charge people.clear_charges people.delete_user".split(" ");for(g=0;g<i.length;g++)f(c,i[g]);b._i.push([a,e,d])};b.__SV=1.2}})(document,window.mixpanel||[]);mixpanel.init("#{KONFIG.mixpanel}");</script>

     <script src='/a/js/kd.#{KONFIG.version}.js'></script>
-    <script src='#{if intro then '/a/js/introapp.'+KONFIG.version+'.js' else ''}'></script>
+    #{if intro then "<script src='/a/js/introapp.#{ KONFIG.version }.js'></script>" else ''}
     <script src='/a/js/koding.#{KONFIG.version}.js'></script>
-    <script src='#{if landing then '/a/js/landingapp.'+KONFIG.version+'.js' else ''}'></script>
+    #{if landing then "<script src='/a/js/landingapp.#{ KONFIG.version }.js'></script>" else ''}
     <script>KD.prefetchedFeeds=#{encodedFeed};</script>


Redundant in backbone 1.0

Redundant in backbone 1.0

--- core/app/backbone/views/sub_comments/sub_comments_list_view.coffee
+++ core/app/backbone/views/sub_comments/sub_comments_list_view.coffee
@@ -4,7 +4,7 @@
   template: 'sub_comments/sub_comments_list'

   initialize: ->
-    @collection.fetch update: true # only fires 'add' and 'remove' events
+    @collection.fetch()

   onShow: ->
     if Factlink.Global.signed_in


VMKite: wrap tell method too to pass vmName with arguments via @chris

--- client/Main/kite/kites/vmkite.coffee
+++ client/Main/kite/kites/vmkite.coffee
@@ -1,6 +1,9 @@
 class KodingKite_VmKite extends KodingKite
-
+
   @createMethod = (ctx, { method, rpcMethod }) ->
     ctx[method] = (payload  = {}) ->
-      payload.vmName = @options.correlationName
       @tell rpcMethod, payload
+
+  tell: (method, payload = {}) ->
+    payload.vmName = @options.correlationName
+    super method, payload


Pricing: always on copy fixed

Pricing: always on copy fixed

--- client/Pricing/views/features.coffee
+++ client/Pricing/views/features.coffee
@@ -42,9 +42,10 @@

ALWAYS ON

-          The maximum number of VMs that you can create. For example,
-          if your total VM quota is 10, and you have 3GB RAM available,
-          you can only run 3 x 1GB RAM, and you will have 7 VMs that are turned off.
+          The maximum number of VMs that you can keep turned on 24/7. For example,
+          you bought a package comes with 10 VMs and one always-on VM.
+          The VM you mark as always-on will work 24/7 but the others will be taken
+          into sleep automatically in 15 minutes after you logout.

           All Koding VMs are optimized for software development,


cleanup; use fat-arrow function binding

cleanup; use fat-arrow function binding

--- lib/assets/javascripts/stark/stark.js.coffee
+++ lib/assets/javascripts/stark/stark.js.coffee
@@ -46,10 +46,9 @@

     # cleanup template namespace
     # removes /templates/ from the middle of the path string to make
-    # referencing in views a bit [c]leaner
+    # referencing in views a bit cleaner
     #
     # also removes templates/ from the start
-    t = @
     r = /\/templates\//
     s = /^templates\//
     _.each _.keys(JST), (k) ->
@@ -180,18 +179,17 @@
     # load data into state, retrieve models which are missing
     needed = state.load_data(state_data)
     if needed? && needed.length > 0
-      app = @
-      Backbone.multi_fetch needed, {initial_load: true}, (err, results) ->
+      Backbone.multi_fetch needed, {initial_load: true}, (err, results) =>
         if err and err.status >= 400 and err.status < 500
           # session timeout
-          app.redir = [ state_name, state_data ]
+          @redir = [ state_name, state_data ]
           state.dispose() # trash state we were building
-          app.redir_to_login()
+          @redir_to_login()
           return

-        app.render_views(state)
-        app.end_group()
-        app.stop_timer(timer_name)
+        @render_views(state)
+        @end_group()
+        @stop_timer(timer_name)

     else
       @render_views(state)


home: minor fix

home: minor fix

--- client/landing/app/Applications/Activity.kdapplication/AppView.coffee
+++ client/landing/app/Applications/Activity.kdapplication/AppView.coffee
@@ -56,7 +56,7 @@
       else
         @setClass "fixed"
     else
-      @unsetClass "fixed"
+      @unsetClass "fixed" unless KD.isLoggedIn()

   navigateHome:(itemData)->


fixup! Handle properly `analytics_session` absence

--- src/analytics.coffee
+++ src/analytics.coffee
@@ -10,15 +10,12 @@

       @session.then @onSession, @onNoSession

-    onNoSession: () =>
-      console.log 'no session returned'
+    onNoSession: ->

     onSession: (analytics_session) =>
-      console.log "%canalytics_session: #{analytics_session}", 'color: green'
       beacon_url = Settings.url.beacon(analytics_session)

       @actions.sendTo(beacon_url).then =>
-        console.log "%cAll actions reported!!", 'color: green'
         @actions.redirect(analytics_session)

   return Analytics


revoke exceptions handling; save file after conversion

--- lib/atomizr.coffee
+++ lib/atomizr.coffee
@@ -56,10 +56,6 @@
     @subscriptions.add atom.commands.add 'atom-workspace', 'atomizr:convert-sublime-text-snippet-to-atom': => @sublSnippetToAtom()
     @subscriptions.add atom.commands.add 'atom-workspace', 'atomizr:toggle-atom-snippet-format': => @atomToAtom()

-    # Write to scope replacement rules to config
-    unless atom.config.get("atomizr.scopeReplacements")
-      atom.config.set("atomizr.scopeReplacements", @exceptions)
-
   deactivate: ->
     @subscriptions.dispose()

@@ -102,7 +98,7 @@
     for k,v of obj

       # Get scope, convert if necessary
-      for subl,atom of atom.config.get("atomizr.scopeReplacements")
+      for subl,atom of @exceptions
         if k is atom
           sublime.scope = subl
         else
@@ -179,7 +175,7 @@
     completions = {}

     # Get scope, convert if necessary
-    for subl,atom of atom.config.get("atomizr.scopeReplacements")
+    for subl,atom of @exceptions
       if obj.scope is subl
         scope = atom
         break
@@ -234,7 +230,7 @@
       return

     # Get scope, convert if necessary
-    for subl,atom of atom.config.get("atomizr.scopeReplacements")
+    for subl,atom of @exceptions
       if obj.scope.toString() is subl
         scope = atom
         break
@@ -345,6 +341,8 @@
       outputFile = path.join parentDir, baseName + ".#{extension}"
       fs.rename inputFile, outputFile

+      atom.workspace.saveActivePaneItem()
+
   invalidFormat: (type) ->
     atom.notifications.addWarning("Atomizr", detail: "This doesn't seem to be a valid #{type} file. Aborting.", dismissable: false)


bugfix: set checkboxes in neuropil edit dialog correctly

--- client/neuropils.coffee
+++ client/neuropils.coffee
@@ -71,7 +71,7 @@

     for tmp in myself.neuropils
       if tmp._id == doc._id
-        if "unspecific" in tmp.type
+        if "unspecified" in tmp.type
           doc.unspecific_is_checked = true
         if "output" in tmp.type
           doc.output_is_checked = true


weather script

weather script

--- scripts/weather.coffee
+++ scripts/weather.coffee
@@ -8,12 +8,12 @@
   robot.respond /weather (.*)/i, (msg) ->
     location = escape(msg.match[1]) or "Kansas City"
     locationOptions =
-      uri: 'http://maps.googleapis.com/maps/api/geocode/json',
+      uri: 'http://maps.googleapis.com/maps/api/geocode/json'
       qs:
         address: location
       headers:
-          'User-Agent': 'Request-Promise'
-    json: true
+        'User-Agent': 'Request-Promise'
+      json: true

     rp(locationOptions)
       .then( (data) ->
@@ -28,22 +28,22 @@
             'User-Agent': 'Request-Promise'
           json: true

-      rp(forecastOptions)
-        .then((data) ->
-          currentTemp = data.currently.temperature
-          summary = data.currently.summary
-          icon = data.currently.icon
-          feelsLike = data.currently.apparentTemperature
-          dailyData = data.daily[0]
-          maxTemp = dailyData.temperatureMax
-          minTemp = dailyData.temperatureMin
-          msg.send('Current Temp: ' + currentTemp + '\n' +
-                  'Max Temp:      ' + maxTemp + '\n' +
-                  'Min Temp:      ' + minTemp )
-        )
-        .catch( (err) ->
-          msg.send('Error in forecast call.\n' + err)
-        )
+        rp(forecastOptions)
+          .then((data) ->
+            currentTemp = data.currently.temperature
+            summary = data.currently.summary
+            icon = data.currently.icon
+            feelsLike = data.currently.apparentTemperature
+            dailyData = data.daily[0]
+            maxTemp = dailyData.temperatureMax
+            minTemp = dailyData.temperatureMin
+            msg.send('Current Temp: ' + currentTemp + '\n' +
+                    'Max Temp:      ' + maxTemp + '\n' +
+                    'Min Temp:      ' + minTemp )
+          )
+          .catch( (err) ->
+            msg.send('Error in forecast call.\n' + err)
+          )
     )
     .catch( (err) ->
       msg.send("Error in google geocode call.\n" +  err )


Limited snippet list to the current or global scopes based on editor grammer

--- lib/snippets-loader.coffee
+++ lib/snippets-loader.coffee
@@ -6,7 +6,8 @@
 module.exports =
 class SnippetsLoader
   loaded: false
-  constructor: (@editor) -> return
+  constructor: (@editor) ->
+    @grammar = @editor.getGrammar()

   getUserSnippetsPath: ->
     userSnippetsPath = CSON.resolve path.join(atom.getConfigDirPath(), "snippets")
@@ -30,8 +31,7 @@
         callback?()

   loadSyntaxPackages: (callback) ->
-    grammar = @editor.getGrammar()
-    grammarPath = grammar.path
+    grammarPath = @grammar.path

     if grammarPath
       packagePath = path.resolve grammarPath, "../.."
@@ -62,6 +62,10 @@

   add: (filePath, snippetsBySelector) ->
     for selector, snippetsByName of snippetsBySelector
+      # Skip snippet if it's not in the same scope or the global scope
+      if selector.indexOf(@grammar.scopeName) is -1 and
+      selector.indexOf("*") is -1
+        continue
       for label, snippet of snippetsByName
         snippet.label = label
         @snippets[label] = snippet


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/animation.coffee
+++ src/animation.coffee
@@ -14,9 +14,7 @@
     $.extend data,
       draw: (canvas, x, y) ->
         canvas.withTransform Matrix.translation(x, y), () ->
-          data.animations.each (animation) ->
-            if activeAnimation == animation
-              spriteLookup[currentSprite].draw(canvas, 0, 0)
+          spriteLookup[currentSprite].draw(canvas, 0, 0)

       update: ->
         advanceFrame(activeAnimation)


updated mail template

updated mail template

--- config/mailer.coffee
+++ config/mailer.coffee
@@ -13,7 +13,7 @@

 		message =

-		  html: '

Hi ' + user.local.pseudo + ' welcome to Challenge Your Friends there is only one final step remaining!

 To become a true challenger one does not simply only create an account but proves himself as a human being!

Please confirm your email by clicking here

 You can also copy/past the following link in your browser: " + gotoUrl + "

- The Team

Challenge your Friends (CyF) is a new service which allows you to receive and send challenge to gamers around the world on any game.Learn more

'
+		  html: '

Hi ' + user.local.pseudo + ' welcome to Challenge Your Friends there is only one final step remaining!

 To become a true challenger one does not simply only create an account but proves himself as a human being!

Please confirm your email by clicking here

 You can also copy/past the following link in your browser:
"' + gotoUrl + '"

- The Team

Challenge your Friends (CyF) is a new service which allows you to receive and send challenge to gamers around the world on any game.Learn more
 For the latest news, follows us on Google+, Twitter and Facebook

'
 		  # text: "Example text content"
 		  subject: user.local.pseudo + " please confirm your email!"
 		  from_email: keys.support.email


Improve tap responsiveness

Improve tap responsiveness

--- src/assets/javascripts/mobile/index.coffee
+++ src/assets/javascripts/mobile/index.coffee
@@ -1,5 +1,5 @@
 $ = require 'jquery'
-Hammer = require 'hammerjs'
+attachFastClick = require 'fastclick'
 Preloader = require '../preloader'
 Carousel = require './carousel'
 Backdrops = require '../backdrops'
@@ -8,12 +8,16 @@
   constructor: ->
     $('body').addClass 'is-mobile'

-    new Preloader(application: 'mobile').until '.pane--backdrop'
+    preloader = new Preloader application: 'mobile'
+    preloader.until '.pane--backdrop'

     carousel = new Carousel $el: $('#panes')
     backdrops = new Backdrops $els: $('.pane--backdrop img')

-    $('.info').each ->
-      hammer = new Hammer this
-      hammer.on 'tap', (e) ->
-        $(e.target).closest('.pane').toggleClass 'is-captioned'
+    @bindEvents()
+
+  bindEvents: ->
+    attachFastClick document.body
+
+    $('.info').on 'click', (e) ->
+      $(e.target).closest('.pane').toggleClass 'is-captioned'


Vastly simplified writing regular expressions

--- lib/supported-files.coffee
+++ lib/supported-files.coffee
@@ -2,39 +2,100 @@
 class SupportedFiles
   @types =
     'js':
-      regExpStr: 'function\\s*REPLACE\\s*\\(|REPLACE\\s*(=|:)\\s*\
-        function\\s*\\('
+      regExp: ///
+        # matches 'function REPLACE('
+        function \s* REPLACE \s* \(
+
+          # or
+          |
+
+        # matches 'REPLACE = function (' or 'REPLACE: function ('
+        REPLACE \s* (=|:) \s* function \s* \(
+      ///
       isTabBased: false
+
     'ts':
-      regExpStr: 'function\\s*REPLACE\\s*\\(|REPLACE\\s*(=|:)\\s*\
-        function\\s*\\('
+      regExp: ///
+        # matches 'function REPLACE('
+        function \s* REPLACE \s* \(
+
+          # or
+          |
+
+        # matches 'REPLACE = function (' or 'REPLACE: function ('
+        REPLACE \s* (=|:) \s* function \s* \(
+      ///
       isTabBased: false
+
     'jsx':
-      regExpStr: 'function\\s*REPLACE\\s*\\(|REPLACE\\s*(=|:)\\s*\
-        function\\s*\\('
+      regExp: ///
+        # matches 'function REPLACE('
+        function \s* REPLACE \s* \(
+
+          # or
+          |
+
+        # matches 'REPLACE = function (' or 'REPLACE: function ('
+        REPLACE \s* (=|:) \s* function \s* \(
+      ///
       isTabBased: false
+
     'php':
-      regExpStr: 'function\\s*REPLACE\\s*\\('
+      regExp: ///
+        # matches 'function REPLACE ('
+        function \s* REPLACE \s* \(
+      ///
       isTabBased: false
+
     'coffee':
       # TODO simplify
-      regExpStr: '(REPLACE\\s*(:|=)\\s*\\([\\,\\s\\w]*\\)\\s*(=>|->))|\
-      (REPLACE\\s*(:|=)\\s*(=>|->))'
+      regExp: ///
+        (
+          REPLACE \s* (:|=) \s* \( [\, \s \w]* \) \s* (=>|->)
+        )
+          |
+        (
+          REPLACE \s* (:|=) \s* (=>|->)
+        )
+      ///
       isTabBased: true
+
     'py':
-      regExpStr: 'def\\s*REPLACE\\s*\\('
+      regExp: ///
+        # matches 'def REPLACE('
+        def \s* REPLACE \s* \(
+      ///
       isTabBased: true
+
     'java':
-      regExpStr: '(public|private|protected)\\s*[\\w\\s\\S]*REPLACE\\s*\\('
+      #regExp: '(public|private|protected)\\s*[\\w\\s\\S]*REPLACE\\s*\\('
+      regExp: ///
+        # matches public or private or protected)
+        (public|private|protected) \s*
+        # matches any type of return value or function property
+        [\w\s\S]*
+        # matches 'REPLACE ('
+        REPLACE \s* \(
+      ///
       isTabBased: false
+
     # 'c':
-    #   regExpStr: ''
+    #   regExp: ''
     #   isTabBased: false
+
     # 'cpp':
-    #   regExpStr: ''
+    #   regExp: ''
     #   isTabBased: false
+
     'cs':
-      regExpStr: '(public|private|protected)\\s*[\\w\\s\\S]*REPLACE\\s*\\('
+      regExp: ///
+        # matches public or private or protected)
+        (public|private|protected) \s*
+        # matches any type of return value or function property
+        [\w\s\S]*
+        # matches 'REPLACE ('
+        REPLACE \s* \(
+      ///
       isTabBased: false

   @isSupported: (fileType) ->
@@ -50,5 +111,18 @@
     if not @types[fileType] then throw new Error "File type #{fileType} is not \
       supported"

-    regExpStr = @types[fileType].regExpStr
-    return new RegExp(regExpStr.replace /REPLACE/g, functionName)
+    # get the regular expression for the file type
+    regExp = @types[fileType].regExp
+
+    # convert to string so we can replace
+    regExpStr = regExp.toString()
+
+    # change all occurances of REPLACE placeholder to the actual function name
+    regExpStr = regExpStr.replace /REPLACE/g, functionName
+
+    # get rid of leading and ending slashes or else they will be escaped
+    # when converting back to a regular expression
+    regExpStr = regExpStr.replace /\//g, ""
+
+    # convert back to regular expression
+    return new RegExp(regExpStr)


make route file classier

make route file classier

--- config/routes.coffee
+++ config/routes.coffee
@@ -2,5 +2,5 @@
     map.get '/data/exist/:id/', 'data#exist'
     map.get '/data/:id/', 'data#find'

+    map.post '/data/', 'data#create'
     map.post '/data/:id/', 'data#create'
-    map.post '/data/', 'data#create'


fixed code snip widget fake data handling

--- client/landing/app/Applications/Activity.kdapplication/widgets/codesnippetwidget.coffee
+++ client/landing/app/Applications/Activity.kdapplication/widgets/codesnippetwidget.coffee
@@ -142,7 +142,7 @@
     @hiddenAceInputClone.setValue ''
     @hiddenAceInputClone.unsetClass 'warn-on-unsaved-data'

-  switchToEditView:(activity)->
+  switchToEditView:(activity,fake=no)->
     unless fake
       @submitBtn.setTitle "Edit code snippet"
       @addCustomData "activity", activity


appmanager appscontroller integration wip

--- core/KD.coffee
+++ core/KD.coffee
@@ -159,7 +159,7 @@
     options.background    ?= no           # a Boolean
     options.hiddenHandle  ?= no           # a Boolean
     options.route        or= ""           # a String
-    options.openWith     or= "lastActive" # a String "lastActive" or "prompt"
+    options.openWith     or= "lastActive" # a String "lastActive","forceNew" or "prompt"
     options.behavior     or= ""           # a String "application", "hideTabs", or ""
     options.thirdParty    ?= no           # a Boolean


Grid Template Cannot Be Saved Unless Uniquely Named

--- app/classifier/tasks/drawing/grid-buttons.cjsx
+++ app/classifier/tasks/drawing/grid-buttons.cjsx
@@ -1,5 +1,6 @@
 React = require 'react'
 Select = require 'react-select'
+debounce = require 'debounce'

 module.exports = React.createClass
   displayName: 'GridButtons'
@@ -45,7 +46,7 @@
     @props.preferences.preferences.activeTemplate = type
     @props.preferences.update 'preferences'
     if type is 'grid'
-      @setState gridSelect: true
+      @setState gridSelect: true, templateForm: null, error: null
     if type is null
       @setState gridSelect: false

@@ -84,26 +85,39 @@
     @props.preferences.update 'preferences'

   saveGrid: (e) ->
-    @props.annotation._completed = true
     e.preventDefault()
-    @activateTemplate 'grid'
+    if @uniqueName @refs.name.value
+      @props.annotation._completed = true
+      @activateTemplate 'grid'

-    newGrid = for cell in @props.annotation.value
-      Object.assign({}, cell)
-    displayName = @refs.name.value
-    @props.user.get('project_preferences', {project_id: @props.workflow.links.project}).then ([pref]) =>
-      pref.update 'preferences.grid': newGrid
-      if !pref.preferences?.savedGrids?
-        pref.update 'preferences.savedGrids': [{ value: Math.random(), label: displayName, template: newGrid}]
-        pref.save()
-      else
-        pref.preferences.savedGrids.push { value: Math.random(), label: displayName, template: newGrid}
-        pref.save()
-    @setState templateForm: false
-    @setState gridSelect: true
+      newGrid = for cell in @props.annotation.value
+        Object.assign({}, cell)
+      displayName = @refs.name.value
+      @props.user.get('project_preferences', {project_id: @props.workflow.links.project}).then ([pref]) =>
+        pref.update 'preferences.grid': newGrid
+        if !pref.preferences?.savedGrids?
+          pref.update 'preferences.savedGrids': [{ value: Math.random(), label: displayName, template: newGrid}]
+          pref.save()
+        else
+          pref.preferences.savedGrids.push { value: Math.random(), label: displayName, template: newGrid}
+          pref.save()
+      @setState templateForm: false
+      @setState gridSelect: true
+      @setState error: null if @state?.error
+
+  uniqueName: (name) ->
+    unique = true
+    @props.preferences.preferences?.savedGrids?.map (template) ->
+      unique = false if template.label == name
+    @setState error: true if unique is false
+    unique

   renderTemplateSave: ->
     <form onSubmit={@saveGrid}>
+      {if @state.error
+

+          Template Name Should be Unique
+
}
       <input className="template-name-input" ref="name" placeholder="Template Name" />

       <button type="submit" className="template-form-button">Save Template</button>
       <button type="button" className="template-form-button" onClick={@setState.bind this, templateForm: null, null}>Cancel</button>


getCompletionsForCursorScope -> builtinCompletionsForCursorScope

--- lib/symbol-provider.coffee
+++ lib/symbol-provider.coffee
@@ -111,7 +111,7 @@
   findSuggestionsForWord: (options) =>
     return unless @symbolList?
     # Merge the scope specific words into the default word list
-    symbolList = @symbolList.concat(@getCompletionsForCursorScope())
+    symbolList = @symbolList.concat(@builtinCompletionsForCursorScope())

     words =
       if atom.config.get("autocomplete-plus.strictMatching")
@@ -157,7 +157,7 @@
   # Private: Finds autocompletions in the current syntax scope (e.g. css values)
   #
   # Returns an {Array} of strings
-  getCompletionsForCursorScope: =>
+  builtinCompletionsForCursorScope: =>
     cursorScope = @editor.scopeDescriptorForBufferPosition(@editor.getCursorBufferPosition())
     completions = @settingsForScopeDescriptor(cursorScope, "editor.completions")
     scopedCompletions = []


use Wreqr's reset() method

use Wreqr's reset() method

--- zechat/static/app.coffee
+++ zechat/static/app.coffee
@@ -27,7 +27,4 @@

 zc.remove_handlers = (app) ->
-  app.commands.removeAllHandlers()
-  app.reqres.removeAllHandlers()
-  _.values(app.vent._events).forEach (event) ->
-    app.vent.off(event.callback)
+  Backbone.Wreqr.radio.channel('global').reset()


remove unnecessary imports

remove unnecessary imports

--- library/membrane/events.coffee
+++ library/membrane/events.coffee
@@ -37,10 +37,6 @@
 https = require "https"
 http = require "http"
 util = require "util"
-
-tools = require "../nucleus/toolkit"
-extendz = require "../nucleus/extends"
-compose = require "../nucleus/compose"

 {EOL} = require "os"
 {format} = require "util"


segregate compression preprocess from actual reduction

--- library/membrane/visual.coffee
+++ library/membrane/visual.coffee
@@ -210,13 +210,21 @@
         context.styles = _.unique context.styles, u
         assert context.scripts = _.unique scripts, u
         assert _.isArray sources = _.unique sources
+        assert not _.isEmpty(sources), emptySources
         return context unless nconf.get compression
+        return @contextReduction context, sources
+
+    # This is the place where actual context sources minification
+    # and reduction takes place. This method invokes the UglifyJS2
+    # on the previously preprocessed sources and the re-emerges it
+    # in the context in place of the originals. This method also
+    # takes care of the compression cache, which is very important.
+    contextReduction: (context, sources) ->
         assert hasher = crypto.createHash "md5"
         joined = context.sources.join new String
         digest = hasher.update(joined).digest "hex"
         context.sources = c if c = @ccache?[digest]
         return context if context.sources and c
-        assert not _.isEmpty(sources), emptySources
         assert sources = _.reject sources, _.isEmpty
         assert minify = require("uglify-js").minify
         minified = minify sources, fromString: yes


Icons

Icons

- make left and right arrow thinner (angle instead of chevron)

--- src/base/Icon/Icon.coffee
+++ src/base/Icon/Icon.coffee
@@ -92,8 +92,8 @@
 		warning: "fa-warning"
 		legal: "fa-legal"
 		cloud: "fa-cloud"
-		left: "fa-chevron-left"
-		right: "fa-chevron-right"
+		left: "fa-angle-left"
+		right: "fa-angle-right"
 		dive: "fa-chevron-right"
 		zoom_in: "fa-search-plus"
 		zoom_out: "fa-search-minus"


Enable transaction

Enable transaction

--- lib/transient-emacs.coffee
+++ lib/transient-emacs.coffee
@@ -201,7 +201,8 @@

   kill_region: ->
     editor = @_push_regeon_to_killring()
-    s.delete() for s in editor.getSelections() when not s.isEmpty()
+    editor.transact =>
+      s.delete() for s in editor.getSelections() when not s.isEmpty()

   copy_region: ->
     editor = @_push_regeon_to_killring()
@@ -210,26 +211,29 @@
   kill_backward_word: ->
     @is_user_command = false
     editor = atom.workspace.getActiveTextEditor()
-    editor.selectToBeginningOfWord()
-    texts = (s.getText() for s in editor.getSelections())
-    @killring.put texts,false
-    s.delete() for s in editor.getSelections() when not s.isEmpty()
+    editor.transact =>
+      editor.selectToBeginningOfWord()
+      texts = (s.getText() for s in editor.getSelections())
+      @killring.put texts,false
+      s.delete() for s in editor.getSelections() when not s.isEmpty()
     @is_user_command = true

   kill: ->
     @is_user_command = false
     editor = atom.workspace.getActiveTextEditor()
-    editor.selectToEndOfLine()
-    texts = (s.getText() or '\n' for s in editor.getSelections())
-    @killring.put texts,true
-    editor.delete()
+    editor.transact =>
+      editor.selectToEndOfLine()
+      texts = (s.getText() or '\n' for s in editor.getSelections())
+      @killring.put texts,true
+      editor.delete()
     @is_user_command = true

   yank: ->
     editor = atom.workspace.getActiveTextEditor()
-    cursors = editor.getCursors()
-    top = @killring.top()
-    if cursors.length == top?.length
-      c.selection.insertText top[i] for c,i in cursors
-    else if top
-      c.selection.insertText top.join '\n' for c in cursors
+    editor.transact =>
+      cursors = editor.getCursors()
+      top = @killring.top()
+      if cursors.length == top?.length
+        c.selection.insertText top[i] for c,i in cursors
+      else if top
+        c.selection.insertText top.join '\n' for c in cursors


Refactor for command query separation

Refactor for command query separation

--- lib/transient-emacs.coffee
+++ lib/transient-emacs.coffee
@@ -11,7 +11,6 @@
     useLegacySearch:
       type: 'boolean'
       default: true
-
   killring: null
   commands: null
   event_listeners: []
@@ -209,22 +208,23 @@
     else
       @kill_region()

-  _push_regeon_to_killring: ->
+  _push_regeon_to_killring: (editor) ->
     editor = atom.workspace.getActiveTextEditor()
     editorView = atom.views.getView editor
     $(editorView).removeClass "transient-marked"
     texts = (s.getText() for s in editor.getSelections())
     @killring.push texts
     @killring.seal()
-    editor

   kill_region: ->
-    editor = @_push_regeon_to_killring()
+    editor = atom.workspace.getActiveTextEditor()
+    @_push_regeon_to_killring editor
     editor.transact ->
       s.delete() for s in editor.getSelections() when not s.isEmpty()

   copy_region: ->
-    editor = @_push_regeon_to_killring()
+    editor = atom.workspace.getActiveTextEditor()
+    @_push_regeon_to_killring editor
     cursor.clearSelection() for cursor in editor.getCursors()

   kill_backward_word: ->


Close #2409; Remove unused code

Close #2409; Remove unused code

--- server/lib/ldap.coffee
+++ server/lib/ldap.coffee
@@ -1,5 +0,0 @@
-Meteor.startup ->
-	if RocketChat.settings.get 'LDAP_Url'
-		LDAP_DEFAULTS.url = RocketChat.settings.get 'LDAP_Url'
-		LDAP_DEFAULTS.port = RocketChat.settings.get 'LDAP_Port' if RocketChat.settings.get 'LDAP_Port'
-		LDAP_DEFAULTS.dn = RocketChat.settings.get 'LDAP_DN' or false


transform to compose v2

transform to compose v2

--- lib/compose.coffee
+++ lib/compose.coffee
@@ -16,7 +16,7 @@
           environment: eth0_pipework_cmd: networkValue
           hostname: "#{serviceName}.#{subDomain}"
           dns_search: subDomain
-          net: 'none'
+          network_mode: 'none'
           labels: labels
           stop_signal: 'SIGKILL'
         if service.container_name
@@ -26,8 +26,9 @@
         # remove the hostname if set in the service, the hostname is set from
         # the network container
         delete service.hostname
-        service.net = "container:bb-net-#{serviceName}"
-      else service.net = "container:bb-net-#{service.labels['bigboat.service.name']}"
+        delete service.net
+        service.network_mode = "service:bb-net-#{serviceName}"
+      else service.network_mode = "service:bb-net-#{service.labels['bigboat.service.name']}"

     addVolumeMapping = (serviceName, service) ->
       bucketPath = path.join config.dataDir, config.domain, options.storageBucket if options.storageBucket
@@ -46,6 +47,11 @@
         else v
       delete service.volumes unless service.volumes

+    migrateLinksToDependsOn = (serviceName, service) ->
+      if service.links
+        service.depends_on = _.union (service.depends_on or []), service.links
+        delete service.links
+
     addDockerMapping = (serviceName, service) ->
       if service.labels['bigboat.container.map_docker'] is 'true'
         service.volumes = [] unless service.volumes
@@ -55,4 +61,7 @@
       addNetworkContainer serviceName, service
       addVolumeMapping serviceName, service
       addDockerMapping serviceName, service
-    doc
+      migrateLinksToDependsOn serviceName, service
+
+    version: '2'
+    services: doc


discussion markdown-css now closer to the actual element that uses it

--- client/landing/app/Applications/Activity.kdapplication/ContentDisplays/Discussion.coffee
+++ client/landing/app/Applications/Activity.kdapplication/ContentDisplays/Discussion.coffee
@@ -268,6 +268,8 @@

     @hightlightCode()

+    @$(".discussion-body .data").addClass "has-markdown"
+
   pistachio:->
     """
     {{> @header}}
@@ -291,7 +293,7 @@

             {{> @editDiscussionLink}}
             {{> @deleteDiscussionLink}}
-
{{@utils.expandUsernames(@utils.applyMarkdown(#(body)),"pre")}}

+
{{@utils.expandUsernames(@utils.applyMarkdown(#(body)),"pre")}}


FIX: HasProperties set should not modify inputs

--- bokehjs/src/coffee/common/has_properties.coffee
+++ bokehjs/src/coffee/common/has_properties.coffee
@@ -33,7 +33,7 @@
       super(attrs, options)

       #cheap memoization, requirejs doesn't seem to do it
-      @_base = false
+      @_base = false
       @properties = {}
       @property_cache = {}
       if not _.has(attrs, @idAttribute)
@@ -82,8 +82,10 @@
            @properties[key]['setter']
           @properties[key]['setter'].call(this, val)
           toremove.push(key)
-      for key in toremove
-        delete attrs[key]
+      if not _.isEmpty(toremove)
+        attrs = _.clone(attrs)
+        for key in toremove
+          delete attrs[key]
       if not _.isEmpty(attrs)
         super(attrs, options)

@@ -242,7 +244,7 @@
       if not @_base
         @_base = require('./base')
       return @_base
-
+
     url: () ->
       # ### method HasProperties::url
       #model where our API processes this model


render portal into container elements;

render portal into container elements;

--- resources/assets/coffee/react/beatmaps/search-panel.coffee
+++ resources/assets/coffee/react/beatmaps/search-panel.coffee
@@ -26,27 +26,40 @@
     @inputRef = React.createRef()
     @pinnedInputRef = React.createRef()

+    # containers for React to render portal into; turbolinks and React portals
+    # don't play well together, otherwise. These aren't needed without turbolinks.
+    @breadcrumbsPortal = document.createElement('div')
+    @contentPortal = document.createElement('div')
+
     @prevText = null
     @debouncedSubmit = _.debounce @submit, 500
+    @breadcrumbsElement = StickyHeader.breadcrumbsElement()
+    @contentElement = StickyHeader.contentElement()

   componentDidMount: =>
     $(document).on 'sticky-header:sticking.search-panel', @setHeaderPinned
-    $(document).on 'turbolinks:before-cache.beatmaps-search-cache', @turbolinksBeforeCache
+    $(document).on 'turbolinks:before-cache.search-panel', () =>
+      # componentWillUnmount is called too late for Beatmaps.
+      @breadcrumbsElement.removeChild @breadcrumbsPortal
+      @contentElement.removeChild @contentPortal
+
+    @breadcrumbsElement.appendChild @breadcrumbsPortal
+    @contentElement.appendChild @contentPortal

   componentWillUnmount: =>
     $(document).off '.search-panel'
-    @turbolinksBeforeCache()
+    @debouncedSubmit.cancel()

   render: =>
     div null,
       if StickyHeader.breadcrumbsElement()?
-        ReactDOM.createPortal @renderBreadcrumbs(), StickyHeader.breadcrumbsElement()
+        ReactDOM.createPortal @renderBreadcrumbs(), @breadcrumbsPortal

       if StickyHeader.contentElement()?
-        ReactDOM.createPortal @renderStickyContent(), StickyHeader.contentElement()
+        ReactDOM.createPortal @renderStickyContent(), @contentPortal

       div
         className: 'osu-page osu-page--beatmapsets-search-header'
@@ -205,11 +218,6 @@
           options: filters.played

-  turbolinksBeforeCache: =>
-    $(document).off '.beatmaps-search-cache'
-    @debouncedSubmit.cancel()
-
-
   setHeaderPinned: (_event, pinned) =>
     if pinned && document.activeElement == @inputRef.current
       @pinnedInputRef.current.focus()


Save target element and offset so we can later reposition on command

--- app/js/jail_iframe/classes/icon_buttons.coffee
+++ app/js/jail_iframe/classes/icon_buttons.coffee
@@ -1,5 +1,7 @@
 class IconButton
   constructor: (options) ->
+    @_targetElement = options.targetElement
+    @_targetOffset = options.targetOffset
     @$el = $ """
       <factlink-icon-button>
         <factlink-icon-button-bubble>
@@ -18,13 +20,16 @@
       mouseleave: options.onmouseleave
     @$el.on 'click', options.onclick

-    @_tether = new Tether
-      element: @$el[0]
-      target: options.targetElement
-      attachment: 'top left'
-      targetAttachment: 'top right'
-      classPrefix: 'factlink-tether'
-      targetOffset: options.targetOffset || '0 0'
+
+    @_tether = new Tether(@_tether_options())
+
+  _tether_options: () ->
+    element: @$el[0]
+    target: @_targetElement
+    attachment: 'top left'
+    targetAttachment: 'top right'
+    classPrefix: 'factlink-tether'
+    targetOffset: @_targetOffset || '0 0'

   destroy: ->
     @_tether.destroy()


Prevent play notify sound when another one is playing

--- views/env.coffee
+++ views/env.coffee
@@ -57,6 +57,7 @@
 NOTIFY_NOTIFICATION_API = true
 if process.platform == 'win32' and semver.lt(os.release(), '6.2.0')
   NOTIFY_NOTIFICATION_API = false
+playing = false
 window.notify = (msg, options) ->
   # Notification config
   enabled = config.get('poi.notify.enabled', true)
@@ -103,10 +104,13 @@
           content: msg
         appIcon.on 'balloon-click', remote.getGlobal('mainWindow').focus

-  if volume > 0.0001
+  if volume > 0.0001 && !playing
+    playing = true
     sound = new Audio(audio)
     sound.volume = volume
     sound.play()
+    sound.onended = (e) ->
+      playing = false

 modals = []
 window.modalLocked = false


Bugfix: .text getter bug

Bugfix: .text getter bug

--- coffee/src/views/summary_view.coffee
+++ coffee/src/views/summary_view.coffee
@@ -62,7 +62,12 @@
     icon = $('<i>').addClass @model.module.icon
     icon.tooltip title: @model.module.description, placement: 'right'

-    @$('.title').first().text(@model.title()).append icon
+    # must be split, as `.text` will return '', not the selector if
+    # @model.title() returns falsy. (will be taken as a getter, not setter)
+    title = @$('.title').first()
+    title.text(@model.title())
+    title.append(icon)
+
     @$('.description').first().text @model.description()
     @$('.thumbnail-view img').first().attr 'src', @model.thumbnail()
     @


[TERMINAL] Auto-scroll (WIP)

[TERMINAL] Auto-scroll (WIP)

--- src/coffee/terminal.coffee
+++ src/coffee/terminal.coffee
@@ -4,6 +4,7 @@
   #
   # terminalScreen [jQuery] jQuery element for the terminal-screen div
   constructor: (terminalScreen) ->
+    @container = terminalScreen
     @interpreter = new CommandInterpreter

     @output = []  # [String[]] output lines, including commands entered by the player
@@ -69,6 +70,7 @@
       syntaxTree = @interpreter.parse commandLine
     catch error
       @print error.message
+      @scrollToBottom()
       return

     try
@@ -78,13 +80,18 @@
     catch error
       @print error.message

-  # Send a text to the terminal output, on one line
+    @scrollToBottom()
+
+# Send a text to the terminal output, on one line
   #
   # lines [String...]
   print: (lines...) =>
     for line in lines
       @outputDiv.append(document.createTextNode(line)).append '<br>'

+  scrollToBottom: =>
+    console.log "scroll #{@container[0].scrollHeight}"
+    @container.animate scrollTop: @container[0].scrollHeight, 200, "swing"

 # Class responsible for syntax analysis (parsing) and execution
 # of the command-lines in the terminal


refactor Attribute

refactor Attribute

--- src/attribute.coffee
+++ src/attribute.coffee
@@ -1,24 +1,19 @@
 {Node} = require './node'

 class Attribute extends Node
-  constructor: (options) ->
-    super options
-    @type = 'attribute'
-    @name = options.name
-    @value = options.value
+  constructor: ({ level, @name, @value }) ->
+    super { level }

   @parse: (s) ->
     { level, node } = Node.parseBasic s
-    m = node.match(/^@(\S+)\s+(.+)$/)
-    if m
-      new Attribute level: level, name: m[1], value: m[2]
+    m = node.match /^@(\S+)\s+(.+)$/
+    if m?
+      new Attribute { level, name: m[1], value: m[2] }

   append: (prev) ->
-    if @level > prev.level
-      prev.setAttribute @name, @value
-      prev
-    else
-      throw new Error()
+    throw new Error() unless @level > prev.level
+    prev.setAttribute @name, @value
+    prev

   write: ->
     throw new Error()


TAP5-2202: t5/core/dom:ElementWrapper.attribute() should remove an element when setting its value to null

--- tapestry-core/src/main/preprocessed-coffeescript/org/apache/tapestry5/t5-core-dom.coffee
+++ tapestry-core/src/main/preprocessed-coffeescript/org/apache/tapestry5/t5-core-dom.coffee
@@ -283,18 +283,18 @@
     attribute: (name, value) ->

       if _.isObject name
-#if jquery
-        @$.attr name
-#elseif prototype
         for attributeName, value of name
           @attribute attributeName, value
-#endif
+
         return this

 #if jquery
       current = @$.attr name
       if arguments.length > 1
-        @$.attr name, value
+        if value is null
+          @$.removeAttr name
+        else
+          @$.attr name, value
       if _.isUndefined current
         current = null
 #elseif prototype


Update colors and speed of sunrise

Update colors and speed of sunrise


Add a getLength attribute for editors

Add a getLength attribute for editors

--- lib/editors.coffee
+++ lib/editors.coffee
@@ -34,6 +34,9 @@
   # @param delta [Delta] the delta to propagate to the editor
   # @see https://github.com/ottypes/rich-text
   updateContents: (delta) -> throw new Error "Implement me"
+
+  # Return the length of the text
+  getLength:() -> throw new Error "Implement me"

 class QuillJs extends Editor
   constructor: (@editor) ->
@@ -80,6 +83,9 @@
   updateContents: (delta)->
     @editor.updateContents delta

+  getLength:() ->
+    @editor.getLength()
+
 class TestEditor extends Editor
   constructor: (@editor) ->
     super


Fix typo

Fix typo

--- components/UrlToTempFile-node.coffee
+++ components/UrlToTempFile-node.coffee
@@ -65,7 +65,7 @@
         error.url = url
       req.on 'error', (err) ->
         error = new Error "Error in UrlToTempFile component. Request returned error for #{url}."
-        err.url = url
+        error.url = url
       req.on 'end', ->
         if error
           tmpFile.unlink()


Use lowercase def

Use lowercase def

--- src/benchmark.coffee
+++ src/benchmark.coffee
@@ -14,13 +14,13 @@
 code = """
   one = λs.λz.s z
   true = λt.λf.t
-  FALSE = λt.λf.f
+  false = λt.λf.f
   and = λp.λq.p q p
   if = λp.λq.λr.p q r
   pred = λn.λs.λz.n (λf.λg.g (f s)) (λx.z) (λx.x)
   sub = λm.λn.n pred m
   mult = λm.λn.λs.m (n s)
-  zero? = λn.n (λx.FALSE) true
+  zero? = λn.n (λx.false) true
   eq = λm.λn.and (zero? (sub m n)) (zero? (sub n m))
   Y = λf.(λx.f (x x)) (λx.f (x x))
   fact = Y λr.λn.if (zero? n) one (mult n (r (pred n)))


remove console logs

remove console logs

--- components/follow_button/view.coffee
+++ components/follow_button/view.coffee
@@ -30,7 +30,6 @@

     if @isLoggedIn
       if @collection.isFollowing @followId
-        console.log('unfollow')
         @collection.unfollow @followId
         analyticsHooks.trigger 'followable:unfollowed', {
           entity_id: @_id,
@@ -41,7 +40,6 @@
         }
       else
         @collection.follow @followId
-        console.log('follow')
         analyticsHooks.trigger 'followable:followed', {
           entity_id: @_id,
           entity_slug: @followId,


Only modify paste when it matches wholely. Fix #240

--- lib/formats.coffee
+++ lib/formats.coffee
@@ -300,23 +300,23 @@
 @coauthorLinkRe = "coauthor:#{coauthorLinkBodyRe}"

 @parseCoauthorMessageUrl = (url) ->
-  match = new RegExp urlFor 'message',
+  match = new RegExp("^#{urlFor 'message',
     group: '(.*)'
     message: '(.*)'
     0: '*'
     1: '*'
-  .exec url
+  }$").exec url
   if match?
     group: match[1]
     message: match[2]

 @parseCoauthorAuthorUrl = (url) ->
-  match = new RegExp urlFor 'author',
+  match = new RegExp("^#{urlFor 'author',
     group: '(.*)'
     author: '(.*)'
     0: '*'
     1: '*'
-  .exec url
+  }$").exec url
   if match?
     group: match[1]
     author: match[2]


Fix \def support

Fix \def support

--- lib/formats.coffee
+++ lib/formats.coffee
@@ -54,12 +54,12 @@
 latex2html = (tex) ->
   defs = {}
   tex = tex.replace /%.*$\n?/mg, ''
-  tex = tex.replace /\\def\s*\\([a-zA-Z]+)\s*{((?:[^{}]|{[^{}]*})*)}/g, (match, p1, p2) ->
+  tex = tex.replace /\\def\s*\\([a-zA-Z]+)\s*{((?:[^{}]|{(?:[^{}]|{[^{}]*})*})*)}/g, (match, p1, p2) ->
     defs[p1] = p2
     ''
   for def, val of defs
-    console.log def, val
-    tex = tex.replace new RegExp("\\\\#{def}\\s*", 'g'), val
+    #console.log "\\#{def} = #{val}"
+    tex = tex.replace ///\\#{def}\s*///g, val
   tex = '
' + tex
   .replace /\\\\/g, '[DOUBLEBACKSLASH]'
   .replace /\\(BY|YEAR)\s*{([^{}]*)}/g, '$2'
@@ -189,9 +189,7 @@
   [text, math]

 postprocessKaTeX = (text, math) ->
-  #replaceMathBlocks text, (block) ->
-  text.replace /MATH(\d+)ENDMATH/g, (match, p1) ->
-    block = math[p1]
+  replacer = (block) ->
     start$ = /^\$+/.exec block
     end$ = /\$+$/.exec block
     display = start$[0].length >= 2
@@ -218,6 +216,11 @@
       .replace /</g, '<'
       .replace />/g, '>'
       "#{latex}"
+  if math?
+    text.replace /MATH(\d+)ENDMATH/g, (match, p1) ->
+      replacer math[p1]
+  else
+    replaceMathBlocks text, replacer

 jsdiff = require 'diff'

@@ -242,7 +245,7 @@
   sanitized

 @formatBody = (format, body, leaveTeX = false) ->
-  [body, math] = preprocessKaTeX body unless leaveTeX
+  [body, math] = preprocessKaTeX body unless leaveTeX or format == 'latex'
   if format of formats
     body = formats[format] body, false
   else


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/controllable.coffee
+++ src/controllable.coffee
@@ -21,7 +21,8 @@
   player.I.x
   # => 13

-  # We keep track of the dire
+  # We keep track of the direction the object is
+
 </pre></code>

 @name Controllable


Adds docs to bootstrap.coffee

Adds docs to bootstrap.coffee

--- src/bootstrap.coffee
+++ src/bootstrap.coffee
@@ -1,3 +1,18 @@
+# This file is responsible for defining window.Hull
+# and providing pooled methods to the user while
+# Hull is actually loading.
+#
+# It provides some global code that is to be executed immediately
+# and an AMD module,
+# The AMD module loads a particvular flavour of Hull, defined as a set of 3 methods:
+# * condition: Returns a promise or a value that indicates the success or failure of the loading process
+# * success: will to be executed if the app is loaded correctly
+# * failure: will be executed if the app fails to load
+#
+# The global code only defines some methods into window.Hull and pools the parameters
+# to replay them if the app has loaded correctly
+#
+
 _setup = null
 _extend = null
 currentFlavour = null


Fix touch coordinates

Fix touch coordinates

--- src/core/bindEvents.coffee
+++ src/core/bindEvents.coffee
@@ -1,6 +1,6 @@
 coordsForTouchEvent = (el, e) ->
-  tx = e.changedTouches[0].pageX
-  ty = e.changedTouches[0].pageY
+  tx = e.changedTouches[0].clientX
+  ty = e.changedTouches[0].clientY
   p = el.getBoundingClientRect()
   return [tx - p.left, ty - p.top]


Client: Activity: Updated loadPopularChannels channel action to fix performance problems

--- client/activity/lib/flux/actions/channel.coffee
+++ client/activity/lib/flux/actions/channel.coffee
@@ -233,21 +233,12 @@

   dispatch LOAD_POPULAR_CHANNELS_BEGIN

-  new Promise (resolve, reject) ->
-
-    kd.singletons.socialapi.channel.fetchPopularTopics options, (err, channels) ->
-      if err
-        dispatch LOAD_POPULAR_CHANNELS_FAIL, { err }
-        reject { err }
-        return
-
-      promises = channels.map (channel) ->
-        loadChannel channel.id
-
-      Promise.all(promises).then (channels) ->
-        channels = channels.map (item) -> item.channel
-        dispatch LOAD_POPULAR_CHANNELS_SUCCESS, { channels }
-        resolve { channels }
+  kd.singletons.socialapi.channel.fetchPopularTopics options, (err, channels) ->
+    if err
+      dispatch LOAD_POPULAR_CHANNELS_FAIL, { err }
+      return
+
+    dispatch LOAD_POPULAR_CHANNELS_SUCCESS, { channels }

 ###*


fix: fallback to generate variant Id logic

--- src/coffee/mapping.coffee
+++ src/coffee/mapping.coffee
@@ -24,9 +24,9 @@
     rowIndex = raw.startRow

     product = @mapBaseProduct raw.master, productType, rowIndex
-    product.masterVariant = @mapVariant raw.master, Number(raw.master[@header.toIndex CONS.HEADER_VARIANT_ID]), productType, rowIndex, product
+    product.masterVariant = @mapVariant raw.master, Number(raw.master[@header.toIndex CONS.HEADER_VARIANT_ID]) || 1, productType, rowIndex, product
     _.each raw.variants, (entry, index) =>
-      product.variants.push @mapVariant entry.variant, Number(entry.variant[@header.toIndex CONS.HEADER_VARIANT_ID]), productType, entry.rowIndex, product
+      product.variants.push @mapVariant entry.variant, Number(entry.variant[@header.toIndex CONS.HEADER_VARIANT_ID]) || index + 2, productType, entry.rowIndex, product

     data =
       product: product


typo(counts): Fix duration computation

typo(counts): Fix duration computation

--- src/flux/stores/thread-counts-store.coffee
+++ src/flux/stores/thread-counts-store.coffee
@@ -84,7 +84,7 @@
       DatabaseStore._query(SetCountsQuery())
     .then =>
       NylasEnv.config.set('nylas.threadCountsValid', true)
-      console.log("Recomputed all thread counts in #{countsStartTime}s")
+      console.log("Recomputed all thread counts in #{Date.now() - countsStartTime}ms")

   _onCountsChanged: =>
     DatabaseStore._query(ReadCountsQuery()).then (results) =>


Если во время долгой загрузки изображения текущее изображение изменилось, загруженное изображение уже не показывается. Вынес в отдельный метод изменения в миниатюрах.

--- src/chocolate.coffee
+++ src/chocolate.coffee
@@ -11,9 +11,10 @@
 #   test.mozRequestFullScreen()

 class Chocolate
-  images:  {}
+  images: {}
+  length: 0
+
   current: null
-  length:  0

   ###
    Конструктор
@@ -43,7 +44,7 @@
       $(window).bind 'popstate', (event) =>
         cid = @getImageFromUri()
         if cid > 0 and cid isnt @current
-          if @current is null then @show cid else @updateImage cid, false
+          if @current is null then @show cid else @open cid, false

     $(window).bind 'keyup', (event) =>
       if @overlay.hasClass 'show'
@@ -132,7 +133,8 @@

     @createThumbnails() if @options.thumbnails
     @overlay.addClass 'show'
-    @updateImage cid
+
+    @open cid
     @

   close: ->
@@ -144,6 +146,10 @@
       @overlay.removeClass 'show'
     @

+  open: (cid) ->
+    @updateImage cid if @images[cid]?
+    @
+
   next: ->
     next = @current + 1

@@ -169,26 +175,13 @@
     @current = cid

     @container.removeClass 'show'
-
-    if @options.thumbnails
-      @thumbnails.find('.selected').removeClass 'selected'
-      thumbnail = @thumbnails.find('[data-cid=' + cid + ']').addClass('selected')
-
-      width = thumbnail.outerWidth() + toInt thumbnail.css('margin-right')
-      left  = thumbnail.offset().left
-
-      if @thumbnails.width() < width + left
-        offset = @thumbnails.scrollLeft() + width
-        offset = width + left if @thumbnails.width() + offset < width + left
-        @thumbnails.scrollLeft offset
-      else if @thumbnails.scrollLeft() > left
-        offset = if left < width then 0 else @thumbnails.scrollLeft() - width
-        @thumbnails.scrollLeft offset

     if isHistory and updateHistory
       title = if @images[cid].title then 'Image: ' + @images[cid].title else null
       history.pushState null, title, '#image' + cid

+    @updateThumbnails() if @options.thumbnails
+
     @getImageSize cid, (cid) ->
       @container.addClass 'show'

@@ -201,18 +194,40 @@
     @

   ###
+   Обновление миниатюр
+  ###
+  updateThumbnails: ->
+    @thumbnails.find('.selected').removeClass 'selected'
+
+    thumbnail = @thumbnails.find('[data-cid=' + @current + ']').addClass 'selected'
+
+    width = thumbnail.outerWidth() + toInt thumbnail.css('margin-right')
+    left  = thumbnail.offset().left
+
+    if @thumbnails.width() < width + left
+      offset = @thumbnails.scrollLeft() + width
+      offset = width + left if @thumbnails.width() + offset < width + left
+
+    else if @thumbnails.scrollLeft() > left
+      offset = if left < width then 0 else @thumbnails.scrollLeft() - width
+
+    @thumbnails.scrollLeft offset if offset > 0
+    @
+
+  ###
    Обновление размеров блока с главным изображением
   ###
   getImageSize: (cid, after = ->) ->
     image = @images[cid]

+    fn = => after.call @, cid if cid is @current
+
     if not image.width or not image.height
       @spinner.removeClass 'hide'

-      element     = new Image()
-      element.src = image.source
-
-      element.onload = (event) =>
+      element        = new Image()
+      element.src    = image.source
+      element.onload = =>
         @spinner.addClass 'hide'

         @images[cid].width  = element.width
@@ -220,9 +235,9 @@

         delete element

-        after.call @, cid
+        fn()
     else
-      after.call @, cid
+      fn()

   ###
    Обновление размеров блока с главным изображением
@@ -232,6 +247,7 @@

     thumbnails = if not @options.thumbnails or @thumbnails.hasClass('hide') then 0 else @thumbnails.height()

+    # TODO оптимизация
     horizontal = toInt(@overlay.css('padding-left')) + toInt(@overlay.css('padding-right'))
     vertical   = toInt(@overlay.css('padding-top')) + toInt(@overlay.css('padding-bottom'))

@@ -306,7 +322,7 @@
                          .replace('{{title}}', if image.title then ' title="' + image.title + '"' else '')

     @thumbnails.html(content).find('.choco-thumbnail').click (event) ->
-      _this.updateImage toInt $(@).attr('data-cid')
+      _this.open toInt $(@).attr('data-cid')

     @overlay.find('.choco-thumbnails-toggle').click (event) ->
       current = _this.images[_this.current]


Set up ctl-e, ctl-a

Set up ctl-e, ctl-a

--- keymap.cson
+++ keymap.cson
@@ -1,2 +1,7 @@
-'body':
-  'ctrl-s': 'core:save'
+'.platform-win32 atom-text-editor, .platform-linux atom-text-editor':
+  'ctrl-e': 'editor:move-to-end-of-screen-line'
+  'ctrl-shift-e': 'find-and-replace:use-selection-as-find-pattern'  # was ctrl-e
+
+'atom-workspace atom-text-editor':
+  'ctrl-shift-a': 'core:select-all'  # was ctrl-a
+  'ctrl-a': 'editor:move-to-first-character-of-line'


Add autoindent to keybindings

Add autoindent to keybindings

--- keymap.cson
+++ keymap.cson
@@ -61,6 +61,7 @@
   'ctrl-o "': 'expand-region:select-around-double-quotes'
   'ctrl-o `': 'expand-region:select-around-back-ticks'
   'ctrl-o t': 'expand-region:select-around-tags'
+  'ctrl-=': 'editor:auto-indent'

   # block-travel
   'alt-p': 'block-travel:move-up'


Show tag value rather than name in tag list

The tag name (given in the JSON structure for the tagger) would be more
descriptive of course, but when we construct the multiword search view
from a CQP expression only the attribute value is available. We might
consider looking up the name in the JSON structure based on the
attribute name and the value.

--- app/assets/javascripts/rglossa/components/cwb-multiword-term.coffee
+++ app/assets/javascripts/rglossa/components/cwb-multiword-term.coffee
@@ -10,9 +10,6 @@
         promptText: ' '
         afterDeletingTag: $.proxy(@_onTagRemoved, @)
       )
-      pos = @get('term.pos.name')
-      @_tagsInput.addTag(pos) if pos
-
     @_tagsInput
   ).property()

@@ -24,7 +21,7 @@

   addPos: (pos) ->
     @set('term.pos', pos)
-    @get('tagsInput').addTag(pos.name)
+    @get('tagsInput').addTag(pos.value)

   addFeature: (option, feature, pos) ->
     @get('term.features').pushObject


forgot to save. Derp.

forgot to save. Derp.

--- app/assets/javascripts/web/controllers/message_controller.js.coffee
+++ app/assets/javascripts/web/controllers/message_controller.js.coffee
@@ -20,7 +20,7 @@
       [ /"/g, """ ],
       [ /\\\\/g, "\" ],
       [ /\s(?=\s)/g, ' '],
-      [ /(\\r\\n|\\n|\\r)/g, '
'],
+      [ /(\\r\\n|\\n|\\r)/g, '
'],
       [
         # Green Text
         /((^>|
>)(.(?!(
|$)))+.)/ig,


Add basic features

Add basic features

--- finnishwordbasehelper.coffee
+++ finnishwordbasehelper.coffee
@@ -2,24 +2,44 @@

 contents = fs.readFileSync('words/kotus/kotus_sanat.txt', 'utf8')
 words = contents.split("\n").map((x) => x.trim())
-board = """
-TEANTAEKÄJ
-AKRAUTROKI
-LOPHTÖNUHN
-OIJIATOÖEY
-EASPNAHPKÄ
-RTEOVNTHSR
-PIOKTIÄYIN
-AEITRURLYU
-JNLIAEOIUV
-NOAVTNEKSJ
-UTISÄTILAU
-YALMIKGPUT
-POKURUÄNEK
+
+
+boardString = """
+SATVSÄTIAS
+SJAOÄMAPUR
+RYPRTEKOTA
+RAKUKEASEA
+AUNIAMUAMU
+JEKOPRENUO
+VHASOUSKÄR
+OTIKUATYTA
+TLENADGLAD
+EPUEÄNAKIN
+ÄNAÄIRERVI
+IONUKOÄNOA
+AOAEAKUEIN
 """
-.split("\n")
-.map((x) => x.trim())
-.filter((x) => x.length == 10)
+positionString = """
+0000000000
+...000.00.
+...000.00.
+...000.00.
+...000.00.
+1........0
+.1........
+.1........
+.1........
+.1........
+.1........
+.1........
+1111111111
+"""
+
+boardStringToArray = (boardString) ->
+    boardString
+    .split("\n")
+    .map((x) => x.trim())
+    .filter((x) => x.length == 10)

 filterWordsByBoard = (words, board) ->
     getCharacterCount = (word) =>
@@ -52,25 +72,106 @@
     MATCH_PREFIX = 1
     MATCH_NONE = 0

-    constructor: (@words) ->
+    constructor: (words) ->
+        @tree = {}
+        for word in words
+            @add(word)
+    add: (word) ->
+        subtree = @tree
+        for c in word
+            #console.log "character", c, "Tree:", @tree, "Subtree:", subtree
+            unless subtree[c]?
+                subtree[c] = {}
+            subtree = subtree[c]
+        subtree['$'] = true
     findMatch: (prefix) ->
         prefix = prefix.toLowerCase()
-        for word in @words
-            if word == prefix
-                return MATCH_EXACT
-            if word[...prefix.length] == prefix
-                return MATCH_PREFIX
-        return MATCH_NONE
+        subtree = @tree
+        for c in prefix
+            unless subtree[c]?
+                return MATCH_NONE
+            subtree = subtree[c]
+        if subtree['$'] == true
+            return MATCH_EXACT
+        else
+            return MATCH_PREFIX

 class BoardSolver
-    constructor: (@board) ->
+    constructor: (@board, @positionBoard, player) ->
+        throw "Player should be 0 or 1" unless player == 0 or player == 1
+        @player = ''+player
+        throw "Board should be defined" unless @board
+        throw "PositionBoard should be defined" unless @positionBoard
+        unless @board.length == @positionBoard.length
+            throw "Board (#{board.length}) and positionBoard (#{positionBoard.length}) should be same length"
+        throw "Board and positionBoard should be same length" unless @board[0].length == @positionBoard[0].length
         @height = @board.length
         @width = @board[0].length
+    opposite: (player) ->
+        if player == '0'
+            player = '1'
+        else if player == '1'
+            player = '0'
+        else
+            throw "Invalid player #{player}"
+    isPlayer: (c) ->
+        c == '0' or c == '1'
     get: (position) ->
         @board[position[1]][position[0]]
+    getPosition: (position) ->
+        @positionBoard[position[1]][position[0]]
+    getPlayerPositions: (player) ->
+        positions = []
+        for x in [0..@width-1]
+            for y in [0..@height-1]
+                if @getPosition([x, y]) == player
+                    positions.push([x, y])
+        positions
+    printPositions: (positions) ->
+        getCharacter = (pos) =>
+            i = 0
+            for position in positions
+                if position[0] == pos[0] and position[1] == pos[1]
+                    return @get(pos)
+                    #else
+                    #    return '@'
+                i += 1
+            return '.'
+        console.log "Board:"
+        console.log positions
+        for y in [0..@height-1]
+            buf = '  '
+            for x in [0..@width-1]
+                buf += getCharacter([x, y])
+            console.log buf
+    evaluate: (positions) ->
+        taken = (position) =>
+            for takenPosition in position
+                if takenPosition[0] == position[0] and takenPosition[1] == position[1]
+                    return true
+            return false
+        score = 0
+        #max_score = 0
+        for position in positions
+            if @isPlayer(@getPosition(position))
+                if @getPosition(position) == @opposite(@player)
+                    factor = 10
+                else
+                    factor = 0
+            else
+                factor = 1
+            if @player == '0'
+                #max_score = Math.max(max_score, position[1])
+                score += position[1]**2 * factor
+            else
+                #max_score = Math.max(max_score, @height - position[1])
+                score += (@height - position[1])**2 * factor
+        #score += max_score**4
+
+        return score
     solve: (tri) ->
-        startingPositions = ([x, @height-1] for x in [0..@width-1])
+        startingPositions = ([x, 0] for x in [0..@width-1])
         findNeighbors = (position) =>
             xys = []
             xys.push [position[0]+i, position[1]+j] for i in [-1..1] for j in [-1..1] when not (i == 0 and j == 0)
@@ -96,8 +197,8 @@
             else
                 #console.log "Testing ", word, positions
                 if match == 2
-                    foundWords.push([word, positions])
-                    console.log "Found word", word, positions
+                    foundWords.push([word, positions, @evaluate(positions)])
+                    #console.log "Found word", word, positions
                 neighbors = findNeighbors(lastPosition)
                 #console.log "neighbors", neighbors
                 potentialPositions = filterVisited(neighbors, positions)
@@ -107,27 +208,39 @@
                     newPositions.push(potentialPosition)
                     recursiveSolve(word, newPositions)

-        console.log "Solving..."
-        for pos in startingPositions
-            console.log "Checking out this ",pos," where", @get(pos)
+        for pos in @getPlayerPositions(@player)
+            #console.log "Checking out this ",pos," where", @get(pos)
             recursiveSolve('', [pos])

-
-
-
+        foundWords.sort (a, b) => b[2] - a[2]
+        foundWords.filter (row) =>
+            for row2 in foundWords
+                if row != row2 and row[0] == row2[0]
+                    return false
+            return true
+
+        foundWords
+
+board = boardStringToArray(boardString)
+positionBoard = boardStringToArray(positionString)

 console.log "Game Board:"
 console.log board
-console.log board.length
-console.log board[12]
-
-
-solver = new BoardSolver(board)
-console.log "Words", words.length
-words = filterWordsByBoard(words, solver)
-console.log "Words (after)", words.length
+console.log "Position Board:"
+console.log positionBoard
+
+solver = new BoardSolver(board, positionBoard, 0)
+
+console.log "Dictionary", words.length
+#words = filterWordsByBoard(words, solver)
+console.log "Dictionary (after filtering)", words.length
+
+time_start = Date.now()
 tri = new Tri(words)
-
-
-
-solver.solve(tri)+console.log "Building tri-tree took ",(Date.now() - time_start)/1000," seconds"
+
+foundWords = solver.solve(tri)
+
+for i in [0..2]
+    console.log "Solution #{i} is #{foundWords[i][0]} at #{foundWords[i][1][0]}, #{foundWords[i][1][1]}"
+    solver.printPositions(foundWords[i][1])


components-gtk: print error in case the UI files cannot be loaded

--- components-gtk/GladeConstructorComponent.coffee
+++ components-gtk/GladeConstructorComponent.coffee
@@ -24,6 +24,7 @@
           object = builder.get_object(objectName)
           objects[objectName] = object if object
     catch e
+      log "Cannot load #{file.get_path()} : #{e.message}"
       error = e

     c.description = "Widgets from #{file.get_basename()}"


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine_selector.coffee
+++ src/engine_selector.coffee
@@ -59,6 +59,8 @@
   @param {String} selector
   @returns {Array} An array of the objects found
   ###
+  each
+
   find: (selector) ->
     results = []


Disable rotate threshold

Disable rotate threshold

--- framer/LayerRotatable.coffee
+++ framer/LayerRotatable.coffee
@@ -9,7 +9,7 @@
 class exports.LayerRotatable extends BaseClass

 	@define "enabled", @simpleProperty("enabled", true)
-	@define "threshold", @simpleProperty("threshold", 64)
+	@define "threshold", @simpleProperty("threshold", 0)

 	constructor: (@layer) ->
 		super
@@ -24,16 +24,16 @@

 	_rotate: (event) =>

-		# return unless event.pointers.length is 2
+		return unless event.pointers.length is 2

-		# pointA =
-		# 	x: event.pointers[0].pageX
-		# 	y: event.pointers[0].pageY
-		# pointB =
-		# 	x: event.pointers[1].pageX
-		# 	y: event.pointers[1].pageY
+		pointA =
+			x: event.pointers[0].pageX
+			y: event.pointers[0].pageY
+		pointB =
+			x: event.pointers[1].pageX
+			y: event.pointers[1].pageY

-		# return unless Utils.pointTotal(Utils.pointAbs(Utils.pointSubtract(pointA, pointB))) > @threshold
+		return unless Utils.pointTotal(Utils.pointAbs(Utils.pointSubtract(pointA, pointB))) > @threshold

 		@_rotationStart ?= @layer.rotation
 		@_rotationOffset ?= event.rotation


fixed logged out when looking at someone else's images

--- src/lib/controllers/user_controller.coffee
+++ src/lib/controllers/user_controller.coffee
@@ -85,7 +85,6 @@

     if my_user_id == parseInt(user_id)
       console.log "this is happening!"
-      logged_in_user = user
       title = 'My Images'
       your_or_their = "Your"
     else
@@ -94,7 +93,7 @@

     res.render 'uploaded', {
       title
-      user: logged_in_user
+      user: req.user
       images
       your_or_their
     }


better behavior when going outside of length bounds

--- graphs.coffee
+++ graphs.coffee
@@ -79,6 +79,7 @@
     @height = 200
     @pxWidth = 800
     @maxLength = @pxWidth / 2
+    @minLength = 3
     @stepLength = 50
     @animationQueue = []
     @working = false
@@ -97,7 +98,7 @@
   setLength: (length) =>
     if @working
       return
-    @length = Math.max 2, Math.min @maxLength, length
+    @length = Math.max @minLength, Math.min @maxLength, length
     @values =  ( value * @height / @length for value in [1..@length] )
     @barWidth = 1
     while @pxWidth / @barWidth / 2 > @length
@@ -328,6 +329,7 @@
   len = $("#js-length").val()
   if isFinite len
     VA.setLength +len
+  $("#js-length").val VA.length

   state = $("#js-state").val()
   if state == "random"


Fix to get Windows udpater to run correctly

--- lib/proscli.coffee
+++ lib/proscli.coffee
@@ -131,7 +131,7 @@
             detail: 'You will need to upgrade PROS CLI for your intallation method.'
       else
         cmd = o.split('\n').filter(Boolean).map Function.prototype.call, String.prototype.trim
-        if navigator.platform == "Win32" and cmd.length == 3  # fix for calling upgrader on Windows
+        if navigator.platform == "Win32" and cmd[0].endsWith "updater.exe"  # fix for calling upgrader on Windows
           cmd[1] = "/checknow"
           cmd[2] = "/reducedgui"
         if not atom.inDevMode()


make clear actually work

make clear actually work

--- lib/results.coffee
+++ lib/results.coffee
@@ -1,14 +1,20 @@
 # TODO: esc key to remove current result
 # In general this is weird and needs a refactor

+{CompositeDisposable} = require 'atom'
 tree = require './tree'

 module.exports =

   activate: ->
-    @subs = atom.commands.add 'atom-text-editor:not([mini])',
+    @subs = new CompositeDisposable()
+
+    @subs.add atom.commands.add 'atom-text-editor:not([mini])',
       'inline-results:clear-current': (e) => @removeCurrent e
       'inline-results:clear-all': => @removeAll()
+
+    @subs.add atom.commands.add '.ink.inline',
+      'inline-results:clear': (e) => e.currentTarget.result.destroy()

   deactivate: ->
     @subs.dispose()
@@ -26,6 +32,7 @@
     view: view

   methods: (r) ->
+    r.view.result = r
     r.destroy = => @remove r
     r.invalidate = => @invalidate r
     r.validate = => @validate r


Remove unused requires

Remove unused requires

--- lib/tweezer.coffee
+++ lib/tweezer.coffee
@@ -1,6 +1,3 @@
-http = require 'http'
-querystring = require 'querystring'
-
 twitter = require 'ntwitter'
 facebook = require 'fbgraph'


Add description for config settings

Add description for config settings

--- lib/welcome.coffee
+++ lib/welcome.coffee
@@ -19,6 +19,7 @@
     showOnStartup:
       type: 'boolean'
       default: true
+      description: 'Show the Welcome package next time a new window is created. This config setting is automatically set to `false` after a new window is created and the Welcome package is shown.'

   activate: ->
     @subscriptions = new CompositeDisposable


Cleanup labels

Cleanup labels

--- src/config/sprites.coffee
+++ src/config/sprites.coffee
@@ -6,7 +6,7 @@
   _LABEL: "sprites"
   NewFromTurtle: (world, args) ->
     world.authority = args
-    dict = {_LABEL: "S_#{args.label()}", _EXPORTS: SPRITE_EXPORTS}
+    dict = {_LABEL: args.label(), _EXPORTS: SPRITE_EXPORTS}
     child = world.add_child dict
   go: (world, args) ->
     split = world.get('split')
@@ -25,7 +25,7 @@
     signal = world.get('next_signal')
     world.call(signal['do'], signal)
   reset: (world, args) ->
-    world.put 'program_counter', 0
+    # TODO: Reset program counter
     for key in ['i', 'j', 'v_i', 'v_j']
       world.put key, undefined
   click: (world, args) ->


fixed memory leak in key fetching

fixed memory leak in key fetching

--- src/core/data/keys.coffee
+++ src/core/data/keys.coffee
@@ -11,18 +11,22 @@

   get_keys = (arr) ->
     if arr instanceof Array
-      for d in arr
-        get_keys d
+      get_keys d for a in arr
     else if validObject arr
       for k, v of arr
-        if validObject v
-          get_keys v
-        else if !(k in vars[type].keys) and v isnt null
+        if k.indexOf("d3plus") isnt 0 and
+           !(k in vars[type].keys) and
+           v isnt null
           vars[type].keys[k] = typeof v

   if validObject vars[type].value
+    lengthMatch = d3.keys(vars[type].value).length is vars.id.nesting.length
+    for k, v of vars[type].value
+      if lengthMatch and vars.id.nesting.indexOf(k) >= 0 and validObject v
+        get_keys vv for kk, vv of v
+      else
+        get_keys v
+  else
     get_keys v for k, v of vars[type].value
-  else
-    get_keys vars[type].value

   print.time timerString if vars.dev.value


Add textDecoration property for underline, overline, linethrough


fix(online-friends-by-default): fix link element selection

--- src/online-friends-by-default/index.coffee
+++ src/online-friends-by-default/index.coffee
@@ -8,6 +8,6 @@
   run: ->
     utils = require "utils"

-    friendsLink = $ "#l_fr > a"
+    friendsLink = $( "#l_fr > a" )[ 0 ]
     utils.setConditional "friends.onlineByDefault", friendsLink, "href",
       true: "/friends?section=online"


Tweak

Tweak

--- test/spec/ecosystem/backbone-relational.tests.coffee
+++ test/spec/ecosystem/backbone-relational.tests.coffee
@@ -1012,5 +1012,4 @@
     ###########################
     done()

-
   it 'CLEANUP', -> kb.configure({orm: 'default'})


activity feed: stop turning off non-existent subscriptions

--- client/app/Applications/Activity.kdapplication/AppController.coffee
+++ client/app/Applications/Activity.kdapplication/AppController.coffee
@@ -76,7 +76,6 @@
     @listController.once 'teasersLoaded', @teasersLoaded.bind @

   continueLoadingTeasers:->
-    @clearPopulateActivityBindings()
     @populateActivity to : @lastFrom

   attachEvents:(controller)->


servers: generalize checkGroupMembership function body

--- workers/social/lib/social/models/account.coffee
+++ workers/social/lib/social/models/account.coffee
@@ -387,15 +387,19 @@
         return callback null, account.id

   checkGroupMembership: secure (client, groupName, callback) ->
-    { delegate } = client.connection
+    unless client?.connection?.delegate
+      return callback new KodingError "invalid request"
+    JAccount.checkGroupMembership client.connection.delegate, groupName, callback
+
+  @checkGroupMembership: (account, groupName, callback) ->
     JGroup = require './group'
     JGroup.one { slug : groupName }, (err, group) ->
       return callback new KodingError 'An error occurred!' if err
-      return callback null, no unless group
+      return callback new KodingError 'Group not found'  unless group

       Relationship.one {
         as          : 'member'
-        targetId    : delegate.getId()
+        targetId    : account.getId()
         sourceId    : group.getId()
       }, (err, relation) ->
         return callback new KodingError 'An error occurred!' if err


fixed contentWidth watcher again

fixed contentWidth watcher again

--- gilbox-kinetic-slider.coffee
+++ gilbox-kinetic-slider.coffee
@@ -182,7 +182,7 @@
         contElm.css 'width', contentWidth + 'px'

       calcContentWidth()
-      scope.$watch('contentWidth', calcContentWidth) if scope.contentWidth?
+      scope.$watch('contentWidth', calcContentWidth) if scope.contentWidth? || scope.contentWidth == 0

       onWinResize = ->
         calcContentWidth()


Few code lines were translated (#5511)

Few code lines were translated (#5511)

8, 15, 17, 20, 26, 47, 72, 73, 74, 75, 83

--- app/locale/ru.coffee
+++ app/locale/ru.coffee
@@ -5,22 +5,22 @@
 #    meta_description: "Learn typed code through a programming game. Learn Python, JavaScript, and HTML as you solve puzzles and learn to make your own coding games and websites."
 #    built_for_teachers_title: "A Coding Game Built with Teachers in Mind"
 #    built_for_teachers_blurb: "Teaching kids to code can often feel overwhelming. CodeCombat helps all educators teach students how to code in either JavaScript or Python, two of the most popular programming languages. With a comprehensive curriculum that includes six computer science units and reinforces learning through project-based game development and web development units, kids will progress on a journey from basic syntax to recursion!"
-#    built_for_teachers_subtitle1: "Computer Science"
+    built_for_teachers_subtitle1: "Информатика"
 #    built_for_teachers_subblurb1: "Starting with our free Introduction to Computer Science course, students master core coding concepts such as while/for loops, functions, and algorithms."
 #    built_for_teachers_subtitle2: "Game Development"
 #    built_for_teachers_subblurb2: "Learners construct mazes and use basic input handling to code their own games that can be shared with friends and family."
 #    built_for_teachers_subtitle3: "Web Development"
 #    built_for_teachers_subblurb3: "Using HTML, CSS, and jQuery, learners flex their creative muscles to program their own webpages with a custom URL to share with their classmates."
     century_skills_title: "Навыки 21 Века"
-#    century_skills_blurb1: "Students Don't Just Level Up Their Hero, They Level Up Themselves"
+    century_skills_blurb1: "Студенты не просто повышают уровень их героев, Они повышают свой уровень"
 #    century_skills_quote1: "You mess up…so then you think about all of the possible ways to fix it, and then try again. I wouldn't be able to get here without trying hard."
-#    century_skills_subtitle1: "Critical Thinking"
+    century_skills_subtitle1: "Критическое мышление"
 #    century_skills_subblurb1: "With coding puzzles that are naturally scaffolded into increasingly challenging levels, CodeCombat's programming game ensures kids are always practicing critical thinking."
 #    century_skills_quote2: "Everyone else was making mazes, so I thought, ‘capture the flag’ and that’s what I did."
-#    century_skills_subtitle2: "Creativity"
+    century_skills_subtitle2: "Креативность"
 #    century_skills_subblurb2: "CodeCombat encourages students to showcase their creativity by building and sharing their own games and webpages."
 #    century_skills_quote3: "If I got stuck on a level. I would work with people around me until we were all able to figure it out."
-#    century_skills_subtitle3: "Collaboration"
+    century_skills_subtitle3: "Коллаборация"
 #    century_skills_subblurb3: "Throughout the game, there are opportunities for students to collaborate when they get stuck and to work together using our pair programming guide."
 #    century_skills_quote4: "I’ve always had aspirations of designing video games and learning how to code ... this is giving me a great starting point."
     century_skills_subtitle4: "Коммуникация"
@@ -69,10 +69,10 @@
 #    global_title: "Join Our Global Community of Learners and Educators"
     global_subtitle1: "Учащиеся"
     global_subtitle2: "Строки с кодом"
-#    global_subtitle3: "Teachers"
-#    global_subtitle4: "Countries"
-#    go_to_my_classes: "Go to my classes"
-#    go_to_my_courses: "Go to my courses"
+    global_subtitle3: "Учителя"
+    global_subtitle4: "Страны"
+    go_to_my_classes: "Пойти на мои уроки"
+    go_to_my_courses: "Пойти на мои курсы"
     quotes_quote1: "Назовите любую онлайн программу, я попробовал её. Ни один из них не соответствует CodeCombat. Любой учитель, который хочет, чтобы их ученики научились кодировать ... начните здесь!"
 #    quotes_quote2: " I was surprised about how easy and intuitive CodeCombat makes learning computer science. The scores on the AP exam were much higher than I expected and I believe CodeCombat is the reason why this was the case."
 #    quotes_quote3: "CodeCombat has been the most beneficial for teaching my students real-life coding capabilities. My husband is a software engineer and he has tested out all of my programs. He put this as his top choice."
@@ -80,7 +80,7 @@
     see_example: "Посмотреть пример"
     slogan: "Самый захватывающий способ обучения реальному программированию."
     teach_cs1_free: "Обучать CS1 Бесплатно"
-#    teachers_love_codecombat_title: "Teachers Love CodeCombat"
+    teachers_love_codecombat_title: "Учителя любят CodeCombat"
 #    teachers_love_codecombat_blurb1: "Report that their students enjoy using CodeCombat to learn how to code"
 #    teachers_love_codecombat_blurb2: "Would recommend CodeCombat to other computer science teachers"
 #    teachers_love_codecombat_blurb3: "Say that CodeCombat helps them support students’ problem solving abilities"


Update ru.coffee

Update ru.coffee

I think, don`t need translate code keywords

--- app/locale/ru.coffee
+++ app/locale/ru.coffee
@@ -118,7 +118,7 @@
     about: "О нас"
     contact: "Связаться"
     twitter_follow: "Подписаться"
-#    students: "Students"
+    students: "Студентам"
     teachers: "Учителям"
     careers: "Карьера"
     facebook: "Facebook"
@@ -188,23 +188,23 @@
     campaign_old_multiplayer: "(Устаревшее) Старые многопользовательские арены"
 #    campaign_old_multiplayer_description: "Relics of a more civilized age. No simulations are run for these older, hero-less multiplayer arenas."

-#  code:
-#    if: "if" # Keywords
-#    else: "else"
-#    elif: "elif"
-#    while: "while"
-#    loop: "loop"
-#    for: "for"
-#    break: "break"
-#    continue: "continue"
-#    then: "then"
-#    do: "do"
-#    end: "end"
-#    function: "function"
-#    def: "def"
-#    self: "self"
-#    hero: "hero"
-#    this: "this"
+  code:
+    if: "if" # Keywords
+    else: "else"
+    elif: "elif"
+    while: "while"
+    loop: "loop"
+    for: "for"
+    break: "break"
+    continue: "continue"
+    then: "then"
+    do: "do"
+    end: "end"
+    function: "function"
+    def: "def"
+    self: "self"
+    hero: "hero"
+    this: "this"

   share_progress_modal:
     blurb: "Вы отлично продвигаетесь! Расскажите своим родителям, как много вы уже выучили с CodeCombat."
@@ -649,13 +649,13 @@
     restricted_to_certain_heroes: "Только определенные герои могут играть на этом уровне."

   skill_docs:
-#    function: "function" # skill types
-#    method: "method"
-#    snippet: "snippet"
-#    number: "number"
-#    array: "array"
-#    object: "object"
-#    string: "string"
+    function: "функция" # skill types
+    method: "метод"
+    snippet: "отрезок"
+    number: "номер"
+    array: "массив"
+    object: "объект"
+    string: "строка"
     writable: "доступно для записи" # Hover over "attack" in Your Skills while playing a level to see most of this
     read_only: "только чтение"
     action: "Действие"


Update uk.coffee (#5618)

Update uk.coffee (#5618)

--- app/locale/uk.coffee
+++ app/locale/uk.coffee
@@ -1478,7 +1478,7 @@
     switch_to: "Переключитись на"
     greetings: "Вітання!"
     back_classrooms: "Повернутись до моїх класів"
-#    back_classroom: "Back to classroom"
+    back_classroom: "Повернутися до класу"
     back_courses: "Повернутись до моїх курсів"
     edit_details: "Редагувати деталі класу"
     purchase_enrollments: "Придбати учнівські ліцензії"


Put back logs for production environment

--- main.coffee
+++ main.coffee
@@ -62,15 +62,15 @@
         text

     info: (texts...) ->
-        if process.env.DEBUG or process.env.NODE_ENV isnt 'test'
+        if process.env.NODE_ENV isnt 'test'
             console.info @format 'info', texts

     warn: (texts...) ->
-        if process.env.DEBUG or process.env.NODE_ENV isnt 'test'
+        if process.env.NODE_ENV isnt 'test'
             console.warn @format 'warn', texts

     error: (texts...) ->
-        if process.env.DEBUG or process.env.NODE_ENV isnt 'test'
+        if process.env.NODE_ENV isnt 'test'
             console.error @format 'error', texts

     debug: (texts...) ->


Minor

Minor

--- src/gral/stories.coffee
+++ src/gral/stories.coffee
@@ -190,6 +190,8 @@
 ### istanbul ignore next ###
 try
   window.addEventListener 'beforeunload', -> mainStory.close()
+try
+  process.on 'exit', -> mainStory.close()

 #-----------------------------------------------
 # ### API


Clean up data/prefixes

Clean up data/prefixes

--- data/prefixes.coffee
+++ data/prefixes.coffee
@@ -23,13 +23,6 @@

   callback(sort(need))

-# Select only special browsers
-map = (browsers, callback) ->
-  for browser in browsers
-    [name, version] = browser.split(' ')
-    version = parseFloat(version)
-    callback(browser, name, version)
-
 # Add data for all properties
 result = { }

@@ -187,11 +180,9 @@

 # Placeholder selector
 feature require('caniuse-db/features-json/css-placeholder'), (browsers) ->
-  browsers = map browsers, (browser, name, version) ->
-    if name == 'firefox' and version <= 18
-      browser + ' old'
-    else
-      browser
+  browsers = browsers.map (i) ->
+    [name, version] = i.split(' ')
+    if name == 'firefox' and parseFloat(version) <= 18 then i + ' old' else i

   prefix '::placeholder',
           selector: true,


Fix widget generation.

Fix widget generation.

--- main.coffee
+++ main.coffee
@@ -150,6 +150,13 @@

     update: ->
         @sheet.spec.data[0][0] =  @slider.slider("value")
+
+
+toolbox =
+    sheet: Sheet
+    figure: PlotXY
+    table: Table
+    markdown: Markdown

 ## From GUI

@@ -272,9 +279,11 @@
         $blab.slider[sl].update()
         console.log $blab.slider[sl].stringify()

-    # local copy of vars
-    for sh of $blab.sheet
-        $blab.sheet[sh].toLocal()
+    # local copy of vars
+    #console.log "?????", $blab.sheet
+    for sym of $blab.sheet
+        #console.log "sh??", sym
+        $blab.sheet[sym].toLocal()

     console.log "######## user-code ########"

@@ -294,6 +303,7 @@

     console.log "#### tables ####"
     for t of $blab.table
+        console.log "t???", t
         $blab.table[t].update()
         console.log $blab.table[t].stringify()

@@ -309,28 +319,27 @@

 loadgist = (gistid, filename) ->
+
     $.ajax(
         url: "https://api.github.com/gists/#{gistid}"
         type: 'GET'
-        dataType: 'jsonp').success((gistdata) ->
+        dataType: 'jsonp'
+
+    ).success((gistdata) ->
         specs = JSON.parse(gistdata.data.files[filename].content)
-        $blab.sheet = []
-        $blab.table = []
-
-        $blab.sheet[spec.id] = new Sheet spec for spec in specs.sheet
-        $blab.table[spec.id] = new Table spec for spec in specs.table
-        #$blab.sheet[spec.id] = new Sheet spec for spec in specs.sheet
-        #$blab.sheet[spec.id] = new Sheet spec for spec in specs.sheet
-
-        console.log "sheet A???", $blab.sheet["A"]
-        console.log "table A???", $blab.table["A"]
+
+        build = (w) ->
+            $blab[w] = {}
+            $blab[w][spec.id] = new toolbox[w] spec for spec in specs[w]
+
+        for spec of specs
+            build(spec)

         compute()
-        return
+
     ).error (e) ->
         # ajax error
         return
-    return

 loadgist("f673df3f600fdeb17608", "app.json");


Taller browser window, more debug

Taller browser window, more debug

--- main.coffee
+++ main.coffee
@@ -27,7 +27,7 @@
     app.quit()

 app.on "ready", ->
-  mainWindow = new BrowserWindow width: 1200, height: 600
+  mainWindow = new BrowserWindow width: 1200, height: 900
   mainWindow.loadUrl "file://#{__dirname}/index.html"

   mainWindow.openDevTools()
@@ -50,7 +50,10 @@
   console.log "Downloading #{item.url} to #{destPath}"
   request.get(item.url).on('response', (response) ->
     console.log "Got status code: #{response.statusCode}"
+    console.log "Got content length: #{response.headers['content-length']}"
   ).pipe(fs.createWriteStream destPath).on 'finish', ->
     console.log "Trying to open #{destPath}"
     shell.openItem(destPath)
-
+
+
+


Update vi.coffee

Update vi.coffee

--- app/locale/vi.coffee
+++ app/locale/vi.coffee
@@ -75,7 +75,7 @@
     subscription_required: "Cần đăng kí"
     anonymous: "Người chơi vô danh"
     level_difficulty: "Khó: "
-#    play_classroom_version: "Play Classroom Version" # Choose a level in campaign version that you also can play in one of your courses
+    play_classroom_version: "Chơi Phiên Bản Lớp Học" # Choose a level in campaign version that you also can play in one of your courses
     campaign_beginner: "Chiến dịch chế độ đơn giản"
     awaiting_levels_adventurer_prefix: "Chúng tôi cho ra 5 bàn mới mỗi tuần." # {change}
     awaiting_levels_adventurer: "Đăng kí với tư cách là nhà thám hiểm"
@@ -114,7 +114,7 @@
     log_in: "đăng nhập với mật khẩu"
     required: "Bạn cần phải đăng nhập trước khi đi bạn có thể đi hướng đó."
     login_switch: "Bạn đã có tài khoản rồi ?"
-#    school_name: "School Name and City"
+    school_name: "Tên Trường và Thành Phố"
 #    optional: "optional"
 #    school_name_placeholder: "Example High School, Springfield, IL"

@@ -164,9 +164,9 @@
     accepted: "Đã được chấp nhận"
     rejected: "Không được chấp nhận"
     withdrawn: "Thu Hồi"
-#    accept: "Accept"
-#    reject: "Reject"
-#    withdraw: "Withdraw"
+    accept: "Chấp Nhận"
+    reject: "Từ Chối"
+    withdraw: "Rút Lui"
     submitter: "Người gửi"
     submitted: "Đã gửi"
     commit_msg: "Tin nhắn sự thay đổi"
@@ -221,12 +221,12 @@
     years: "năm"

   play_level:
-#    completed_level: "Completed Level:"
+    completed_level: "Hoàn thành Level:"
 #    course: "Course:"
     done: "Hoàn thành"
-#    next_level: "Next Level:"
+    next_level: "Level tiếp theo:"
 #    next_game: "Next game"
-#    show_menu: "Show game menu"
+    show_menu: "Hiện game menu"
 #    home: "Home" # Not used any more, will be removed soon.
     level: "Bàn" # Like "Level: Dungeons of Kithgard"
     skip: "Bỏ qua"
@@ -261,7 +261,7 @@
     victory_hour_of_code_done_yes: "Đúng vậy, tôi đã hoàn tất thời gian lập trình!"
     victory_experience_gained: "XP nhận được"
     victory_gems_gained: "Ngọc nhận được"
-#    victory_new_item: "New Item"
+    victory_new_item: "Vật phẩm mới"
 #    victory_viking_code_school: "Holy smokes, that was a hard level you just beat! If you aren't already a software developer, you should be. You just got fast-tracked for acceptance with Viking Code School, where you can take your skills to the next level and become a professional web developer in 14 weeks."
 #    victory_become_a_viking: "Become a Viking"
     guide_title: "Hướng dẫn"


Widget class.

Widget class.

--- main.coffee
+++ main.coffee
@@ -1,3 +1,12 @@
+class Widget
+
+    constructor: ->
+
+    update: ->
+
+    stringify: ->
+        JSON.stringify(@spec)
+

 class Sheet

@@ -31,98 +40,86 @@
         c = JSON.stringify(@colHeaders)
         "{spec:#{s}, data:#{d}, rowHeaders:#{r}, colHeaders:#{c}}"

-class PlotXY
+
+
+class PlotXY extends Widget

     constructor: (@spec) ->
-
-        @spec.data["columns"] = @getCols() # c3-style data
+
+        @sheets = ($blab.sheet[id] for id in @spec.sheetIds)
+
+        defaults = {}

         @chart = c3.generate(
             bindto: $("##{@spec.id}")[0]
-            data: @spec.data
+            data: $.extend({}, defaults, @spec.data, @getCols())
         )

     getCols: ->
         cols = []
-        cols.push c for c in sheet.labelRows() for sheet in @spec.data.sheets
+        cols.push c for c in sheet.labelRows() for sheet in @sheets
         columns: cols

     update: ->
         @chart.load(@getCols())

-    stringify: ->
-        JSON.stringify($blab.figure[@spec.id]["spec"])
-
-class Table
-
+class Table extends Widget
+
     constructor: (@spec) ->

-        sheet = $blab.sheet[@spec.id]
-
-        data = sheet.data
-        numRows = data.length
-        numCols = data[0].length
+        @sheet = $blab.sheet[@spec.id]
         container = $("[data-sym=#{@spec.id}][data-type='table']")[0]

-        @table = new Handsontable container,
-            data: data
-            startRows: numRows
-            startCols: numCols
-            rowHeaders: sheet.rowHeaders
-            colHeaders: sheet.colHeaders
-            contextMenu: true
-            columns: ({type: 'numeric'} for k in [1..numCols])
+        @defaults =
+            data: @sheet.data
             afterChange: (change, source) =>
                 compute() if source is "edit" and @spec.compute
-
-    update: ->
-        @table.loadData $blab.sheet[@spec.id].data
+            columns: ({type: 'numeric'} for k in [1..@sheet.data[0].length])
+            rowHeaders: @sheet.rowHeaders
+            colHeaders: @sheet.colHeaders
+            contextMenu: false
+
+        @table = new Handsontable container, $.extend({}, @defaults, @spec.table)
+
+    update: ->
+        @table.loadData @sheet.data
         @table.render()

-    stringify: ->
-        JSON.stringify($blab.table[@spec.id]["spec"])
-
-
-class Slider
+
+class Slider extends Widget

     constructor: (@spec) ->

-        @spec.width ?= 500
-        @spec.value ?= 1
-        @spec.min ?= 0
-        @spec.max ?= 10
-        @spec.step ?= 1
-
-        sheet = $blab.sheet[@spec.id]
+        @sheet = $blab.sheet[@spec.id]
+
+        @container = $("[data-sym=#{@spec.id}][data-type='slider']")
+        @container.append("

")
+        @container.append("

")
+        @container.append("<input type='text' readonly class='report'>")
+        @report = @container.find('.report')
+
+        defaults =
+            width: 500
+            value: 1
+            min: 0
+            max: 10
+            step: 1
+            change: (event, ui) =>
+                @report.val ui.value
+                compute()
+
+        settings = $.extend({}, defaults, @spec)
+
+        @container.find('.label').html(@sheet.rowHeaders[0])
+        @container.css("width", settings.width)

-        container = $("[data-sym=#{@spec.id}][data-type='slider']")
-        container.css("width", @spec.width)
-        container.append("

")
-        container.append("

")
-        container.append("<input type='text' readonly class='report'>")
-
-        label = container.find('.label')
-        label.html(sheet.rowHeaders[0])
-
-        report = container.find('.report')
-
-        @slider = container.find('.slider')
-        @slider.slider
-            value: @spec.value
-            min: @spec.min
-            max: @spec.max
-            step: @spec.step
-            change: (event, ui) =>
-                report.val ui.value
-                compute() if @spec.compute
-
-        report.val @slider.slider("value")
-
-    update: ->
-        $blab.sheet[@spec.id].data[0][0] =  @slider.slider("value")
-
-    stringify: ->
-        JSON.stringify($blab.slider[@spec.id]["spec"])
+        @slider = @container.find('.slider')
+        @slider.slider settings
+
+        @report.val @slider.slider("value")
+
+    update: ->
+        @sheet.data[0][0] =  @slider.slider("value")

 ## From GUI

@@ -151,35 +148,34 @@
 $blab.sheet['q'].colHeaders = ['i','ii','iii','iv','v','vi']

+
+
 # slider

 slid = (id) -> new Slider
     id:id
-    width: 500
-    value: 1
-    min: 0
-    max: 10
-    step: 1
-    compute: true
+    value: 3

 $blab.slider =
     z: slid "z"

 # tables

-tab = (id) -> new Table {id:id, compute:true}
+tab = (id) -> new Table {id:id, compute:true, table: {rowHeaders: false}}
 $blab.table =
     A: tab "A"
     x: tab "x"
     b: tab "b"
     y: tab "y"

+
+
 # figures

 fig1 =  -> new PlotXY
     id: "fig1"
+    sheetIds: ["q"]
     data:
-        sheets: [$blab.sheet["q"]]
         x: ""
         types:
             one: 'area'
@@ -187,8 +183,8 @@

 fig2 =  -> new PlotXY
     id: "fig2"
+    sheetIds: ["x1", "q"]
     data:
-        sheets: [$blab.sheet["x1"], $blab.sheet["q"]]
         x: "r0"
         types:
             one: 'spline'
@@ -198,6 +194,7 @@
     fig1: fig1()
     fig2: fig2()

+console.log $blab.slider["z"].stringify()

 # user code


write natal config file

write natal config file

--- main.coffee
+++ main.coffee
@@ -33,6 +33,17 @@
   fs.writeFileSync path, pairs.reduce (contents, [rx, replacement]) ->
     contents.replace rx, replacement
   , fs.readFileSync path, encoding: 'ascii'
+
+
+writeConfig = (config) ->
+  try
+    fs.writeFileSync '.natal', JSON.stringify config, null, 2
+  catch {message}
+    logErr \
+      if message.match /EACCES/i
+        'Invalid write permissions for creating .natal config file'
+      else
+        message

 init = (projName) ->
@@ -142,8 +153,9 @@
         ]
       ]

+    log 'Creating Natal config'
     process.chdir '../..'
-    openXcode projName
+    writeConfig name: projName

     log '\nWhen Xcode appears, click the play button to run the app on the simulator.', 'yellow'
     log 'Then run the following for an interactive workflow:', 'yellow'


Added some words and a regex to match them

--- main.coffee
+++ main.coffee
@@ -243,6 +243,18 @@
 			val = Math.floor Math.random() * letters.length
 			@string += letters.charAt val
 		@img = string_to_image @string, 'red'
+
+# Just random words
+class Words extends Block
+	constructor: ->
+		words = ['fire','walk','with','me','the','owls','are','not','what','they','seem','to','be','I','will','be','back',
+				 'star','trek','turtle','chaos','engage','Qapla\'','It is alive','Do','ya','feel','lucky?',
+				 'Well','do','ya','punk?','star','wars',
+				 'may','the','force','be','with','you','xkcd','Wax','on','Wax','off','Yippekiyiyay','Hasta','la','vista']
+		word = Math.floor Math.random() * words.length
+		@string = words[word]
+		@img = string_to_image @string, 'red'
+

 class GameRegex
 	constructor: (regex) ->
@@ -282,6 +294,8 @@
 			regex = new StartsWithpow()
 		when 7
 			regex = new Containsaa()
+		when 8
+			regex = new Words()
 		else
 			regex = new LettersDigits()

@@ -304,7 +318,7 @@
 	return [cx,cy]

-all_regexes = ['^\\d*$','^\\w+$','^\\\\$','^\\w{1,3}\\d{1,3}$','^Mathias$','zaS$','^pow','aa','^...$','\\.']
+all_regexes = ['^\\d*$','^\\w+$','^\\\\$','^\\w{1,3}\\d{1,3}$','^Mathias$','zaS$','^pow','aa','^...$','\\.','^\\w+\\W$']

 generate_regex_menu = (world) ->


Better impl. of getDirection

Better impl. of getDirection

--- src/direction.coffee
+++ src/direction.coffee
@@ -1,3 +1,5 @@
+winston = require 'winston'
+
 exports.directions =
 	up: [0, -1]
 	top: [0, -1]
@@ -19,16 +21,39 @@

 exports.asString = (offset) ->
 	dirs = while offset[0] isnt 0 or offset[1] isnt 0
-		if offset[0] > 0 then offset[0]--; 'right'
+		if offset[1] > 0 then offset[1]--; 'down'
+		else if offset[1] < 0 then offset[1]++; 'up'
+		else if offset[0] > 0 then offset[0]--; 'right'
 		else if offset[0] < 0 then offset[0]++; 'left'
-		else if offset[1] > 0 then offset[1]--; 'down'
-		else if offset[1] < 0 then offset[1]++; 'up'

 	dirs.join '-'

 exports.getDirection = (x0, y0, x1, y1) ->
-	{bresenhamLine} = require './util'
+	# y1 is sub. from y0, but x0 is sub. from x1.
+	# atan has positive Y be up, but in-game
+	# positive Y is down, so it needs to be inverted

-	dest = (bresenhamLine x0, y0, x1, y1)[1]
+	exports.radToDirection Math.atan2 y0-y1, x1-x0

-	exports.asString [dest.x-x0, dest.y-y0]+exports.radToDirection = (angle) ->
+	angle /= Math.PI # radians to a factor
+	while angle < 0 then angle += 2
+	while angle > 2 then angle -= 2
+
+	# right is added twice, so 1.875 <= angle < 2 gets handled properly
+	angles = [
+		'right'
+		'up-right'
+		'up'
+		'up-left'
+		'left'
+		'down-left'
+		'down'
+		'down-right'
+		'right'
+	]
+
+	for a in angles
+		return a if (-0.125 <= angle < 0.125)
+
+		angle -= 2/8


Fix "Correctly replace @mentions in Talker adapter"

--- src/hubot/talker.coffee
+++ src/hubot/talker.coffee
@@ -37,11 +37,9 @@

     bot.on "TextMessage", (room, message)->
       unless self.name == message.user.name
-        content = message.content
-
         # Replace "@mention" with "mention: ", case-insensitively
-        regexp = new RegExp("\\b@#{self.quoteRegex(self.name)}\\b", 'i')
-        content = message.content.replace(regexp, "#{self.name}: ")
+        regexp = new RegExp "^@#{self.name}", 'i'
+        content = message.content.replace(regexp, "#{self.name}:")

         self.receive new Robot.TextMessage self.userForMessage(room, message), content

@@ -62,9 +60,6 @@
     author = @userForId(message.user.id, message.user)
     author.room = room
     author
-
-  quoteRegex: (string) ->
-    string.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&")

 module.exports = Talker