Clean text

Clean text

--- src/dtc.coffee
+++ src/dtc.coffee
@@ -36,10 +36,18 @@
       location = response.headers['location']
       return send_quote(message, location, response_handler)

-    txt = get_quote(body, "p.item-content")
+    txt = get_quote(body, "p.item-content a")

     response_handler txt

 get_quote = (body, selector)->
   $ = cheerio.load(body)
-  he.decode $(selector).first().text()
+  res = []
+  $(selector).first().contents().map((i, el) ->
+    $(this).text()
+  ).filter((i, txt) ->
+    txt != ''
+  ).map((i, txt) ->
+    a.push txt
+  )
+  he.decode txt.join '\n'


variable name change

variable name change

--- src/lib/models/document.coffee
+++ src/lib/models/document.coffee
@@ -613,7 +613,6 @@
 		# Prepare
 		file = @
 		CSON = require('cson')  unless CSON
-
 		# Fetch
 		fullPath = @get('fullPath')
 		content = @get('content')
@@ -632,7 +631,7 @@
 		@set({parser,header,body,content,source})

 		# Write content
-		balUtil.writeFile fileOutPath, source, (err) ->
+		balUtil.writeFile fullPath, source, (err) ->
 			# Check
 			return next(err)  if err


Don't mistake long error messages as stack trace lines.

Fixes #2, finally ;)

--- src/ParsedError.coffee
+++ src/ParsedError.coffee
@@ -2,10 +2,10 @@

 module.exports = class ParsedError
   constructor: (@error) ->
+    do @_parse
+
+  _parse: ->
     @_trace = []
-    do @_parse
-
-  _parse: ->
     @_kind = 'Error'
     @_wrapper = ''

@@ -14,8 +14,7 @@
     unless typeof @error is 'object'
       @_message = String @error
     else
-      @_message = @error.message? and String(@error.message) or ''
-      @_stack = @error.stack? and @error.stack or null
+      @_stack = @error.stack

       if @error.kind?
         @_kind = String @error.kind
@@ -23,36 +22,36 @@
         if m = @_stack.match /^([a-zA-Z0-9\_\$]+):\ /
           @_kind = m[1]

-      do @_parseTrace
+      if typeof @_stack is 'string'
+        @_parseStack()
+      else
+        @_message = @error.message? and String(@error.message) or ''

     return

-  _parseTrace: ->
-    if typeof @_stack is 'string'
-      @_parseTextAsStack @_stack
-    else if Array.isArray @_stack
-      @_parseArrayAsStack @_stack
-
-    return
-
-  _parseArrayAsStack: (a) ->
-    for item in a
-      @_trace.push String item
-
-    return
-
-  _parseTextAsStack: (text) ->
-    # remove the error kind
-    text = text.replace /^([a-zA-Z0-9\_\$]+):\ /, ''
-
-    # remove the message, if it matches
-    if text.substr(0, @message.length) is @message
-      text = text.substr(@message.length, text.length)
-
-    text = text.trim()
-
-    for line in text.split "\n"
-      @_trace.push @_parseTraceItem line
+  _parseStack: ->
+    messageLines = []
+    reachedTrace = no
+
+    for line in @_stack.split '\n'
+      continue if line.trim() is ''
+      if reachedTrace
+        @_trace.push @_parseTraceItem line
+      else
+        if line.match /^\s*at\s.+/
+          reachedTrace = yes
+          @_trace.push @_parseTraceItem line
+        else
+          messageLines.push line
+
+    message = messageLines.join '\n'
+    if message.substr(0, @_kind.length) is @_kind
+      message =
+        message
+        .substr(@_kind.length, message.length)
+        .replace(/^\:\s+/, '')
+
+    @_message = message

     return


inclusion validator

inclusion validator

--- src/validators.coffee
+++ src/validators.coffee
@@ -36,4 +36,13 @@
     else
       @msg = "Value #{value} reserved"
       false
-      +
+
+class InclusionValidator extends Validator
+  validate: (value, attributes) ->
+    range = attributes['within'] || []
+    if range.indexOf(value) > -1
+      true
+    else
+      @msg = "Value #{value} should be in range #{range}"
+      false


Automatically reload when provisioning Academica courses

This avoids a race condition where the CourseInstances are loaded but
the Classroom isn't.

--- app/views/courses/CoursesView.coco.coffee
+++ app/views/courses/CoursesView.coco.coffee
@@ -230,6 +230,13 @@
       @ownerNameMap[ownerID] = NameLoader.getName(ownerID) for ownerID in ownerIDs
       @render?()
     )
+
+    academicaCS1CourseInstance = _.find(@courseInstances.models ? [], (ci) -> ci.get('_id') is '610047c74bc544001e26ea12')
+    if academicaCS1CourseInstance
+      academicaGlobalClassroom = _.find(@classrooms.models ? [], (c) -> c.get('_id') is '610047c673801a001f85fd43')
+      if not academicaGlobalClassroom and utils.getQueryVariable('autorefresh') isnt true
+        # Refresh so that we make sure we get this loaded
+        window.location.href = '/students?autorefresh=true'

     if not @classrooms.models.length
       @nextLevelInfo = courseAcronym: 'CS1'  # Don't both trying to figure out the next level for edge case of student with no classrooms


Fix unexpected type for selectedMailboxID

--- client/app/components/application.coffee
+++ client/app/components/application.coffee
@@ -127,7 +127,7 @@
             currentSearch         : SearchStore.getCurrentSearch()
             modal                 : LayoutStore.getModal()
             useIntents            : LayoutStore.intentAvailable()
-            selectedMailboxID     : AccountStore.getSelectedMailbox()
+            selectedMailboxID     : selectedMailboxID
         }


Minor tweaks and fixes

Minor tweaks and fixes

--- src/ac-webhooks.coffee
+++ src/ac-webhooks.coffee
@@ -37,7 +37,7 @@
     taskName = timeRecord['task_name']

     taskMessage = ''
-    if typeof taskName != undefined then taskMessage = 'task ' + taskName + ' for '
+    if typeof taskName != 'undefined' then taskMessage = 'task ' + taskName + ' for the '

     message = userName + ' logged ' + time + ' ' + billableStatus + ' hours against the ' +
       taskMessage + 'project ' + projectName + ' with the description ' + summary


Finger trees: alias build for buildLeft

Finger trees: alias build for buildLeft

--- spec/finger_tree_spec.coffee
+++ spec/finger_tree_spec.coffee
@@ -143,7 +143,7 @@

 describe "An sorted sequence", ->
-  tree = SortedSeq.build [8, 3, 4, 2, 0, 1, 7, 5, 6, 9]...
+  tree = SortedSeq.build 8, 3, 4, 2, 0, 1, 7, 5, 6, 9

   it "should have the right elements in the right order", ->
     expect(asArray tree).toEqual [0..9]
@@ -164,8 +164,8 @@
     expect(asArray t).toEqual [0, 1, 3, 4, 6, 8, 9]

 describe "A pair of sorted sequences", ->
-  t1 = SortedSeq.build [8, 5, 7, 9, 1, 6]...
-  t2 = SortedSeq.build [4, 7, 2, 0, 3]...
+  t1 = SortedSeq.build 8, 5, 7, 9, 1, 6
+  t2 = SortedSeq.build 4, 7, 2, 0, 3

   it "should merge correctly", ->
     expect(asArray t1.merge(t2)).toEqual [0, 1, 2, 3, 4, 5, 6, 7, 7, 8, 9]


s/github/gitHub

s/github/gitHub

--- spec/github-file-spec.coffee
+++ spec/github-file-spec.coffee
@@ -359,66 +359,66 @@
           expect(githubFile.openUrlInBrowser).toHaveBeenCalledWith \
             'https://github.com/some-user/some-repo/issues'

-  describe "githubRepoUrl", ->
+  describe "gitHubRepoUrl", ->
     beforeEach ->
       githubFile = new GitHubFile()

     it "returns the GitHub.com URL for an HTTPS remote URL", ->
       githubFile.gitUrl = -> "https://github.com/foo/bar.git"
-      expect(githubFile.githubRepoUrl()).toBe "https://github.com/foo/bar"
+      expect(githubFile.gitHubRepoUrl()).toBe "https://github.com/foo/bar"

     it "will only strip a single .git suffix", ->
       githubFile.gitUrl = -> "https://github.com/foo/bar.git.git"
-      expect(githubFile.githubRepoUrl()).toBe "https://github.com/foo/bar.git"
+      expect(githubFile.gitHubRepoUrl()).toBe "https://github.com/foo/bar.git"

       githubFile.gitUrl = -> "https://github.com/foo/bar.git.other.git"
-      expect(githubFile.githubRepoUrl()).toBe "https://github.com/foo/bar.git.other"
+      expect(githubFile.gitHubRepoUrl()).toBe "https://github.com/foo/bar.git.other"

     it "returns the GitHub.com URL for an HTTP remote URL", ->
       githubFile.gitUrl = -> "http://github.com/foo/bar.git"
-      expect(githubFile.githubRepoUrl()).toBe "http://github.com/foo/bar"
+      expect(githubFile.gitHubRepoUrl()).toBe "http://github.com/foo/bar"

     it "returns the GitHub.com URL for an SSH remote URL", ->
       githubFile.gitUrl = -> "git@github.com:foo/bar.git"
-      expect(githubFile.githubRepoUrl()).toBe "http://github.com/foo/bar"
+      expect(githubFile.gitHubRepoUrl()).toBe "http://github.com/foo/bar"

     it "returns a GitHub enterprise URL for a non-Github.com remote URL", ->
       githubFile.gitUrl = -> "https://git.enterprize.me/foo/bar.git"
-      expect(githubFile.githubRepoUrl()).toBe "https://git.enterprize.me/foo/bar"
+      expect(githubFile.gitHubRepoUrl()).toBe "https://git.enterprize.me/foo/bar"

       githubFile.gitUrl = -> "git@git.enterprize.me:foo/bar.git"
-      expect(githubFile.githubRepoUrl()).toBe "http://git.enterprize.me/foo/bar"
+      expect(githubFile.gitHubRepoUrl()).toBe "http://git.enterprize.me/foo/bar"

     it "returns the GitHub.com URL for a git:// URL", ->
       githubFile.gitUrl = -> "git://github.com/foo/bar.git"
-      expect(githubFile.githubRepoUrl()).toBe "http://github.com/foo/bar"
+      expect(githubFile.gitHubRepoUrl()).toBe "http://github.com/foo/bar"

     it "returns the GitHub.com URL for a ssh:// URL", ->
       githubFile.gitUrl = -> "ssh://git@github.com/foo/bar.git"
-      expect(githubFile.githubRepoUrl()).toBe "http://github.com/foo/bar"
+      expect(githubFile.gitHubRepoUrl()).toBe "http://github.com/foo/bar"

     it "returns undefined for Bitbucket URLs", ->
       githubFile.gitUrl = -> "https://bitbucket.org/somebody/repo.git"
-      expect(githubFile.githubRepoUrl()).toBeUndefined()
+      expect(githubFile.gitHubRepoUrl()).toBeUndefined()

       githubFile.gitUrl = -> "https://bitbucket.org/somebody/repo"
-      expect(githubFile.githubRepoUrl()).toBeUndefined()
+      expect(githubFile.gitHubRepoUrl()).toBeUndefined()

       githubFile.gitUrl = -> "git@bitbucket.org:somebody/repo.git"
-      expect(githubFile.githubRepoUrl()).toBeUndefined()
+      expect(githubFile.gitHubRepoUrl()).toBeUndefined()

       githubFile.gitUrl = -> "git@bitbucket.org:somebody/repo"
-      expect(githubFile.githubRepoUrl()).toBeUndefined()
+      expect(githubFile.gitHubRepoUrl()).toBeUndefined()

     it "removes leading and trailing slashes", ->
       githubFile.gitUrl = -> "https://github.com/foo/bar/"
-      expect(githubFile.githubRepoUrl()).toBe "https://github.com/foo/bar"
+      expect(githubFile.gitHubRepoUrl()).toBe "https://github.com/foo/bar"

       githubFile.gitUrl = -> "https://github.com/foo/bar//////"
-      expect(githubFile.githubRepoUrl()).toBe "https://github.com/foo/bar"
+      expect(githubFile.gitHubRepoUrl()).toBe "https://github.com/foo/bar"

       githubFile.gitUrl = -> "git@github.com:/foo/bar.git"
-      expect(githubFile.githubRepoUrl()).toBe "http://github.com/foo/bar"
+      expect(githubFile.gitHubRepoUrl()).toBe "http://github.com/foo/bar"

   it "activates when a command is triggered on the active editor", ->
     activationPromise = atom.packages.activatePackage('open-on-github')


Adjust for mobile Safari's incorrect ClientRect implementation

--- src/trix/views/text_view.coffee
+++ src/trix/views/text_view.coffee
@@ -186,6 +186,14 @@
         pageX = rect.right
         pageY = rect.top + rect.height / 2

+        # All browsers except mobile Safari correctly return ClientRect
+        # position properties that are relative to the viewport. Mobile
+        # Safari's properties are relative to the body, so adjust them.
+        if /mobile.*safari/i.test(navigator.userAgent)
+          pageX -= document.body.scrollLeft
+          pageY -= document.body.scrollTop
+
+
         [pageX, pageY]

   getSelectedRange: ->


use a smaller image in about, not necessary 512

--- src/ui/views/aboutlayout.coffee
+++ src/ui/views/aboutlayout.coffee
@@ -24,7 +24,7 @@
 module.exports = exp = layout ->
     div class: 'about', ->
         div ->
-            img src: path.join __dirname, '..', '..', 'icons', 'icon@32.png'
+            img src: path.join __dirname, '..', '..', 'icons', 'icon@8.png'
         div class: 'name', ->
             h2 'YakYak v' + localVersion
         # TODO: if objects are undefined then it should check again on next


What is this

What is this

--- src/coffee/geometry/rectangle.coffee
+++ src/coffee/geometry/rectangle.coffee
@@ -73,7 +73,6 @@
   overlaps: (other) ->

     ###
-      Fuck you whore
       Redirects to appropriate method.

       I/P: Polygon/Circle/Rect


Small arrangement

Small arrangement

--- src/6.web.coffee
+++ src/6.web.coffee
@@ -3,100 +3,103 @@

 global.web = {}
 web.request = (options) -> new Promise((resolve, reject) ->
-    method = options.method
-    url = options.url
-    headers = options.headers ? null
-    body = options.body ? null
-    timeout = options.timeout ? null
-    responseBodyType = options.responseBodyType ? "text"
-    if body? and typeof body != "string" and body not instanceof Uint8Array
-        reject(new Error("error")); return
-    if responseBodyType not in ["binary", "text", "json"]
-        reject(new Error("error")); return
-    if mate.environmentType == "browser" then do ->
-        xhr = new XMLHttpRequest()
-        xhr.open(method, url)
-        if headers?
-            Object.forKeyValue(headers, (key, value) -> xhr.setRequestHeader(key, value))
-        xhr.responseType =
-            if responseBodyType == "binary"
-                "arraybuffer"
-            else if responseBodyType == "text"
-                "text"
-            else if responseBodyType == "json"
-                "text"
-        xhr.timeout = timeout ? 0
-        xhr.onload = ->
-            response =
-                statusCode: xhr.status
-                statusReason: xhr.statusText
-                headers:
-                    xhr.getAllResponseHeaders()
-                    .stripTrailingNewline()
-                    .splitDeep("\r\n", ": ", 1)
-                    .map((header) -> [header[0].toLowerCase(), header[1]])
-                    .toObject()
-                body:
-                    if responseBodyType == "binary"
-                        new Uint8Array(xhr.response)
-                    else if responseBodyType == "text"
-                        xhr.response
-                    else if responseBodyType == "json"
-                        JSON.parse(xhr.response)
-            if (200 <= response.statusCode < 300)
-                resolve(response)
-            else
-                reject(response)
-        xhr.onerror = ->
-            reject(new Error("error"))
-        xhr.ontimeout = ->
-            reject(new Error("timeout"))
-        xhr.send(body)
-    else do ->
-        http = module.require("http")
-        https = module.require("https")
-        urlMod = module.require("url")
-        parsedUrl = urlMod.parse(url)
-        httpOrHttps = if parsedUrl.protocol == "https:" then https else http
-        rawRequest = httpOrHttps.request(
-            {
-                method: method
-                hostname: parsedUrl.hostname
-                port: parsedUrl.port
-                path: parsedUrl.path
-                headers: headers
-            },
-            (rawResponse) ->
-                data = new Buffer(0)
-                rawResponse.on("data", (chunk) ->
-                    data = Buffer.concat([data, chunk])
+    try
+        method = options.method
+        url = options.url
+        headers = options.headers ? null
+        body = options.body ? null
+        timeout = options.timeout ? null
+        responseBodyType = options.responseBodyType ? "text"
+        if body? and typeof body != "string" and body not instanceof Uint8Array
+            fail()
+        if responseBodyType not in ["binary", "text", "json"]
+            fail()
+        if mate.environmentType == "browser" then do ->
+            xhr = new XMLHttpRequest()
+            xhr.open(method, url)
+            if headers?
+                Object.forKeyValue(headers, (key, value) -> xhr.setRequestHeader(key, value))
+            xhr.responseType =
+                if responseBodyType == "binary"
+                    "arraybuffer"
+                else if responseBodyType == "text"
+                    "text"
+                else if responseBodyType == "json"
+                    "text"
+            xhr.timeout = timeout ? 0
+            xhr.onload = ->
+                response =
+                    statusCode: xhr.status
+                    statusReason: xhr.statusText
+                    headers:
+                        xhr.getAllResponseHeaders()
+                        .stripTrailingNewline()
+                        .splitDeep("\r\n", ": ", 1)
+                        .map((header) -> [header[0].toLowerCase(), header[1]])
+                        .toObject()
+                    body:
+                        if responseBodyType == "binary"
+                            new Uint8Array(xhr.response)
+                        else if responseBodyType == "text"
+                            xhr.response
+                        else if responseBodyType == "json"
+                            JSON.parse(xhr.response)
+                if (200 <= response.statusCode < 300)
+                    resolve(response)
+                else
+                    reject(response)
+            xhr.onerror = ->
+                reject(new Error())
+            xhr.ontimeout = ->
+                reject(new Error("timeout"))
+            xhr.send(body)
+        else do ->
+            http = module.require("http")
+            https = module.require("https")
+            urlMod = module.require("url")
+            parsedUrl = urlMod.parse(url)
+            httpOrHttps = if parsedUrl.protocol == "https:" then https else http
+            rawRequest = httpOrHttps.request(
+                {
+                    method: method
+                    hostname: parsedUrl.hostname
+                    port: parsedUrl.port
+                    path: parsedUrl.path
+                    headers: headers
+                },
+                (rawResponse) ->
+                    data = new Buffer(0)
+                    rawResponse.on("data", (chunk) ->
+                        data = Buffer.concat([data, chunk])
+                    )
+                    rawResponse.on("end", ->
+                        response =
+                            statusCode: rawResponse.statusCode
+                            statusReason: rawResponse.statusMessage
+                            headers: rawResponse.headers
+                            body:
+                                if responseBodyType == "binary"
+                                    new Uint8Array(data)
+                                else if responseBodyType == "text"
+                                    data.toString()
+                                else if responseBodyType == "json"
+                                    JSON.parse(data.toString())
+                        if (200 <= response.statusCode < 300)
+                            resolve(response)
+                        else
+                            reject(response)
+                    )
+            )
+            if timeout?
+                rawRequest.setTimeout(timeout, ->
+                    rawRequest.abort()
+                    reject(new Error("timeout"))
                 )
-                rawResponse.on("end", ->
-                    response =
-                        statusCode: rawResponse.statusCode
-                        statusReason: rawResponse.statusMessage
-                        headers: rawResponse.headers
-                        body:
-                            if responseBodyType == "binary"
-                                new Uint8Array(data)
-                            else if responseBodyType == "text"
-                                data.toString()
-                            else if responseBodyType == "json"
-                                JSON.parse(data.toString())
-                    if (200 <= response.statusCode < 300)
-                        resolve(response)
-                    else
-                        reject(response)
-                )
-        )
-        if timeout?
-            rawRequest.setTimeout(timeout, ->
-                rawRequest.abort()
-                reject(new Error("timeout"))
-            )
-        rawRequest.on("error", (e) ->
-            reject(new Error("error"))
-        ).end(if body instanceof Uint8Array then new Buffer(body) else body)
+            rawRequest.on("error", (e) ->
+                reject(new Error())
+            ).end(if body instanceof Uint8Array then new Buffer(body) else body)
+    catch ex
+        reject(ex)
 )
 web.get = (url, options) ->
     actualOptions =


Added more language modes.

Added more language modes.

--- src/atom/buffer.coffee
+++ src/atom/buffer.coffee
@@ -25,8 +25,14 @@

     extension = if @url then @url.split('/').pop().split('.').pop() else null
     modeName = switch extension
-      when "js" then "javascript"
-      else "text"
+      when 'js' then 'javascript'
+      when 'coffee' then 'coffee'
+      when 'rb', 'ru' then 'ruby'
+      when 'c', 'h', 'cpp' then 'c_cpp'
+      when 'html', 'htm' then 'html'
+      when 'css' then 'css'
+
+      else 'text'

     @mode = new (require("ace/mode/#{modeName}").Mode)
     @mode.name = modeName


Use a DocumentFragment to insert all lines at once

--- src/atom/editor.coffee
+++ src/atom/editor.coffee
@@ -202,14 +202,13 @@
     elementsToReplace = @lineCache[startRow...endRow]
     @lineCache[startRow...endRow] = lineElements?.toArray() or []

-    lines = @lines[0]
     if lineElements
+      fragment = document.createDocumentFragment()
+      lineElements.each -> fragment.appendChild(this)
       if elementToInsertBefore
-        lineElements.each ->
-          lines.insertBefore(this, elementToInsertBefore)
+        @lines[0].insertBefore(fragment, elementToInsertBefore)
       else
-        lineElements.each ->
-          lines.appendChild(this)
+        @lines[0].appendChild(fragment)

     $(elementsToReplace).remove()


make modeForURL has no side effects

make modeForURL has no side effects

--- src/atom/editor.coffee
+++ src/atom/editor.coffee
@@ -35,19 +35,21 @@
     Gemfile: 'ruby'
     Rakefile: 'ruby'

-  setModeForSession: (session) ->
-    return if not @url
+  modeForURL: (url) ->
+    return if not url

-    if not modeName = @modeFileMap[ _.last @url.split '/' ]
-      language = _.last @url.split '.'
+    if not modeName = @modeFileMap[ _.last url.split '/' ]
+      language = _.last url.split '.'
       language = language.toLowerCase()
       modeName = @modeMap[language] or language

     try
-      mode = require("ace/mode/#{modeName}").Mode
-      session.setMode new mode
+      require("ace/mode/#{modeName}").Mode
     catch e
       console.error e
+
+  setModeForURL: (url) ->
+    @ace.session.setMode new (@modeForURL url)

   title: ->
     if @url then _.last @url.split '/' else 'untitled'
@@ -73,8 +75,8 @@
     session.setTabSize if useSoftTabs then @guessTabSize code else 8
     session.setUndoManager new UndoManager
     session.on 'change', => @dirty = true
-    @setModeForSession session
     @ace.setSession session
+    @setModeForURL @url if @url

     window.setTitle @title()
     @dirty = false
@@ -118,7 +120,7 @@

   setCode: (code) ->
     @ace.getSession().setValue code
-
+
   removeTrailingWhitespace: ->
     return
     @ace.replaceAll "",


Typo

Typo

Signed-off-by: Salim Djerbouh <64b2b6d12bfe4baae7dad3d018f8cbf6b0e7a044@caddydz.me>

--- src/atom/snippets.cson
+++ src/atom/snippets.cson
@@ -1,21 +1,21 @@
 '.source.php':
-	'comment':
-		'prefix' : 'comment'
-		'body': '/**\n * $1\n * @param $2\n * @var $3\n * @return $4\n */$5'
+  'comment':
+    'prefix' : 'comment'
+    'body': '/**\n * $1\n * @param $2\n * @var $3\n * @return $4\n */$5'
 '.blade.php':
-	'bstext':
-		'prefix' : 'bstext'
-		'body': '
\n\t$2:\n\t<input type="text" class="form-control" name="$1" id="$1">\n
\n$3'
-	'Vue JS Router':
-		'prefix' : 'router'
-		'body': '<router-link to="${1:url}" exact>${2:title}</router-link>\n\t$3'
-	'Localization retrieval':
-		'prefix' : 'translate'
-		'body': '{{ __(\'${1}\') }}'
+  'bstext':
+    'prefix' : 'bstext'
+    'body': '
\n\t$2:\n\t<input type="text" class="form-control" name="$1" id="$1">\n
\n$3'
+  'Vue JS Router':
+    'prefix' : 'router'
+    'body': '<router-link to="${1:url}" exact>${2:title}</router-link>\n\t$3'
+  'Localization retrieval':
+    'prefix' : 'translate'
+    'body': '{{ __(\'${1}\') }}'
 '.source.js':
-	'VueJS component':
-		'prefix' : 'component'
-		'body': 'Vue.component (\'${1:name}\', {\n\tprops: [\n\t\t\'${2:property}\',\n\t],\n\ttemplate: `\n\t\t${3:html}\n\t`,\n\tdata: function(){\n\t\treturn {\n\t\t\t${4:key}: ${5:value}\n\t\t}\n\t}\n\tmethods: {\n\t\t${6:method_name}() {\n\t\t\t${7:// code}\n\t\t}\n\t}\n});\n$8'
-	'new vue':
-		'prefix' : 'new vue'
-		'body': 'new Vue({\n\tel: \'${1:#app}\',\n\tdata: {\n\t\t${2:name} : \'${3:value}\',\n\t}\n});\n$7'
+  'VueJS component':
+    'prefix' : 'component'
+    'body': 'Vue.component (\'${1:name}\', {\n\tprops: [\n\t\t\'${2:property}\',\n\t],\n\ttemplate: `\n\t\t${3:html}\n\t`,\n\tdata: function(){\n\t\treturn {\n\t\t\t${4:key}: ${5:value}\n\t\t}\n\t},\n\tmethods: {\n\t\t${6:method_name}() {\n\t\t\t${7:// code}\n\t\t}\n\t}\n});\n$8'
+  'new vue':
+    'prefix' : 'new vue'
+    'body': 'new Vue({\n\tel: \'${1:#app}\',\n\tdata: {\n\t\t${2:name} : \'${3:value}\',\n\t}\n});\n$7'


Reverse project page plots

Reverse project page plots

--- static/scripts/plottings.coffee
+++ static/scripts/plottings.coffee
@@ -47,7 +47,7 @@
                                 0

         _preparePlot: (plot) ->
-            _.map plot.reverse(), (item, num) -> [num, item]
+            _.map plot, (item, num) -> [num, item]

         _checkDatasets: (datasets) ->
             datasets.length and (_.any datasets, (@item) ->


Fixed teams filtering/pagination JS bugs

--- static/src/scripts/teams.coffee
+++ static/src/scripts/teams.coffee
@@ -4,11 +4,13 @@
   addUrlParam = (search, key, val) ->
     newParam = key + '=' + val
     params = '?' + newParam
-    # Try to replace an existance instance
-    params = search.replace(new RegExp('[?&]' + key + '[^&]*'), '&' + newParam)
-    # If nothing was replaced, then add the new param to the end
-    if params == search
-      params += '&' + newParam
+    # If the "search" string exists, then build params from it
+    if search
+      # Try to replace an existance instance
+      params = search.replace(new RegExp('([\?&])' + key + '[^&]*'), '$1' + newParam)
+      # If nothing was replaced, then add the new param to the end
+      if params == search
+        params += '&' + newParam

     params

@@ -30,13 +32,15 @@

   $('#university').change ->
     value = $(this).val()
-    console.log value
     if value == "ALL"
-      window.location = removeURLParam(window.location.href, 'university')
+      url = removeURLParam window.location.href, 'university'
+      window.location = removeURLParam url, 'page' # reset pagination when changing filters
     else
       newUrl = document.location.pathname + addUrlParam document.location.search, 'university', value
+      newUrl = removeURLParam newUrl, 'page' # reset pagination
       window.location = newUrl

   $('#order-by').change ->
     value = $(this).val()
-    window.location = document.location.pathname + addUrlParam document.location.search, 'orderBy', value+    newUrl = document.location.pathname + addUrlParam document.location.search, 'orderBy', value
+    window.location = removeURLParam newUrl, 'page' # reset pagination


Ensured it is never less than 0

Ensured it is never less than 0

--- coffee/react-countdown-clock.coffee
+++ coffee/react-countdown-clock.coffee
@@ -62,7 +62,7 @@
     setTimeout ( =>
       duration = Date.now() - start
       @setState
-        seconds: @state.seconds - duration / 1000
+        seconds: Math.max(0,@state.seconds - duration / 1000)
       @_tick() unless @state.seconds <= 0
     ), 30


Tooltip: hide on scroll overheade minimized

--- components/tooltip/KDTooltip.coffee
+++ components/tooltip/KDTooltip.coffee
@@ -23,7 +23,7 @@

     super options, data

-    @visible    = yes
+    @visible    = no
     @parentView = @getDelegate()
     @wrapper    = new KDView cssClass : 'wrapper'
     @arrow      = new KDView cssClass : 'arrow'
@@ -57,10 +57,14 @@

     super

+    @visible = yes
+
   hide: (event)->
+    return unless @visible
     super
     @getDomElement().remove()
     @getSingleton("windowController").removeLayer this
+    @visible = no

   update:(o = @getOptions(), view = null)->
     unless view


No longer using React

No longer using React

--- atom.symlink/config.cson
+++ atom.symlink/config.cson
@@ -37,7 +37,6 @@
     'sublime-tabs'
     'jshint'
   ]
-  'useReactEditor': true
 'autosave': {}
 'spell-check':
   'grammars': [


feat(atom): update config

feat(atom): update config

--- atom.symlink/config.cson
+++ atom.symlink/config.cson
@@ -1,7 +1,10 @@
 "*":
   "atom-beautify":
     _analyticsUserId: "3eaeb424-5202-4cf8-af0f-57c4b346b7ec"
+    general:
+      _analyticsUserId: "43482f2d-cbc8-4850-bf69-0c859856d2f7"
   core:
+    closeDeletedFileTabs: true
     disabledPackages: [
       "background-tips"
       "exception-reporting"
@@ -17,6 +20,8 @@
       "tmp"
       "vendor"
     ]
+    projectHome: "/Users/ludow/code"
+    telemetryConsent: "no"
     themes: [
       "unity-ui"
       "glacier-syntax"
@@ -25,7 +30,9 @@
     fontFamily: "Menlo"
     fontSize: 15
     invisibles: {}
+    preferredLineLength: 100
     showIndentGuide: true
+    showInvisibles: true
     softWrapAtPreferredLineLength: true
   "release-notes":
     viewedVersion: "0.89.0"
@@ -35,5 +42,8 @@
       "source.gfm"
       "text.git-commit"
     ]
+  "tree-view":
+    hideVcsIgnoredFiles: true
+    showOnRightSide: true
   welcome:
     showOnStartup: false


This is going to get annoying

This is going to get annoying

--- atom.symlink/config.cson
+++ atom.symlink/config.cson
@@ -1,7 +1,7 @@
 'editor':
-  'fontSize': 15
   'showIndentGuide': true
   'fontFamily': 'Menlo'
+  'fontSize': 15
 'core':
   'excludeVcsIgnoredPaths': true
   'ignoredNames': [
@@ -31,5 +31,5 @@
 'welcome':
   'showOnStartup': false
 'release-notes':
-  'viewedVersion': '0.85.0'
+  'viewedVersion': '0.89.0'
 'tree-view': {}


change settings of atom minimap

change settings of atom minimap

--- atom.symlink/config.cson
+++ atom.symlink/config.cson
@@ -26,4 +26,6 @@
   ]
 'minimap':
   'autoToggle': true
+  'displayPluginsControls': false
+  'plugins': {}
 'linter': {}


atom should autosave

atom should autosave

--- atom.symlink/config.cson
+++ atom.symlink/config.cson
@@ -3,6 +3,8 @@
     fileBlacklist: [
       "*.txt"
     ]
+  autosave:
+    enabled: true
   core:
     telemetryConsent: "limited"
     themes: [


Update Atom settings

Update Atom settings

--- atom.symlink/config.cson
+++ atom.symlink/config.cson
@@ -2,6 +2,9 @@
   "atom-beautify":
     general:
       _analyticsUserId: "3d743396-3e0b-468d-9bc6-eeae402b90de"
+  "atom-ide-ui":
+    use:
+      "atom-ide-diagnostics-ui": false
   "atom-package-deps":
     ignored: []
   core:
@@ -28,8 +31,8 @@
     openEmptyEditorOnStart: false
     telemetryConsent: "limited"
     themes: [
-      "one-light-ui"
-      "solarized-light-syntax"
+      "nord-atom-ui"
+      "nord-atom-syntax"
     ]
   editor:
     backUpBeforeSaving: true
@@ -97,6 +100,10 @@
     hideVcsIgnoredFiles: true
   welcome:
     showOnStartup: false
+".blade.html.php.text":
+  editor:
+    commentEnd: " --}}"
+    commentStart: "{{-- "
 ".html.php.text":
   editor:
     tabLength: 4


Atom: Enable SQL plugin

Atom: Enable SQL plugin

--- atom.symlink/config.cson
+++ atom.symlink/config.cson
@@ -22,7 +22,6 @@
       "language-perl"
       "language-php"
       "language-sass"
-      "language-sql"
       "autocomplete-css"
       "language-toml"
     ]


Added autocomplete="off" to TextInputView

--- core/app/backbone/factlink/text_input_view.coffee
+++ core/app/backbone/factlink/text_input_view.coffee
@@ -13,7 +13,7 @@
     placeholder: @options.placeholder

   template:
-    text: '<input name="text_input_view" type="text" value="{{text}}" class="typeahead" placeholder="{{placeholder}}">'
+    text: '<input name="text_input_view" autocomplete="off" type="text" value="{{text}}" class="typeahead" placeholder="{{placeholder}}">'

   initialize: ->
     @bindTo @model, 'change', @updateHtml, this


don't show mode for signed in/not signed in

--- core/app/backbone/views/interactor_name_view.coffee
+++ core/app/backbone/views/interactor_name_view.coffee
@@ -4,11 +4,7 @@
   template: 'fact_relations/interactor_name'

   templateHelpers: =>
-    if Factlink.Global.signed_in
-      show_links: => not @model.is_current_user()
-      name: => if @model.is_current_user()
-                'You'
-               else
-                @model.get('name')
-    else
-      show_links: false
+    name: =>
+      if @model.is_current_user() then 'You' else @model.get('name')
+    show_links: =>
+      Factlink.Global.signed_in and not @model.is_current_user()


Hide browser extension button on feed when kennisland

--- core/app/backbone/views/react_feed_selection.coffee
+++ core/app/backbone/views/react_feed_selection.coffee
@@ -26,8 +26,9 @@
           _label [ htmlFor: 'FeedChoice_Personal' ],
             'Personal'

-          _div ['feed-selection-install-extension-button'],
-            ReactInstallExtensionOrBookmarklet()
+          if !window.is_kennisland
+            _div ['feed-selection-install-extension-button'],
+              ReactInstallExtensionOrBookmarklet()

       ReactFeedActivitiesAutoLoading
         model: @state.feeds[@state.feedChoice]


improved error handling to remove the subchannel again

--- core/app/backbone/views/related_channel_view.coffee
+++ core/app/backbone/views/related_channel_view.coffee
@@ -11,5 +11,7 @@
     @options.addToCollection.add(ch)
     ch.save({},{
       success: => @$('a.follow').hide()
-      error:   -> alert('something went wrong while adding this channel')
+      error: =>
+        @options.addToCollection.remove(ch)
+        alert('something went wrong while adding this channel')
     })


Quick fix for error on rename

Quick fix for error on rename

--- containers/mixins/Parser.jsx.coffee
+++ containers/mixins/Parser.jsx.coffee
@@ -36,7 +36,7 @@
         when 'input-group'            then return @inputGroup          def, base
         when 'dynamic-def'            then return @dynamicDefinition   def, base
         when 'nested-form-group'      then return @nestedFormGroup     def, base
-        when 'nested-field-group'     then return @nestedGroup    def, data, base
+        when 'nested-field-group'     then return @nestedFieldGroup    def, data, base
         when 'markdown-field'         then return @markdown       def, data, base
         when 'hidden-field'           then return @hidden         def, data, base
         when 'text-field'             then return @text           def, data, base
@@ -216,8 +216,8 @@
       newData.push({})
       @props.onDataChanged(dataKey, newData)

-  nestedGroup: (def, data, base) ->
-    Controls.NestedGroup
+  nestedFieldGroup: (def, data, base) ->
+    Controls.NestedFieldGroup
       displayName   : def.displayName
       dataKey       : @generateDataKey(def.dataKey, base)
       formDef       : def


Moderation: Change synonym labels to parent

--- client/Social/Topics/AppController.coffee
+++ client/Social/Topics/AppController.coffee
@@ -104,7 +104,7 @@
           @updateTopic topicItem
         KD.getSingleton('mainController').on "TopicItemDeleteClicked", (topicItem)=>
           @deleteTopic topicItem
-        KD.getSingleton('mainController').on "TopicItemSynonymClicked", (topicItem)=>
+        KD.getSingleton('mainController').on "TopicItemSetParentClicked", (topicItem)=>
           @setSynonymTopic topicItem

     @createFeed mainView, loadFeed
@@ -140,8 +140,9 @@

   setSynonymTopic:(topicItem) ->
     topic = topicItem.getData()
+    {synonym: parent} = topic
     modal = new KDModalViewWithForms
-      title                       : "Set Topic Synonym for #{topic.title}"
+      title                       : "Set Parent Topic for #{topic.title}"
       height                      : "auto"
       cssClass                    : "compose-message-modal"
       width                       : 779
@@ -165,7 +166,7 @@
                   modal.destroy()
             fields                :
               Synonym             :
-                label             : "Synonym"
+                label             : "Parent"
                 type              : "hidden"
             callback              : (formData) =>
               showStatus = (status) =>


login: failure notice section

login: failure notice section

--- client/landing/Login/AppView.coffee
+++ client/landing/Login/AppView.coffee
@@ -181,6 +181,9 @@
       domId    : "invite-recovery-notification-bar"
       cssClass : "invite-recovery-notification-bar hidden"
       partial  : "..."
+
+    @failureNotice = new KDCustomHTMLView
+      cssClass     : "failure-notice hidden"

     KD.getSingleton("mainController").on "landingSidebarClicked", => @unsetClass 'landed'

@@ -255,6 +258,7 @@

         {{> @resendForm}}

+      {{> @failureNotice}}

{{> @goToRecoverLink}}

{{> @goToRegisterLink}}•{{> @backToLoginLink}}

@@ -551,6 +555,10 @@
         when "resendEmail"
           @$('.flex-wrapper').addClass 'one'
           @resendForm.usernameOrEmail.input.setFocus()
+        when "failureNotice"
+          @$('.flex-wrapper').addClass 'one'
+          @failureNotice.show()
+          @github.hide()

   getRouteWithEntryPoint:(route)->
     {entryPoint} = KD.config
@@ -575,3 +583,11 @@
       new KDNotificationView
         title   : err.message
         duration: 1000
+
+  setFailureNotice: ({cssClass, title, message}) ->
+    @failureNotice.setClass cssClass  if cssClass
+    @failureNotice.updatePartial \
+      """
+      #{title}
+
#{message}

+      """


store floor in cookie

store floor in cookie

--- app/javascript/works.coffee
+++ app/javascript/works.coffee
@@ -42,6 +42,8 @@
   d.setHours(24)
   lastLocation = $("form#new_work input#work_location").val();
   docCookies.setItem("lastLocation", lastLocation, d)
+  lastLocationFloor = $("form#new_work input#work_location_floor").val();
+  docCookies.setItem("lastLocationFloor", lastLocationFloor, d)
 )

 window.addEventListener "load", (event)->
@@ -51,10 +53,12 @@
     show_or_hide_selected_works()
   , 500)
   $("form#new_work input#work_location").val(docCookies.getItem("lastLocation"))
+  $("form#new_work input#work_location_floor").val(docCookies.getItem("lastLocationFloor"))

 document.addEventListener "turbolinks:load", (event)->
   $("form#new_work input#work_location").val(docCookies.getItem("lastLocation"))
+  $("form#new_work input#work_location_floor").val(docCookies.getItem("lastLocationFloor"))
   show_or_hide_selected_works()
   show_offline_stored_count_message()


zoom getting closer to working (still breaks when slider approaches right bound!); outliers needs fixing

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -81,11 +81,6 @@
     #   @ctx.font = '10pt Arial'
     #   @ctx.textAlign = 'left'
     #   @ctx.fillText( @toDataYCoord((-yClick+@canvas.height)).toFixed(4), 15, yClick+5 ) # don't forget to flip y-axis values
-
-
-  toggleOutliers: ->
-    # @processLightcurve()
-

   processLightcurve: (removeOutliers=false) ->


fix call order

fix call order

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -163,11 +163,11 @@
     e.preventDefault()
     window.removeEventListener 'mousemove', @onMouseMove
     window.removeEventListener 'mouseup', @onMouseUp
+    @canvasGraph.marks.add(@) unless (@ in @canvasGraph.marks.all)
+    @canvasGraph.marks.remove(@) if e.target.className is "top-bar"
     for mark in @canvasGraph.marks.all
       mark.dragging = false
       mark.moving = false
-    @canvasGraph.marks.add(@) unless (@ in @canvasGraph.marks.all)
-    @canvasGraph.marks.remove(@) if e.target.className is "top-bar"

   toCanvasXPoint: (e) -> e.pageX - @canvas.getBoundingClientRect().left - window.scrollX


loop through indexes for new data

loop through indexes for new data

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -10,6 +10,8 @@

     @largestX = Math.max @data.x...
     @largestY = Math.max @data.y...
+
+    @arrayLength = Math.min @data.x.length, @data.y.length

     @mirrorVertically()

@@ -29,8 +31,8 @@
     @xMin = xMin
     @xMax = xMax
     @clearCanvas()
-    for point, i in @data.x
-      x = ((point - xMin) / (xMax - xMin)) * @canvas.width
+    for i in [0...@arrayLength]
+      x = ((+@data.x[i] - xMin) / (xMax - xMin)) * @canvas.width
       y = ((+@data.y[i] - @largestY) / (@smallestY - @largestY)) * @canvas.height
       @ctx.fillStyle = "#fff"
       @ctx.fillRect(x, y,2,2)


Temporarily pass dashboard ID along with tool creation.

--- app/models/dashboard.coffee
+++ app/models/dashboard.coffee
@@ -25,7 +25,8 @@
     response

   createTool: (toolType) =>
-    @tools.add
+    @tools.add
+      dashboard_id: @get 'id'
       type: toolType

   toJSON: ->


Highlights bugfixes + beginning remixing.

(buggy)

--- coffee/src/shells/highlights_shell.coffee
+++ coffee/src/shells/highlights_shell.coffee
@@ -282,7 +282,7 @@
           clipView.inputView.progress @_progress

       # inactivate all other highlights when one comes active
-      clipView.on 'ClickSelect:Active', =>
+      clipView.on 'ClipSelect:Active', (clipView) =>
         _.each @highlightViews, (highlightView) =>
           unless highlightView is clipView
             highlightView.toggleActive false


Remove extra database call in show_map

Remove extra database call in show_map


시간 테스트

시간 테스트


new Framer Studio version check, loads images from server if not locally available

--- framer/Components/DeviceComponent.coffee
+++ framer/Components/DeviceComponent.coffee
@@ -303,7 +303,12 @@

 		# If we're running Framer Studio and have local files, we'd like to use those
 		if Utils.isFramerStudio() and window.FramerStudioInfo
-			resourceUrl = window.FramerStudioInfo.deviceImagesUrl
+
+			if @_device.minStudioVersion and Utils.framerStudioVersion() >= @_device.minStudioVersion or !@_device.minStudioVersion
+
+				if @_device.maxStudioVersion and Utils.framerStudioVersion() <= @_device.maxStudioVersion or !@_device.maxStudioVersion
+
+					resourceUrl = window.FramerStudioInfo.deviceImagesUrl

 		# We'd like to use jp2 if possible, or check if we don't for this specific device
 		if Utils.isJP2Supported() and @_device.deviceImageJP2 is true
@@ -619,7 +624,7 @@
 		resourceUrl = "//resources.framerjs.com/static/DeviceResources"

 		# If we're running Framer Studio and have local files, we'd like to use those
-		if Utils.isFramerStudio() and window.FramerStudioInfo
+		if Utils.isFramerStudio() and window.FramerStudioInfo and Utils.framerStudioVersion() >= newDeviceMinVersion
 			resourceUrl = window.FramerStudioInfo.deviceImagesUrl

 		# We'd like to use jp2 if possible, or check if we don't for this specific device
@@ -630,7 +635,10 @@

 ###########################################################################
-# NEW DEVICE CONFIGURATIONS
+# DEVICE CONFIGURATIONS
+
+newDeviceMinVersion = 51
+oldDeviceMaxVersion = 50

 iPadAir2BaseDevice =
 	deviceImageWidth: 1856
@@ -639,6 +647,7 @@
 	screenWidth: 1536
 	screenHeight: 2048
 	deviceType: "phone"
+	minStudioVersion: newDeviceMinVersion

 iPadMini4BaseDevice =
 	deviceImageWidth: 1936
@@ -647,6 +656,7 @@
 	screenWidth: 1536
 	screenHeight: 2048
 	deviceType: "phone"
+	minStudioVersion: newDeviceMinVersion

 iPadProBaseDevice =
 	deviceImageWidth: 2448
@@ -655,6 +665,7 @@
 	screenWidth: 2048
 	screenHeight: 2732
 	deviceType: "phone"
+	minStudioVersion: newDeviceMinVersion

 iPhone6BaseDevice =
 	deviceImageWidth: 874
@@ -663,6 +674,7 @@
 	screenWidth: 750
 	screenHeight: 1334
 	deviceType: "phone"
+	minStudioVersion: newDeviceMinVersion
 	hands:
 		"iphone-hands-2":
 			width:  2400
@@ -678,6 +690,7 @@
 	screenWidth: 1242
 	screenHeight: 2208
 	deviceType: "phone"
+	minStudioVersion: newDeviceMinVersion
 	hands:
 		"iphone-hands-2":
 			width:  3987
@@ -693,6 +706,7 @@
 	screenWidth: 640
 	screenHeight: 1136
 	deviceType: "phone"
+	minStudioVersion: newDeviceMinVersion
 	hands:
 		"iphone-hands-2":
 			width:  2098
@@ -710,6 +724,7 @@
 	screenWidth: 640
 	screenHeight: 1136
 	deviceType: "phone"
+	minStudioVersion: newDeviceMinVersion
 	hands:
 		"iphone-hands-2":
 			width:  2098
@@ -727,6 +742,7 @@
 	screenWidth: 768
 	screenHeight: 1280
 	deviceType: "phone"
+	minStudioVersion: newDeviceMinVersion
 	hands:
 		"iphone-hands-2":
 			width:  2362
@@ -744,6 +760,7 @@
 	screenWidth: 1080
 	screenHeight: 1920
 	deviceType: "phone"
+	minStudioVersion: newDeviceMinVersion
 	hands:
 		"iphone-hands-2":
 			width:  3292
@@ -761,6 +778,7 @@
 	screenWidth: 1440
 	screenHeight: 2560
 	deviceType: "phone"
+	minStudioVersion: newDeviceMinVersion
 	hands:
 		"iphone-hands-2":
 			width:  4304
@@ -778,6 +796,7 @@
 	screenWidth: 1536
 	screenHeight: 2048
 	deviceType: "phone"
+	minStudioVersion: newDeviceMinVersion

 HTCa9BaseDevice =
 	deviceImageWidth: 1252
@@ -786,6 +805,7 @@
 	screenWidth: 1080
 	screenHeight: 1920
 	deviceType: "phone"
+	minStudioVersion: newDeviceMinVersion
 	hands:
 		"iphone-hands-2":
 			width:  3436
@@ -803,6 +823,7 @@
 	screenWidth: 1080
 	screenHeight: 1920
 	deviceType: "phone"
+	minStudioVersion: newDeviceMinVersion
 	hands:
 		"iphone-hands-2":
 			width:  3436
@@ -820,6 +841,7 @@
 	screenWidth: 1440
 	screenHeight: 2560
 	deviceType: "phone"
+	minStudioVersion: newDeviceMinVersion
 	hands:
 		"iphone-hands-2":
 			width:  4494
@@ -837,6 +859,7 @@
 	screenWidth: 1440
 	screenHeight: 2560
 	deviceType: "phone"
+	minStudioVersion: newDeviceMinVersion
 	hands:
 		"iphone-hands-2":
 			width:  4279
@@ -854,6 +877,7 @@
 	screenWidth: 312
 	screenHeight: 390
 	backgroundColor: "black"
+	minStudioVersion: newDeviceMinVersion

 AppleWatch38Device =
 	deviceImageWidth: 472
@@ -862,6 +886,7 @@
 	screenWidth: 272
 	screenHeight: 340
 	backgroundColor: "black"
+	minStudioVersion: newDeviceMinVersion

 AppleWatch38BlackLeatherDevice =
 	deviceImageWidth: 472
@@ -870,6 +895,7 @@
 	screenWidth: 272
 	screenHeight: 340
 	backgroundColor: "black"
+	minStudioVersion: newDeviceMinVersion

 ###########################################################################
 # OLD DEVICE CONFIGURATIONS
@@ -881,12 +907,14 @@
 	screenWidth: 750
 	screenHeight: 1334
 	deviceType: "phone"
+	maxStudioVersion: oldDeviceMaxVersion

 old_iPhone6BaseDeviceHand = _.extend {}, old_iPhone6BaseDevice,
 	deviceImageWidth: 1988
 	deviceImageHeight: 2368
 	deviceImageJP2: true
 	paddingOffset: -150
+	maxStudioVersion: oldDeviceMaxVersion

 old_iPhone6PlusBaseDevice =
 	deviceImageWidth: 1460
@@ -895,13 +923,14 @@
 	screenWidth: 1242
 	screenHeight: 2208
 	deviceType: "phone"
+	maxStudioVersion: oldDeviceMaxVersion

 old_iPhone6PlusBaseDeviceHand = _.extend {}, old_iPhone6PlusBaseDevice,
 	deviceImageWidth: 3128
 	deviceImageHeight: 3487
 	deviceImageJP2: true
 	paddingOffset: -150
-
+	maxStudioVersion: oldDeviceMaxVersion

 old_iPhone5BaseDevice =
 	deviceImageWidth: 780
@@ -910,6 +939,7 @@
 	screenWidth: 640
 	screenHeight: 1136
 	deviceType: "phone"
+	maxStudioVersion: oldDeviceMaxVersion
 	# keyboards:
 	# 	portrait:
 	# 		image:  "ios-keyboard.png"
@@ -925,6 +955,7 @@
 	deviceImageHeight: 2234
 	deviceImageJP2: true
 	paddingOffset: -200
+	maxStudioVersion: oldDeviceMaxVersion

 old_iPhone5CBaseDevice =
 	deviceImageWidth: 776
@@ -933,6 +964,7 @@
 	screenWidth: 640
 	screenHeight: 1136
 	deviceType: "phone"
+	maxStudioVersion: oldDeviceMaxVersion
 	# keyboards:
 	# 	portrait:
 	# 		image:  "ios-keyboard.png"
@@ -948,7 +980,7 @@
 	deviceImageHeight: 2244
 	deviceImageJP2: true
 	paddingOffset: -200
-
+	maxStudioVersion: oldDeviceMaxVersion

 old_iPadMiniBaseDevice =
 	deviceImageWidth: 872
@@ -957,13 +989,14 @@
 	screenWidth: 768
 	screenHeight: 1024
 	deviceType: "tablet"
+	maxStudioVersion: oldDeviceMaxVersion

 old_iPadMiniBaseDeviceHand = _.extend {}, old_iPadMiniBaseDevice,
 	deviceImageWidth: 1380
 	deviceImageHeight: 2072
 	deviceImageJP2: true
 	paddingOffset: -120
-
+	maxStudioVersion: oldDeviceMaxVersion

 old_iPadAirBaseDevice =
 	deviceImageWidth: 1769
@@ -972,13 +1005,14 @@
 	screenWidth: 1536
 	screenHeight: 2048
 	deviceType: "tablet"
+	maxStudioVersion: oldDeviceMaxVersion

 old_iPadAirBaseDeviceHand = _.extend {}, old_iPadAirBaseDevice,
 	deviceImageWidth: 4744
 	deviceImageHeight: 4101
 	deviceImageJP2: true
 	paddingOffset: -120
-
+	maxStudioVersion: oldDeviceMaxVersion

 old_Nexus5BaseDevice =
 	deviceImageWidth: 1208
@@ -987,12 +1021,14 @@
 	screenWidth: 1080
 	screenHeight: 1920
 	deviceType: "phone"
+	maxStudioVersion: oldDeviceMaxVersion

 old_Nexus5BaseDeviceHand = _.extend {}, old_Nexus5BaseDevice, # 2692 × 2996
 	deviceImageWidth: 2692
 	deviceImageHeight: 2996
 	deviceImageJP2: true
 	paddingOffset: -120
+	maxStudioVersion: oldDeviceMaxVersion

 old_Nexus9BaseDevice =
 	deviceImageWidth: 1733
@@ -1001,6 +1037,7 @@
 	screenWidth: 1536
 	screenHeight: 2048
 	deviceType: "tablet"
+	maxStudioVersion: oldDeviceMaxVersion

 old_AppleWatch42Device =
 	deviceImageWidth: 552
@@ -1008,6 +1045,7 @@
 	deviceImageJP2: true
 	screenWidth: 312
 	screenHeight: 390
+	maxStudioVersion: oldDeviceMaxVersion

 old_AppleWatch38Device =
 	deviceImageWidth: 508
@@ -1015,6 +1053,7 @@
 	deviceImageJP2: true
 	screenWidth: 272
 	screenHeight: 340
+	maxStudioVersion: oldDeviceMaxVersion

 Devices =

@@ -1135,6 +1174,7 @@
 		deviceImageWidth: 1136
 		deviceImageHeight: 760
 		deviceImageJP2: true
+		maxStudioVersion: oldDeviceMaxVersion
 	"desktop-safari-1280-800":
 		deviceType: "browser"
 		name: "Desktop Safari 1280 x 800"
@@ -1143,6 +1183,7 @@
 		deviceImageWidth: 1392
 		deviceImageHeight: 960
 		deviceImageJP2: true
+		maxStudioVersion: oldDeviceMaxVersion
 	"desktop-safari-1440-900":
 		deviceType: "browser"
 		name: "Desktop Safari 1440 x 900"
@@ -1151,6 +1192,7 @@
 		deviceImageWidth: 1552
 		deviceImageHeight: 1060
 		deviceImageJP2: true
+		maxStudioVersion: oldDeviceMaxVersion

 	# iPhone 6
 	"iphone-6-spacegray": _.clone(old_iPhone6BaseDevice)


Fix localStorage usage

Fix localStorage usage

--- Web/js/classes/notification-center.coffee
+++ Web/js/classes/notification-center.coffee
@@ -11,7 +11,7 @@
 	constructor: () ->
 		@denied = false;
 		@hasPermission = false;
-		if window.localStorage.get( 'notifications-enabled' )
+		if window.localStorage.getItem 'notifications-enabled'
 			@getPermission()
 			# Just in case we're using an older ver of the speck, ask on the first click as well.
 			clickus = () =>
@@ -62,5 +62,5 @@

 	enableNotifications: () ->
 		@getPermission();
-		window.localStorage.set( 'notifications-enabled', true )
+		window.localStorage.setItem 'notifications-enabled', true


:white_check_mark: Add test for widget element property

--- specs/units/widgets/widgets.spec.coffee
+++ specs/units/widgets/widgets.spec.coffee
@@ -23,10 +23,11 @@

         widget = agt.widgets.widgetsFor(element, 'dummy')

-      it 'calls the widget method', ->
+      it 'calls the widget method and creates a widget', ->
         expect(spy).toHaveBeenCalled()
         expect(spy.calls.first().args[0]).toBe(element)
         expect(spy.calls.first().object).toBe(widget)
+        expect(widget.element).toBe(element)

       it 'activates the widget object', ->
         expect(widget.active).toBeTruthy()


tasks: tried to address sync problem by changing sync event and cleaning up scope of variables; however, I doubt this is going to fix the problem.

--- salvus/page/tasks.coffee
+++ salvus/page/tasks.coffee
@@ -36,7 +36,9 @@
 HEADINGS    = ['custom', 'description', 'due', 'last-edited']
 HEADING_MAP = {custom:'position', description:'desc', due:'due_date', 'last-edited':'last_edited'}

-SPECIAL_PROPS = {element:true, changed:true, last_desc:true}
+SPECIAL_PROPS = {element:true, changed:true, last_desc:true, desc_timer:true, desc_last_sync:true}
+
+MIN_TIME = 1000 # minimum time in ms between sync events

 sortnum = (a,b) -> a - b

@@ -923,37 +925,37 @@
         elt.find(".CodeMirror-hscrollbar").remove()
         elt.find(".CodeMirror-vscrollbar").remove()

-        last_sync = undefined
-        min_time = 1500
+        task.desc_last_sync = undefined

         task.last_desc = task.desc  # initialize last_desc, in case we get an update before ever sync'ing.
         sync_desc = () =>
-            last_sync      = misc.mswalltime()
+            task.desc_last_sync = misc.mswalltime()
             desc           = cm.getValue()
             task.last_desc = desc  # update current description before syncing.
             task.desc      = desc
-            @display_desc(task) # update the preview
+            @display_desc(task)    # update the preview
             task.last_edited = (new Date()) - 0
             @db.update
                 set   : {desc    : task.desc, last_edited : task.last_edited}
                 where : {task_id : task.task_id}
             @set_dirty()

-        timer = undefined
-        cm.on 'change', () =>
+        task.desc_timer = undefined
+        cm.on 'changes', () =>
             t = misc.mswalltime()
-            if not last_sync?
+            if not task.desc_last_sync?
                 sync_desc()
             else
-                if t - last_sync >= min_time
+                if t - task.desc_last_sync >= MIN_TIME
                     sync_desc()
                 else
-                    if not timer?
+                    if not task.desc_timer?
                         f = () ->
-                            timer = undefined
-                            if misc.mswalltime() - last_sync >= min_time
+                            task.desc_timer = undefined
+                            if misc.mswalltime() - task.desc_last_sync >= MIN_TIME
                                 sync_desc()
-                        timer = setTimeout(f, min_time - (t - last_sync))
+                        task.desc_timer = setTimeout(f, MIN_TIME - (t - task.desc_last_sync))
+
         cm.on 'focus', () ->
             currently_focused_editor = cm
             $(cm.getWrapperElement()).addClass('salvus-new-task-cm-editor-focus')


Fix favorite button

Fix favorite button

--- server/app/assets/javascript/fav.coffee
+++ server/app/assets/javascript/fav.coffee
@@ -33,7 +33,7 @@
       counter.val(data)

 checkButton = (btn, path) ->
-  if location.protocol != "https"
+  if location.protocol != 'https:'
     btn.attr('disabled', 'disabled')
   else
     $.get('/rest/v1/is_faved/' + encodeURIComponent(path))


modified comments

modified comments

--- src/coffee/util.coffee
+++ src/coffee/util.coffee
@@ -8,19 +8,19 @@
    * @param {Object} obj
    * @type boolean
   ###
-  isObject:( obj )-> return typeof obj is "object"
+  isObject:( obj )-> typeof obj is "object"

   ###
    * @param {Object} obj
    * @type boolean
   ###
-  isArray:( obj )-> return Object.prototype.toString.call(obj) == "[object Array]"
+  isArray:( obj )-> Object.prototype.toString.call(obj) == "[object Array]"

   ###
    * @param {function} func
    * @type boolean
   ###
-  isFunction:( func )-> return typeof func is "function"
+  isFunction:( func )-> typeof func is "function"

   ###
    * @param {Object} obj
@@ -36,8 +36,8 @@
            length is 0 or ( typeof length is "number" && length > 0 && ( length - 1 ) of obj )

   ###
-   *
-   *
+   * @param {Object} obj
+   * @param {function} callback
   ###
   each:( obj, callback )->
     value = null


Proper handling `base_url` property.

Proper handling `base_url` property.

--- src/cli/toaster/toast.coffee
+++ src/cli/toaster/toast.coffee
@@ -92,6 +92,10 @@
       msg += '\nCheck your `toaster.coffee` config file.'
       return error msg

+    # ...: main - mandatory
+    if config.base_url?
+      config.base_url += '/' if config.base_url.slice -1 isnt '/'
+
     # ...: dirs - mandatory
     if config.dirs is null or config.dirs.length is 0
       msg = 'You need to inform at least one dir or source files.'


fix trivial mistake

fix trivial mistake

--- salvus/storage_server.coffee
+++ salvus/storage_server.coffee
@@ -63,7 +63,7 @@
     winston.debug("_smc_storage_no_queue: #{misc.to_json(opts.args)}")
     misc_node.execute_code
         command : "smc_storage.py"
-        args    : args
+        args    : opts.args
         timeout : opts.timeout
         cb      : (err, output) =>
             if err


outlines for other tests

outlines for other tests

--- test/services/budget_loader_spec.coffee
+++ test/services/budget_loader_spec.coffee
@@ -31,5 +31,13 @@
 			budget_loader.loadFromRootScope()
 			expect(budget_loader.scope.currentBudgetId).to.eq(5)

+  describe 'defaultToFirstBudget', ->
+    it 'does nothing if scope.budgets empty'
+    it 'sets rootScope.currentBudget to the first budget'
+    it 'sets scope.currentbudgetId to the budget id'
+
+  describe 'setBudget', ->
+    it 'sets rootScope.currentBudget to the first budget with matching id'

+


Register error handler before converting.

--- src/coffee/csvmapping.coffee
+++ src/coffee/csvmapping.coffee
@@ -91,11 +91,11 @@
     deferred = Q.defer()

     Csv().from.string(csvString)
+    .on 'error', (error) ->
+      deferred.reject error
+
     .to.array (data) ->
       deferred.resolve data[0]
-
-    .on 'error', (error) ->
-      deferred.reject error

     deferred.promise


Add resetStatistics to team

Add resetStatistics to team

--- src/collections/teams.coffee
+++ src/collections/teams.coffee
@@ -94,6 +94,20 @@

   @collections.teams.exec('updateTimeZone', options)

+exports.resetStatistics = (teamId, callback) ->
+  unless teamId
+    throw new TSArgsError 'teamsnap.resetStatistics',
+      "`teamId` must be provided"
+  if @isItem teamPreferencesId, 'teamId'
+    teamId = teamId.id
+  if not @isId teamId
+    throw new TSArgsError 'teamsnap.resetStatistics',
+      "`teamId` must be a valid id"
+
+  params = teamId: teamId
+  @collections.teams.exec('resetStatistics', params)
+  .callback callback
+
 # Converts memberId or memberIds into an array
 cleanArray = (obj, prop) ->
   plural = prop + 's'


Delete comments in gulpfile

Delete comments in gulpfile


修正xRender的顶层节点不支持c-repeat的BUG

修正xRender的顶层节点不支持c-repeat的BUG

--- src/core/dom-template.coffee
+++ src/core/dom-template.coffee
@@ -130,6 +130,12 @@
 		finally
 			cola.currentScope = oldScope

+	# 处理xRender的顶层节点中包含c-repeat的情况
+	if dom?.getAttribute("c-repeat")
+		documentFragment = document.createDocumentFragment()
+		documentFragment.appendChild(dom)
+		dom = null
+
 	if dom
 		cola._renderDomTemplate(dom, model, context)
 	else if documentFragment


update regex from Sublime Text package

update regex from Sublime Text package

--- grammars/innosetup.cson
+++ grammars/innosetup.cson
@@ -3,13 +3,13 @@
   "isl"
 ]
 name: "Inno Setup"
-comment: "https://github.com/idleberg/InnoSetup-Sublime-Text"
+comment: "https://github.com/idleberg/atom-language-inno"
 patterns: [
   {
     captures:
       "1":
         name: "punctuation.definition.comment.inno"
-    match: "^\\s*(;|#\s).*$\\n?"
+    match: "^\\s*(;|#).*$"
     name: "comment.line.semicolon.inno"
   }
   {
@@ -56,5 +56,8 @@
     match: "\\b(aa|ab|ae|af|ak|am|an|ar|as|av|ay|az|ba|be|bg|bh|bi|bm|bn|bo|br|bs|ca|ce|ch|co|cr|cs|cu|cv|cy|da|de|dv|dz|ee|el|en|eo|es|et|eu|fa|ff|fi|fj|fo|fr|fy|ga|gd|gl|gn|gu|gv|ha|he|hi|ho|hr|ht|hu|hy|hz|ia|id|ie|ig|ii|ik|io|is|it|iu|ja|jv|ka|kg|ki|kj|kk|kl|km|kn|ko|kr|ks|ku|kv|kw|ky|la|lb|lg|li|ln|lo|lt|lu|lv|mg|mh|mi|mk|mk|ml|mn|mr|ms|mt|my|na|nb|nd|ne|ng|nl|nn|no|nr|nv|ny|oc|oj|om|or|os|pa|pi|pl|ps|pt|qt|qu|rg|rm|rn|ro|ru|rw|sa|sc|sd|se|sg|si|sk|sl|sm|sn|so|sq|sr|ss|st|su|sv|sw|ta|te|tg|th|ti|tk|tl|tn|to|tr|ts|tt|tw|ty|ug|uk|ur|uz|ve|vi|vo|wa|wo|xh|yi|yo|zh)(?:\\.\\w)?\\b"
     name: "variable.language.inno"
   }
+  {
+    include: "source.pascal"
+  }
 ]
 scopeName: "source.inno"


Make setLogLevel() available again on the eventric main object

--- src/eventric.coffee
+++ src/eventric.coffee
@@ -5,6 +5,8 @@
 Projection = require './projection'
 Context = require './context'
 uidGenerator = require './uid_generator'
+logger = require './logger'
+

 class Eventric

@@ -73,6 +75,10 @@
     uidGenerator.generateUid()

+  setLogLevel: (logLevel) ->
+    logger.setLogLevel logLevel
+
+
   _handleRemoteRPCRequest: (request, callback) =>
     context = @getContext request.contextName
     if not context


fixed the date issue

fixed the date issue

--- src/facebook.coffee
+++ src/facebook.coffee
@@ -3,6 +3,8 @@

 class FacebookStatus extends Status
     constructor: (id, username, avatar, date, text, raw, @userid) ->
+        date = new Date().setISO8601(date)
+
         super id, username, avatar, date, text, raw

     getStreamUrl: -> "http://facebook.com/profile.php?id=#{@userid}"
@@ -33,7 +35,7 @@
         id = match and match[1]
         link = data.source or data.link
         text = data.message ? ''
-        if link? then text += """ #{data.name}"""
+        if link? then text += """ #{ data.name or 'link' }"""

         new FacebookStatus id, data.from.name, null, data.created_time, text, data, data.from.id

@@ -62,4 +64,36 @@
     pattern: /^FB:(.*)$/
     fun: (_, name) -> new FacebookTimeline name

+`Date.prototype.setISO8601 = function(dString){
+    var regexp = /(\d\d\d\d)(-)?(\d\d)(-)?(\d\d)(T)?(\d\d)(:)?(\d\d)(:)?(\d\d)(\.\d+)?(Z|([+-])(\d\d)(:)?(\d\d))/,
+        d = dString.match(regexp);
+
+    if (d != null) {
+        var offset = 0;
+        this.setUTCDate(1);
+        this.setUTCFullYear(parseInt(d[1],10));
+        this.setUTCMonth(parseInt(d[3],10) - 1);
+        this.setUTCDate(parseInt(d[5],10));
+        this.setUTCHours(parseInt(d[7],10));
+        this.setUTCMinutes(parseInt(d[9],10));
+        this.setUTCSeconds(parseInt(d[11],10));
+
+        if (d[12]) {
+            this.setUTCMilliseconds(parseFloat(d[12]) * 1000);
+        } else {
+            this.setUTCMilliseconds(0);
+        }
+
+        if (d[13] != 'Z') {
+            offset = (d[15] * 60) + parseInt(d[17],10);
+            offset *= ((d[14] == '-') ? -1 : 1);
+            this.setTime(this.getTime() - offset * 60 * 1000);
+        }
+    } else {
+        this.setTime(Date.parse(dString));
+    }
+
+    return this;
+};`
+
 extend Chorus, {FacebookStatus, FacebookTimeline}


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -29,8 +29,7 @@
       channels: channels

       equals: (other) ->
-        return
-          other.channels.r == channels.r &&
+        return other.channels.r == channels.r &&
           other.channels.g == channels.g &&
           other.channels.b == channels.b &&
           other.channels.a = channels.a


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -872,11 +872,26 @@
     toString: ->
       "rgba(#{@r}, #{@g}, #{@b}, #{@a})"

+    ###*
+    returns string rgba representation of the color.
+
+

+    red = Color('#ff0000')
+
+    red.toString()
+    # => 'rgba(255, 0, 0, 1)'
+

+
+    @name toString
+    @methodOf Color#
+
+    @returns {String} The rgba string representation of the color
+    ###
     transparentize: (amount) ->
-      @copy().transparentize$(amount)
+      @copy().transparentize$(amount)

     transparentize$: (amount) ->
-      @a -= amount
+      @a = (@a - amount).clamp(0, 1)

       return this


cleaning up array constructions

cleaning up array constructions

--- src/color.coffee
+++ src/color.coffee
@@ -26,12 +26,10 @@
   parseRGB = (colorString) ->
     return undefined unless bits = rgbParser.exec(colorString)

-    return [
-      parseFloat(bits[1]).round()
-      parseFloat(bits[2]).round()
-      parseFloat(bits[3]).round()
-      if bits[4]? then parseFloat(bits[4]) else 1.0
-    ]
+    rgbMap = bits.splice(1, 3).map (channel) ->
+      parseFloat(channel).round()
+
+    return (rgbMap).concat(if bits[1]? then parseFloat(bits[1]) else 1.0)

   parseHSL = (colorString) ->
     return undefined unless bits = hslParser.exec(colorString)


made regex case insensitive

made regex case insensitive

--- src/hangouts.coffee
+++ src/hangouts.coffee
@@ -11,7 +11,7 @@
 hangoutsDomain = process.env.HUBOT_GOOGLE_HANGOUTS_DOMAIN

 module.exports = (robot) ->
-  robot.respond /hangouts?( me)?\s*"?(.*?)"?$/, (msg) ->
+  robot.respond /hangouts?( me)?\s*"?(.*?)"?$/i, (msg) ->
     return if missingEnvironment(msg)

     console.log msg.match


Remove known keys without iterating

Remove known keys without iterating

--- src/loaders/collection-loader.coffee
+++ src/loaders/collection-loader.coffee
@@ -70,7 +70,7 @@
     # Loop over all returned data types and update our local storage to represent any new data.

     results = resp['results']
-    keys = _.reject(_.keys(resp), (key) -> _.contains(knownResponseKeys, key))
+    keys = _.without(_.keys(resp), knownResponseKeys...)
     unless _.isEmpty(results)
       keys.splice(keys.indexOf(@loadOptions.name), 1) if keys.indexOf(@loadOptions.name) != -1
       keys.push(@loadOptions.name)


Removed date

Removed date

--- src/hangouts.coffee
+++ src/hangouts.coffee
@@ -16,9 +16,10 @@

     console.log msg.match
     console.log msg.message.user
-    title = "#{msg.match[2] || msg.message.user.name}-#{+new Date()}"
-    slug  = title.replace(/[^0-9a-z-]+/gi, '-')
-    msg.send "I've started a Hangout! Join here: https://plus.google.com/hangouts/_/#{hangoutsDomain}/#{slug}"
+    username = msg.message.user.name
+    slug = msg.match[2].replace(" ", "-")
+    title = "#{msg.match[2] || username}"
+    msg.send "OK #{username}, I've started a Hangout! Join here: https://plus.google.com/hangouts/_/#{hangoutsDomain}/#{title}"

   missingEnvironment = (msg) ->
     missingAnything = false


`process.argv[0]` refers to the Node process.

--- src/docco.coffee
+++ src/docco.coffee
@@ -215,7 +215,7 @@

 # Run the script.
 # For each source file passed in as an argument, generate the documentation.
-sources = process.argv.sort()
+sources = process.argv.slice(1).sort()
 if sources.length
   ensure_directory 'docs', ->
     fs.writeFile 'docs/docco.css', docco_styles


throttle, debounce for events

throttle, debounce for events

--- src/event.coffee
+++ src/event.coffee
@@ -95,6 +95,10 @@
         scan = true
         modifiers = []
         filterByKey = null
+        throttle = null
+        throttleId = null
+        debounce = null
+        debounceTime = 0

         modifier = alight.hooks.eventModifier[eventName]
         if modifier
@@ -122,6 +126,12 @@
             if k is 'noscan'
                 scan = false
                 continue
+            if k.substring(0, 8) is 'throttle'
+                throttle = Number k.substring 8
+                continue
+            if k.substring(0, 8) is 'debounce'
+                debounce = Number k.substring 8
+                continue

             modifier = alight.hooks.eventModifier[k]
             if modifier
@@ -155,19 +165,7 @@
                 getValue = ->
                     element.value

-            handler = (event) ->
-                if filterByKey
-                    if not filterByKey[event.keyCode]
-                        return
-
-                if modifiers.length
-                    env =
-                        stop: false
-                    for modifier in modifiers
-                        modifier.fn event, env
-                        if env.stop
-                            return
-
+            execute = (event) ->
                 if prevent
                     event.preventDefault()
                 if stop
@@ -183,6 +181,34 @@
                         event: event
                 if scan
                     scope.$scan()
+                return
+
+            handler = (event) ->
+                if filterByKey
+                    if not filterByKey[event.keyCode]
+                        return
+
+                if modifiers.length
+                    env =
+                        stop: false
+                    for modifier in modifiers
+                        modifier.fn event, env
+                        if env.stop
+                            return
+
+                if throttle
+                    if throttleId
+                        clearTimeout throttleId
+                    throttleId = setTimeout ->
+                        throttleId = null
+                        execute event
+                    , throttle
+                else if debounce
+                    if debounceTime < Date.now()
+                        debounceTime = Date.now() + debounce
+                        execute event
+                else
+                    execute event
                 return

             for e in eventList


videoLinkShell: pass eventhub to progressBarView

--- coffee/src/shells/videolink.shell.coffee
+++ coffee/src/shells/videolink.shell.coffee
@@ -81,6 +81,7 @@
       eventhub: @eventhub

     @progressBarView = new acorn.player.ValueSliderView
+      eventhub: @eventhub
       value: 0
       extraClasses: 'progress-bar-view'


Added a fix to reset enabledClass on nanoScroller.reset()

--- coffeescripts/jquery.nanoscroller.coffee
+++ coffeescripts/jquery.nanoscroller.coffee
@@ -657,6 +657,7 @@
           $(".nano").nanoScroller();
     ###
     reset: ->
+      options = @options
       if @iOSNativeScrolling
         @contentHeight = @content.scrollHeight
         return
@@ -717,10 +718,12 @@
           @pane.outerHeight(true) >= content.scrollHeight and contentStyleOverflowY isnt SCROLL)
         do @pane.hide
         @isActive = false
+        @$el.removeClass options.enabledClass
       else if @el.clientHeight is content.scrollHeight and contentStyleOverflowY is SCROLL
         do @slider.hide
       else
         do @slider.show
+        @$el.addClass options.enabledClass

       # allow the pane element to stay visible
       @pane.css


bugfix: log out of other tabs when the cookie changes

https://app.asana.com/0/1030072860077/6432131515387

--- core/KDWindowController.coffee
+++ core/KDWindowController.coffee
@@ -102,6 +102,15 @@

     window.addEventListener 'beforeunload', @bound "beforeUnload"

+    @utils.repeat 1000, do =>
+      cookie = $.cookie 'clientId'
+      =>
+        if KD.isLoggingIn then KD.isLoggingIn = no
+        else if cookie? and cookie isnt $.cookie 'clientId'
+          window.removeEventListener 'beforeunload', @bound 'beforeUnload'
+          window.location.replace '/'
+        cookie = $.cookie 'clientId'
+
   addUnloadListener:(listener)-> @unloadListeners.push listener

   beforeUnload:(event)->
@@ -110,10 +119,10 @@

     # all the listeners make their checks if it is safe or not to reload the page
     # they either return true or false if any of them returns false we intercept reload
-    if no in votes = (listener() for listener in @unloadListeners)
-      msg = "Please make sure that you saved all your work."
-      event.returnValue = msg
-      return msg
+
+    for listener in @unloadListeners
+      if listener() is off
+        return "Please make sure that you saved all your work."

   setDragInAction:(@dragInAction = no)->
     $('body')[if @dragInAction then "addClass" else "removeClass"] "dragInAction"


Undo select node for instpeciton when dragged in. [#102184884]

https://www.pivotaltracker.com/story/show/102184884

--- src/code/views/graph-view.coffee
+++ src/code/views/graph-view.coffee
@@ -81,7 +81,6 @@
         title: title
         image: paletteItem.image
     @props.graphStore.editNode(node.key)
-    @props.selectionManager.selectForInspection node

   getInitialState: ->
     nodes: []


Add image events.

Add image events.

--- src/coffee/kopi/ui/images.coffee
+++ src/coffee/kopi/ui/images.coffee
@@ -17,6 +17,9 @@
   TODO Do not subtitute image when page is scrolling
   ###
   class Image extends widgets.Widget
+
+    this.IMAGE_LOAD_EVENT = "imageload"
+    this.IMAGE_ERROR_EVENT = "imageerror"

     this.widgetName "Image"

@@ -91,6 +94,7 @@
             .height(options.height)
             .width(options.width)
             .attr(SRC, self._src)
+          self.emit(cls.IMAGE_LOAD_EVENT)
         img.onerror = (e) ->
           element.removeClass(loadingClass)
           if options.fallbackSrc
@@ -98,6 +102,7 @@
             image.attr(SRC, options.fallbackSrc)
           else
             image.attr(SRC, self._src)
+          self.emit(cls.IMAGE_ERROR_EVENT)
         img.src = self._src
       else
         self._image.attr SRC, self._src


Fix deploy delete logging

Fix deploy delete logging

--- deployment/deleteFilesFromS3.coffee
+++ deployment/deleteFilesFromS3.coffee
@@ -17,7 +17,7 @@
                 Bucket: project_config.AWS_BUCKET
                 Delete:
                     Objects: keys
-            SDKError.log(SDKError.colors("Deleting #{ keys.length } files from S3..."))
+            SDKError.log(SDKError.colors.grey("Deleting #{ keys.length } files from S3..."))
             s3.deleteObjects s3_options, (err) ->
                 if err?
                     throw new SDKError('deploy.s3', err)


updated & commented width calculation for ie

--- src/components/scores/index.cjsx
+++ src/components/scores/index.cjsx
@@ -68,10 +68,13 @@
     @setState({tableWidth: @tableWidth(), tableHeight: @tableHeight()})

   tableWidth: ->
+    windowEl = @_getWindowSize()
     table = React.findDOMNode(@refs.tableContainer)
-    rightPadding = 120
-    # max sets width based on window for IE, if clientWidth returns 0 on initial load.
-    Math.max(window.innerWidth - rightPadding, table.clientWidth)
+    # tableHorzSpacing is body width - table container width
+    wrap = React.findDOMNode(@refs.scoresWrap)
+    tableHorzSpacing = document.body.clientWidth - wrap.clientWidth
+    # since table returns 0 on initial load in IE, include windowEl as a fallback
+    Math.max(windowEl.width - tableHorzSpacing, table.clientWidth)

   tableHeight: ->
     windowEl = @_getWindowSize()
@@ -274,7 +277,7 @@

     if data.rows.length > 0 then students = true

-

+

         Student Scores
         {scoresExport if students}
         {if isConceptCoach then scoresNote}


Align then methods and pass arguments through to them

--- src/components/tutor-dialog.cjsx
+++ src/components/tutor-dialog.cjsx
@@ -59,11 +59,10 @@
   propTypes: _.omit(DialogProperties, 'body')

   componentDidMount: ->
+    # While unlikely, the onOk and onCancel properties could have been updated while the dialog was visible
+    # If they were we need to call their current functions
     TutorDialog.show(_.extend({}, @props, body: @props.children)).then(
-      => @props.onOk?()
-      ,
-      =>
-        @props.onCancel?()
+      ( => @props.onOk?(arguments...) ) , ( @props.onCancel?(arguments...) )
     )

   componentWillUnmount: ->
@@ -86,6 +85,7 @@
           @dialog.replaceProps(props)
         else
           div = document.body.appendChild( document.createElement('div') )
+          div.id
           @dialog = React.render(React.createElement(DetachedTutorDialog, props), div)

         @dialog


Improved unrender.

Improved unrender.

--- src/EntityUtils.coffee
+++ src/EntityUtils.coffee
@@ -397,9 +397,8 @@
   unrender: (id) ->
     df = Q.defer()
     @renderQueue.add id, =>
-      @renderingEnabledDf.promise.then Meteor.bindEnvironment =>
-        AtlasManager.unrenderEntity(id)
-        df.resolve(id)
+      AtlasManager.unrenderEntity(id)
+      df.resolve(id)
     df.promise

   show: (id) ->


Style XSS warning avoidance: second attempt

--- packages/emberella/lib/mixins/style_bindings.coffee
+++ packages/emberella/lib/mixins/style_bindings.coffee
@@ -6,6 +6,7 @@
   # This prevents anything other than a single style from being
   # set. However, this may disrupt some legitimate styles and
   # and doesn't cover all XSS attack vectors.
+  return '' unless Ember.typeOf(value) is 'string'
   value.replace /;.*$/, ''

 Ember.StyleBindingsMixin = Ember.Mixin.create
@@ -50,7 +51,8 @@
       styleTokens = styles.map (style) =>
         @createStyleString style, lookup[style]
       styleString = styleTokens.join('')
-      return new Ember.Handlebars.SafeString(styleString) unless styleString.length is 0
+      # TODO: Figure out better way to return empty style without deprecation warning
+      new Ember.Handlebars.SafeString(if (styleString.length is 0) then '' else styleString)

     # add dependents to computed property
     styleComputed.property.apply(styleComputed, properties)


Jensen is literally the worst person on earth

I hate you so much, and you hate art so much.

cc/ @jensechu -- :skull:

--- scripts/ayp.coffee
+++ scripts/ayp.coffee
@@ -71,8 +71,8 @@
     # Another one fond of aliases
     name = 'minusx'

-  if /jense/i.test(name) or /prawn/i.test(name)
-    # Has bouncer, bounces anyway
+  if /jense/i.test(name) or /prawn/i.test(name) or /sinjen/.test(name)
+    # Has bouncer, bounces anyway, and also no one likes her
     name = 'prawn'

   if /geckomuerto/i.test(name)


WIP: Build comic and build panels in a class

--- scripts/ayp.coffee
+++ scripts/ayp.coffee
@@ -95,7 +95,7 @@
   robot.respond /ayp(\s+(me)?)?\s*$/i, (msg) ->
     buffer.get 6, (err, lines) ->
       # Build a strip of AYP
-      buildComic lines, (err, image) ->
+      new AYPStrip lines, (err, image) ->
         return msg.reply "SOMETHING TERRIBLE HAPPENED: #{err}" if err

         # Save locally, upload, cleanup
@@ -118,69 +118,73 @@
               return msg.reply "Woooops! Failed to upload: #{err}" if err
               msg.reply "I made a thing: http://s3.amazonaws.com/#{AYP_AWS_BUCKET}/#{name}"

-# Build a comic and invoke the cb(err, res) with res
-# being the resulting image. `err` will be true if an error
-# is encountered.
-buildComic = (lines, cb) ->
-  buildPanels lines, (err, panels) ->
-    return cb(err, null) if err
-    loaders =
-      png: GD.openPng
-      jpg: GD.openJpeg
-      jpeg: GD.openJpeg
-
-
-    fs.readdir BG_BASE, (err, files) ->
-      cb(err, null) if err
-      files = files.filter (f) -> f[0] != '.'
-      selected = files[Math.round(Math.random() * (files.length - 1))]
-      ext = selected[-3..].toLowerCase()
-      loader = loaders[ext]
-
-      # If we pick a BG we can't load, panic
-      # TODO: Or try again a few times?
-      cb("Can't find loader for #{selected}", null) unless loader
-
-      loader path.resolve(BG_BASE, selected), (err, bg) ->
-        return cb(err, bg) if err
-        totalPadding = (AYP_PANEL_PADDING * 2)
-        left = 0
-        top = Math.round(totalPadding / 2)
-        for panel in panels
-          do (panel) ->
-            compositeImage bg, panel, Math.round(left += (totalPadding / 2)), top
-            left += panel.width # Panel width
-            left += Math.round(totalPadding / 2)
-        cb(false, bg)
-
-# Turn a set of 6 `lines` into 3 panels
-# using two lines per panel, then invokes `cb`.
-#
-# callback invoked as `cb(err, [image, image, image])`
-# Error will only be set on failure.
-buildPanels = (lines, cb) ->
-  failed = false
-  panels = [null, null, null]
-
-  # Handler for when a panel fails to build.
-  fail = (err) ->
-    cb(err, [])
-    return failed = true
-
-  # Handler for panel completion, store in the list, see if we're done,
-  # invoke callback if we are.
-  finishPanel = (err, n, panel) ->
-    # No one cares if we already lost.
-    return if failed
-    return fail(err) if err
-
-    panels[n] = panel
-    cb(false, panels) if panels.every (p) -> p
-
-  # TODO: There's a loop here, somewhere
-  buildPanel lines[0..1], (err, panel) -> finishPanel(err, 0, panel)
-  buildPanel lines[2..3], (err, panel) -> finishPanel(err, 1, panel)
-  buildPanel lines[4..5], (err, panel) -> finishPanel(err, 2, panel)
+class AYPStrip
+  constructor: (@script, @ready) ->
+    do @buildComic
+
+  # Build a comic and invoke the @ready(err, res) callback with res
+  # being the resulting image. `err` will be true if an error
+  # is encountered.
+  buildComic: =>
+    @buildPanels (err, panels) =>
+      return @ready(err, null) if err
+      loaders =
+        png: GD.openPng
+        jpg: GD.openJpeg
+        jpeg: GD.openJpeg
+
+
+      fs.readdir BG_BASE, (err, files) =>
+        @ready(err, null) if err
+        files = files.filter (f) -> f[0] != '.'
+        selected = files[Math.round(Math.random() * (files.length - 1))]
+        ext = selected[-3..].toLowerCase()
+        loader = loaders[ext]
+
+        # If we pick a BG we can't load, panic
+        # TODO: Or try again a few times?
+        @ready("Can't find loader for #{selected}", null) unless loader
+
+        loader path.resolve(BG_BASE, selected), (err, bg) =>
+          return @ready(err, bg) if err
+          totalPadding = (AYP_PANEL_PADDING * 2)
+          left = 0
+          top = Math.round(totalPadding / 2)
+          for panel in panels
+            do (panel) ->
+              compositeImage bg, panel, Math.round(left += (totalPadding / 2)), top
+              left += panel.width # Panel width
+              left += Math.round(totalPadding / 2)
+          @ready(false, bg)
+
+  # Turn the `@script` into 3 panels
+  # using two lines per panel, then invokes `cb`.
+  #
+  # callback invoked as `cb(err, [image, image, image])`
+  # Error will only be set on failure.
+  buildPanels: (cb) =>
+    failed = false
+    panels = [null, null, null]
+
+    # Handler for when a panel fails to build.
+    fail = (err) ->
+      cb(err, [])
+      return failed = true
+
+    # Handler for panel completion, store in the list, see if we're done,
+    # invoke callback if we are.
+    finishPanel = (err, n, panel) ->
+      # No one cares if we already lost.
+      return if failed
+      return fail(err) if err
+
+      panels[n] = panel
+      cb(false, panels) if panels.every (p) -> p
+
+    # TODO: There's a loop here, somewhere
+    buildPanel @script[0..1], (err, panel) -> finishPanel(err, 0, panel)
+    buildPanel @script[2..3], (err, panel) -> finishPanel(err, 1, panel)
+    buildPanel @script[4..5], (err, panel) -> finishPanel(err, 2, panel)

 # Load avatars for `names` and invoke `cb` as:
 # `cb(err, {"Nickname": avatarImg, ...})`


Add some basic responses

Add some basic responses

--- scripts/consciousness.coffee
+++ scripts/consciousness.coffee
@@ -1,12 +1,16 @@
 module.exports = (robot) ->
-  robot.hear /rasteiro/i, (msg) ->
-    msg.send ">HIIIHIIIHIIIHAAAHAAAHAAAHAAA\n -Rasteiro"
-
+  robot.respond /hi|hey|hello|ol[aá]/i, (msg) ->
+    msg.send "Hey!"
+
+  robot.respond /(who|what) are you/i, (msg) ->
+    msg.send "I'm the culmination of years of AI research.\n" +
+      "Ask me privately for help and I'll tell you what I can do for you."
+
   robot.hear /mek ?é das cenas\??/i, (msg) ->
     msg.send "ta tudo"
-
+
   robot.respond /relay(?: to)? (#[^ ]+) +(.*)/i, (msg) ->
     channel = msg.match[1]
     text = msg.match[2]
     msg.envelope.room = channel
-    msg.send text+    msg.send text


updating 'toaster.coffee' usage example

updating 'toaster.coffee' usage example

--- usage/toaster.coffee
+++ usage/toaster.coffee
@@ -9,7 +9,7 @@
 	# expose: ''
 	# minify: false

-	# => RELEASE / DEBUG (required)
+	# => WEBROOT (optional), RELEASE / DEBUG (required)
 	webroot: 'js'
 	release: 'www/js/app.js'
 	debug: 'www/js/app-debug.js'


Fix listener on input instead of .input in modal

--- admin/static/coffee/ui_components/modals.coffee
+++ admin/static/coffee/ui_components/modals.coffee
@@ -11,7 +11,7 @@
             'click .cancel': 'cancel_modal'
             'click .close': 'cancel_modal'
             'click .btn-primary': 'abstract_submit'
-            'keypress .input': 'check_keypress_is_enter'
+            'keypress input': 'check_keypress_is_enter'
             'click .close_error': 'close_error'

         close_error: (event) ->


- [grunt] removed `<%= path.build.styles %>/{,**/}` from task files in `grunt-browser-sync` since previous declaration already include it too

--- tasks/grunt-browser-sync.coffee
+++ tasks/grunt-browser-sync.coffee
@@ -21,6 +21,5 @@
       files:
         src: [
           '<%= path.build.root %>/{,**/}*'
-          '<%= path.build.styles %>/{,**/}*'
           '!<%= path.build.styles %>/{,**/}*.css.map'
         ]


Stringify objects in logging

Stringify objects in logging

--- src/observers/logger.observer.coffee
+++ src/observers/logger.observer.coffee
@@ -3,13 +3,13 @@
 module.exports =
   listenTo: (observable) ->
     observable.on "started", ({ context: { log }, notification, id }) ->
-      log.info "A new message has been received", { id, notification }
+      log.info "A new message has been received", { id, notification: JSON.stringify(notification) }

     observable.on "successful", ({ context: { log }, id }) ->
       log.info "Process successful", { id }

     observable.on "unsuccessful", ({ context: { log }, id, error }) ->
-      log.error "Process unsuccessful", { id, error: errorToJson(error) }
+      log.error "Process unsuccessful", { id, error: JSON.stringify(errorToJson(error)) }

     observable.on "ignored", ({ context: { log }, id }) ->
       log.verbose "Message ignored", { id }


Check existence of panelIndex

Check existence of panelIndex

--- app/assets/javascripts/alongslide/layout.coffee
+++ app/assets/javascripts/alongslide/layout.coffee
@@ -496,7 +496,7 @@
   #
   horizontalPanelAt: (position, edge) ->
     edges = if edge? then [edge] else @HORIZONTAL_EDGES
-    _(@panelIndex[position] || []).any (panel) ->
+    _(@panelIndex?[position] || []).any (panel) ->
       _(edges).any (edge) -> $(panel).hasClass(edge)

   # Check if flow frame shares space with horizontally-pinned panel.


Set up reduce only for models not for collection

--- src/index.coffee
+++ src/index.coffee
@@ -261,7 +261,7 @@
         query.range(options.range) if options.range?

         # If a model asking for query, implement with a reduce
-        query.reduce(true) unless model.models?
+        query.reduce(unless model.models? then true else false)

         query.skip(skip) if options.skip?
         # If stale is false, it waits for the last elements to be indexed


connect.serverClose closes all servers now

The problem was that certain variables were singleton to the module
creating a second server was replacing them and they were not
possible to get a reference to in order to clean it up properly.

This change moves those variables to instance variables on the app
which is stored and then they are all closed when serverClose is
called.

--- src/index.coffee
+++ src/index.coffee
@@ -7,66 +7,77 @@
 connect = require("connect")
 liveReload = require("connect-livereload")
 tiny_lr = require("tiny-lr")
-opt = {}
-server = undefined
 lr = undefined
+apps = []

 class ConnectApp
   constructor: (options) ->
-    opt = options
-    opt.port = opt.port || "8080"
-    opt.root = opt.root || path.dirname(module.parent.id)
-    opt.host = opt.host || "localhost"
-    opt.debug = opt.debug || false
-    @oldMethod("open") if opt.open
-    @server()
+    @port = options.port || "8080"
+    @root = options.root || path.dirname(module.parent.id)
+    @host = options.host || "localhost"
+    @debug = options.debug || false
+    @silent = options.silent || false
+    @https = options.https || false
+    @livereload = options.livereload || false
+    @middleware = options.middleware || undefined
+    @fallback = options.fallback || undefined
+    @oldMethod("open") if options.open
+    @sockets = []
+    @run()

-  server: ->
+  run: ->
     app = connect()
-    @middleware().forEach (middleware) ->
+    app.use connect.directory(if typeof @root == "object" then @root[0] else @root)
+
+    @handlers().forEach (middleware) ->
       if typeof (middleware) is "object"
         app.use middleware[0], middleware[1]
       else
         app.use middleware
-    if opt.https?
-      server = https.createServer
-        key: opt.https.key || fs.readFileSync __dirname + '/certs/server.key'
-        cert: opt.https.cert || fs.readFileSync __dirname + '/certs/server.crt'
-        ca: opt.https.ca || fs.readFileSync __dirname + '/certs/ca.crt'
-        passphrase: opt.https.passphrase || 'gulp'
+
+    if @https
+      @server = https.createServer
+        key: @https.key || fs.readFileSync __dirname + '/certs/server.key'
+        cert: @https.cert || fs.readFileSync __dirname + '/certs/server.crt'
+        ca: @https.ca || fs.readFileSync __dirname + '/certs/ca.crt'
+        passphrase: @https.passphrase || 'gulp'
         app
     else
-      server = http.createServer app
-    app.use connect.directory(if typeof opt.root == "object" then opt.root[0] else opt.root)
-    server.listen opt.port, (err) =>
+      @server = http.createServer app
+
+    @server.listen @port, (err) =>
       if err
         @log "Error on starting server: #{err}"
       else
-        @log "Server started http#{if opt.https? then 's' else ''}://#{opt.host}:#{opt.port}"
+        @log "Server started http#{if @https? then 's' else ''}://#{@host}:#{@port}"

         stoped = false;
         sockets = [];

-        server.on 'close', =>
+        @server.on "close", =>
           if (!stoped)
             stoped = true
             @log "Server stopped"

         # Log connections and request in debug
-        server.on "connection", (socket) =>
-          sockets.push socket
+        @server.on "connection", (socket) =>
+          @logDebug "Received incoming connection from #{socket.address().address}"
+          @sockets.push socket
           socket.on "close", =>
-            sockets.splice sockets.indexOf(socket), 1
+           @sockets.splice @sockets.indexOf(socket), 1

-        server.on "request", (request, response) =>
+        @server.on "request", (request, response) =>
           @logDebug "Received request #{request.method} #{request.url}"
+
+        @server.on "error", (err) =>
+          @log err.toString()

         stopServer = =>
           if (!stoped)
-            sockets.forEach (socket) =>
+            @sockets.forEach (socket) =>
               socket.destroy()

-            server.close()
+            @server.close()
             process.nextTick( ->
               process.exit(0);
             )
@@ -74,45 +85,45 @@
         process.on("SIGINT", stopServer);
         process.on("exit", stopServer);

-        if opt.livereload
+        if @livereload
           tiny_lr.Server::error = ->
-          if opt.https?
+          if @https
             lr = tiny_lr
-              key: opt.https.key || fs.readFileSync __dirname + '/certs/server.key'
-              cert: opt.https.cert || fs.readFileSync __dirname + '/certs/server.crt'
+              key: @https.key || fs.readFileSync __dirname + '/certs/server.key'
+              cert: @https.cert || fs.readFileSync __dirname + '/certs/server.crt'
           else
             lr = tiny_lr()
-          lr.listen opt.livereload.port
-          @log "LiveReload started on port #{opt.livereload.port}"

-  middleware: ->
-    middleware = if opt.middleware then opt.middleware.call(this, connect, opt) else []
-    if opt.livereload
-      opt.livereload = {}  if typeof opt.livereload is "boolean"
-      opt.livereload.port = 35729  unless opt.livereload.port
-      middleware.unshift liveReload(port: opt.livereload.port)
-    if typeof opt.root == "object"
-      opt.root.forEach (path) ->
-        middleware.push connect.static(path)
+          lr.listen @livereload.port
+          @log "LiveReload started on port #{@livereload.port}"
+
+  handlers: ->
+    steps = if @middleware then @middleware.call(this, connect, @) else []
+    if @livereload
+      @livereload = {}  if typeof @livereload is "boolean"
+      @livereload.port = 35729  unless @livereload.port
+      steps.unshift liveReload(port: @livereload.port)
+    if typeof @root == "object"
+      @root.forEach (path) ->
+        steps.push connect.static(path)
     else
-      middleware.push connect.static(opt.root)
-    if opt.fallback
-      middleware.push (req, res) ->
-        require('fs').createReadStream(opt.fallback).pipe(res);
+      steps.push connect.static(@root)
+    if @fallback
+      steps.push (req, res) =>
+        require('fs').createReadStream(@fallback).pipe(res);
+    return steps

-    return middleware
+  log: (text) ->
+    if !@silent
+      util.log util.colors.green(text)

-  log: (@text) ->
-    if !opt.silent
-      util.log util.colors.green(@text)
+  logWarning: (text) ->
+    if !@silent
+      util.log util.colors.yellow(text)

-  logWarning: (@text) ->
-    if !opt.silent
-      util.log util.colors.yellow(@text)
-
-  logDebug: (@text) ->
-    if opt.debug
-      util.log util.colors.blue(@text)
+  logDebug: (text) ->
+    if @debug
+      util.log util.colors.blue(text)

   oldMethod: (type) ->
     text = 'does not work in gulp-connect v 2.*. Please read "readme" https://github.com/AveVlad/gulp-connect'
@@ -120,12 +131,17 @@
       when "open" then @logWarning("Option open #{text}")

 module.exports =
-  server: (options = {}) -> new ConnectApp(options)
+  server: (options = {}) ->
+    app = new ConnectApp(options)
+    apps.push(app)
+    app
   reload: ->
     es.map (file, callback) ->
-      if opt.livereload and typeof lr == "object"
+      if @livereload and typeof lr == "object"
         lr.changed body:
           files: file.path
       callback null, file
   lr: lr
-  serverClose: -> do server.close
+  serverClose: ->
+    apps.forEach((app) -> do app.server.close)
+    apps = []


Added safe mode

Added safe mode

--- src/index.coffee
+++ src/index.coffee
@@ -15,7 +15,7 @@
 					o = {}
 					o[i] = j
 					@data.push o
-
+
 		else @data = data

 		Readable.call this, objectMode: yes
@@ -32,20 +32,32 @@
 			@push @data
 			@push null

-# Extend the prototypes
-module.exports = ->
+
+# Option 1: extend the prototypes
+extendPrototypes = ->

 	# Primitive types require .valueOf() to get their primitive value, otherwise they will be evaluated as objects
 	for i in [Boolean, Number, String]
 		Object.defineProperty i.prototype, 'toStream',
-			value: -> new Stream this.valueOf()
+			value: -> new Stream(this.valueOf())
 			writable: false
 			configurable: false
 			enumerable: false

 	for i in [Array, Buffer, Object]
 		Object.defineProperty i.prototype, 'toStream',
-			value: -> new Stream this
+			value: -> new Stream(this)
 			writable: false
 			configurable: false
-			enumerable: false+			enumerable: false
+
+
+# Option 2: safe wrapper
+safeWrapper = (data) ->
+	if typeof data in ['boolean', 'number', 'string']
+		return new Stream(data.valueOf())
+	else
+		return new Stream(data)
+
+module.exports = extendPrototypes
+module.exports.toStream = safeWrapper


Remove unneeded soak

Remove unneeded soak

--- src/index.coffee
+++ src/index.coffee
@@ -14,7 +14,7 @@

     # Give the plugins a chance to create the XHR object
     for plugin in plugins
-      if xhr = plugin?.createXHR? req then break # First come, first serve
+      if xhr = plugin.createXHR? req then break # First come, first serve
     xhr ?= new XHR

     # Because XHR can be an XMLHttpRequest or an XDomainRequest, we add


Normalize file paths for Windows

Normalize file paths for Windows

--- src/index.coffee
+++ src/index.coffee
@@ -1,5 +1,6 @@
 URI = require 'URIjs'
 cheerio = require 'cheerio'
+path = require 'path'

 isHTML = (filename) ->
@@ -48,14 +49,19 @@

     # Need to transform uri.path() into something Metalsmith can recognise
     else
-      linkPath = uri.absoluteTo(filename).path()
+      unixFilename = filename.replace(/\\/g, '/')
+      linkPath = uri.absoluteTo(unixFilename).path()
+
       if linkPath.slice(-1) is '/'
         linkPath += 'index.html'
+
       if linkPath.charAt(0) is '/'
         linkPath = linkPath.slice(1)

+      filePath = linkPath.split('/').join(path.sep)
+
       # Check the linked file actually exists
-      linkPath not of files
+      filePath not of files

   toString: ->
     "href: \"#{@href}\", text: \"#{@text}\""


Update public API.

Update public API.

--- src/index.coffee
+++ src/index.coffee
@@ -1,3 +1,4 @@
 module.exports =
   publish: require('./publisher').publish
   login: require('./api-tools').login
+  initialize: require('./config').initialize


Fix CSS inlining

Fix CSS inlining

--- base/Asset.cjsx
+++ base/Asset.cjsx
@@ -37,7 +37,7 @@

         when 'css', 'sass'
             if _ext is 'sass'
-                full_path = full_path.replace('.sass', '.css')
+                _path = _path.replace('.sass', '.css')
             file_content = fs.readFileSync(_path).toString()
             return <style dangerouslySetInnerHTML={__html: file_content} />
         else


engine#add

engine#add

--- src/engine_game_state.coffee
+++ src/engine_game_state.coffee
@@ -75,10 +75,12 @@

   shake: (options={}) ->
     self.camera(options.camera).shake(options)
+
   saveState: ->
     I.currentState.saveState()
+
   loadState: (newState) ->
     I.currentState.loadState(newState)
+
   reload: ->
     I.currentState.reload()
-


fix voxel generate position offset

fix voxel generate position offset

--- app/assets/javascripts/static_editors.js.coffee
+++ app/assets/javascripts/static_editors.js.coffee
@@ -227,7 +227,9 @@
         normalMatrix.getNormalMatrix intersect.object.matrixWorld
         normal = intersect.face.normal.clone()
         normal.applyMatrix3(normalMatrix).normalize()
-        position = new THREE.Vector3().addVectors(intersect.point, normal)
+        #position = new THREE.Vector3().addVectors(intersect.point, normal)
+        console.log intersect.point.clone()
+        position = intersect.point.clone()
         voxelPos = {}
         voxelPos.x = Math.floor(position.x) + 0.5
         voxelPos.y = Math.floor(position.y) + 0.5


Cleanup.

Cleanup.

--- src/jnoid.coffee
+++ src/jnoid.coffee
@@ -23,7 +23,7 @@
 Jnoid.next = (value) -> new Next(value)
 Jnoid.end = -> new End()

-Jnoid.join = (streamOfStreams)-> streamOfStreams.map(toEvent).flatMap(id)
+Jnoid.join = (streamOfStreams)-> streamOfStreams.flatMap(id)

 class Event
   isEnd: -> false


add deploy command to li

add deploy command to li

--- code/li.coffee
+++ code/li.coffee
@@ -40,11 +40,18 @@

 sh = (args) ->
   Linode.get args[2], (instance) ->
-    callbacks =
-      stdout: (data) -> process.stdout.write data
-      stderr: (data) -> process.stderr.write data
-      exit: (code) -> console.log "Exit code: #{code}"
-    instance.sh (args[3..].join ' '), callbacks
+    callback = (code) ->
+        console.log "Exit code: #{code}"
+        process.exit code
+
+    instance.sh (args[3..].join ' '), callback
+
+deploy = (args) ->
+  Linode.get args[2], (instance) ->
+    callback = (code) ->
+        console.log "Exit code: #{code}"
+        process.exit code
+    instance.run_hooks callback

 list = (args) ->
   process.stdout.write "Listing instances...\n"
@@ -70,6 +77,7 @@
     when 'destroy' then destroy(args)
     when 'list' then list(args)
     when 'sh' then sh(args)
+    when 'deploy' then deploy(args)
     when undefined then help(args)
     else process.stderr.write("Try li help for help, not #{args}\n")