collaboration: modify kick participant to follow same callback pattern

--- client/ide/lib/collaborationcontroller.coffee
+++ client/ide/lib/collaborationcontroller.coffee
@@ -104,24 +104,24 @@

     return  unless @amIHost

-    targetUser = account.profile.nickname
-
-    displayError = (err) ->
-      showError err
-      throwError err
-
-    @setMachineUser [targetUser], no, (err) =>
-      return displayError err  if err
-
+    target = account.profile.nickname
+
+    # this object is used to follow the same pattern as other
+    # methods. IMO, it makes it easier to read. ~Umut
+    callbacks =
+      success: =>
+        @broadcastMessage { target, type: 'ParticipantKicked' }
+        @handleParticipantKicked target
+      error: (err) ->
+        # TODO: better error handling.
+        showError err
+        throwError err
+
+    @setMachineUser [target], no, (err) =>
+      return callbacks.error err  if err
       socialHelpers.kickParticipants @socialChannel, [account], (err, result) =>
-        return displayError err  if err
-
-        message    =
-          type     : 'ParticipantKicked'
-          target   : targetUser
-
-        @broadcastMessage message
-        @handleParticipantKicked targetUser
+        return callbacks.error err  if err
+        callbacks.success()

   handleParticipantKicked: (username) ->


Collaboration: Resurrection: Wait for the ready state of watch map and prevent race condition

--- client/ide/lib/collaborationcontroller.coffee
+++ client/ide/lib/collaborationcontroller.coffee
@@ -243,12 +243,16 @@

   setWatchMap: ->

-    return  if @myWatchMap.values().length
+    if @myWatchMap.values().length
+      @emit 'WatchMapIsReady'
+      return

     @listChatParticipants (accounts) =>
       accounts.forEach (account) =>
-        {nickname} = account.profile
+        { nickname } = account.profile
         @myWatchMap.set nickname, nickname
+
+      @emit 'WatchMapIsReady'

   activateRealtimeManagerForHost: ->
@@ -623,18 +627,23 @@
   ###
   resurrectParticipantSnapshot: ->

-    @whenRealtimeReady =>
-
+    doResurrection_ = =>
       @removeInitialViews()
-      mapLength = @myWatchMap.values()?.length
-
-      ## The `mapLength=0` meant the participant joined the collaboration just now.
-      if not mapLength or @amIWatchingChangeOwner(@collaborationHost)
+
+      if @amIWatchingChangeOwner @collaborationHost
         @getHostSnapshot (snapshot) =>
           @layoutManager.resurrectSnapshot snapshot, yes  if snapshot
       else
         @fetchSnapshot (snapshot) =>
           @layoutManager.resurrectSnapshot snapshot, yes  if snapshot
+
+
+    @whenRealtimeReady =>
+
+      if @myWatchMap.values()?.length
+        doResurrection_()
+      else
+        @once 'WatchMapIsReady', doResurrection_

   showShareButton: ->


collaboration: streamline cleaningup process

--- client/ide/lib/collaborationcontroller.coffee
+++ client/ide/lib/collaborationcontroller.coffee
@@ -736,11 +736,18 @@
   getCollaborationHost: -> if @amIHost then nick() else @collaborationHost

-  cleanupCollaboration: ->
+  cleanupCollaboration: (options = {}) ->
+    return throwError 'RealTimeManager is not set'  unless @rtm

     kd.utils.killRepeat @pingInterval
     @rtm?.dispose()
     @rtm = null
+    KD.singletons.mainView.activitySidebar.emit 'ReloadMessagesRequested'
+    @forEachSubViewInIDEViews_ 'editor', (ep) => ep.removeAllCursorWidgets()
+
+    { reinit } = options
+
+    @prepareCollaboration()  if reinit

   # environment related


Collaboration: Host is the one to watch!

--- client/ide/lib/collaborationcontroller.coffee
+++ client/ide/lib/collaborationcontroller.coffee
@@ -253,16 +253,11 @@

   setWatchMap: ->

-    if @myWatchMap.values().length
-      @emit 'WatchMapIsReady'
-      return
-
-    @listChatParticipants (accounts) =>
-      accounts.forEach (account) =>
-        { nickname } = account.profile
-        @myWatchMap.set nickname, nickname
-
-      @emit 'WatchMapIsReady'
+    return @emit 'WatchMapIsReady'  if @amIHost
+
+    host = @collaborationHost
+    @myWatchMap.set host, host
+    @emit 'WatchMapIsReady'

   activateRealtimeManagerForHost: ->
@@ -411,7 +406,6 @@
       return  if nickname is nick()

       @statusBar.createParticipantAvatar nickname, no
-      @watchParticipant nickname

       if @amIHost
         @setParticipantPermission nickname


Hotfix bug in POST.

Hotfix bug in POST.

--- tunemachine/routes/api.coffee
+++ tunemachine/routes/api.coffee
@@ -88,6 +88,7 @@
         s.save (err) ->
           if err?
             return models.err res, err
+          req.snapId = s.id
           get req, res

   # POST /api/playlist/:playlistId


payment: add provider to default state in form

--- client/Main/payment/form.coffee
+++ client/Main/payment/form.coffee
@@ -11,8 +11,9 @@
 class PaymentForm extends JView

   getInitialState: -> {
-    planInterval   : PaymentWorkflow.planInterval.MONTH
-    planTitle      : PaymentWorkflow.planTitle.HOBBYIST
+    planInterval : PaymentWorkflow.planInterval.MONTH
+    planTitle    : PaymentWorkflow.planTitle.HOBBYIST
+    provider     : PaymentWorkflow.provider.KODING
   }

   constructor: (options = {}, data) ->


It's now all url

It's now all url

--- resources/assets/coffee/react/beatmap-discussions/main.coffee
+++ resources/assets/coffee/react/beatmap-discussions/main.coffee
@@ -72,11 +72,10 @@
     @cache = {}

   componentDidUpdate: =>
-    queryString = @queryStringFromState()
-
-    if document.location.search != queryString
-      url = encodeURI "#{document.location.pathname}#{queryString}"
-      Turbolinks.controller.advanceHistory url
+    url = @urlFromState()
+
+    if document.location.href != url
+      Turbolinks.controller.advanceHistory encodeURI(url)

   componentWillUnmount: =>
@@ -348,14 +347,6 @@
     BeatmapDiscussionHelper.urlParse(null, discussions)

-  queryStringFromState: =>
-    beatmapId = @currentBeatmap().id
-    page = @state.mode
-    filter = @state.currentFilter
-
-    BeatmapDiscussionHelper.url({beatmapId, page, filter})
-
-
   setBeatmapsetDiscussion: (_e, {beatmapsetDiscussion, callback}) =>
     @setState
       beatmapsetDiscussion: beatmapsetDiscussion
@@ -415,6 +406,14 @@
     @setState {beatmapset}

+  urlFromState: =>
+    beatmapId = @currentBeatmap().id
+    page = @state.mode
+    filter = @state.currentFilter
+
+    BeatmapDiscussionHelper.url({beatmapId, page, filter})
+
+
   users: =>
     if !@cache.users?
       @cache.users = _.keyBy @state.beatmapsetDiscussion.users, 'id'


Prevented error being thrown from timeEnd().

--- src/Log.coffee
+++ src/Log.coffee
@@ -80,6 +80,7 @@

   timeEnd: (name) ->
     timer = @_timers[name]
+    return unless timer?
     time = Date.now() - timer.date
     if @shouldLog(timer.level)
       Log[timer.level](name + ': ' + time + 'ms')


Support other as answer, without further details.

--- services/web/public/coffee/services/log-hints-feedback.coffee
+++ services/web/public/coffee/services/log-hints-feedback.coffee
@@ -22,6 +22,11 @@
 			formData.append "Field#{ reasonFieldAPIId }", feedbackOpt
 			formData.append "Field#{ reasonOtherFieldAPIId }", feedbackOtherVal
 			formData.append "idstamp", idStampVal
+
+			# Allow user to specify "other" without any extra details; an empty string
+			# would trigger an error submitting.
+			if feedbackOpt == feedbackOpts.OTHER and feedbackOtherVal == ""
+				formData.set "Field#{ reasonOtherFieldAPIId }", "#{ feedbackOpts.OTHER } empty"

 			req =
 				method: 'POST'


Fix page title

Fix page title

It was testing for page numbers, which we don’t use anymore.
It seems to work as desired if the tests are removed and the title is
simply allowed to update.

--- src/scripts/helpers/backbone/views/base.coffee
+++ src/scripts/helpers/backbone/views/base.coffee
@@ -72,12 +72,7 @@
       if Backbone.history.fragment?.match? and linksHelper.getCurrentPathComponents().page?
         historyPage = linksHelper.getCurrentPathComponents().page
       if @pageTitle
-        # If it's a page in a book
-        if currentPage? and historyPage?
-          if currentPage is historyPage or currentPage is '1' and historyPage is ''
-            document.title = @pageTitle + settings.titleSuffix
-        else
-          document.title = @pageTitle + settings.titleSuffix
+        document.title = @pageTitle + settings.titleSuffix

       canonical = @canonical?() or @canonical
       if canonical isnt undefined


fixed coffeescript syntax error

fixed coffeescript syntax error

--- src/coffee/plugins/jquery-placeholderText.coffee
+++ src/coffee/plugins/jquery-placeholderText.coffee
@@ -33,11 +33,11 @@
     placeholder[9] = "Aenean placerat. In vulputate urna eu arcu. Aliquam erat volutpat. Suspendisse potenti. Morbi mattis felis at nunc. Duis viverra diam non justo. In nisl. Nullam sit amet magna in magna gravida vehicula. Mauris tincidunt sem sed arcu. Nunc posuere. Nullam lectus justo, vulputate eget, mollis sed, tempor sed, magna. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam neque. Curabitur ligula sapien, pulvinar a, vestibulum quis, facilisis vel, sapien. Nullam eget nisl. Donec vitae arcu."

     createPlaceholderContent = (el) ->
-      options =
-        type: $(el).data('placeholderType') if $(el).data('placeholderType') != 'undefined'
-        amount: $(el).data('placeholderAmount') if $(el).data('placeholderAmount') != 'undefined'
-        html: $(el).data('placeholderHtml') if $(el).data('placeholderHtml') != 'undefined'
-        punctuation: $(el).data('placeholderPunctuation') if $(el).data('placeholderPunctuation') != 'undefined'
+      options = {}
+      options.type = $(el).data('placeholderType') if $(el).data('placeholderType') != 'undefined'
+      options.amount = $(el).data('placeholderAmount') if $(el).data('placeholderAmount') != 'undefined'
+      options.html = $(el).data('placeholderHtml') if $(el).data('placeholderHtml') != 'undefined'
+      options.punctuation = $(el).data('placeholderPunctuation') if $(el).data('placeholderPunctuation') != 'undefined'

       opts = $.extend({}, $.fn.placeholderText.defaults, options)


allow querying using adhoc params

allow querying using adhoc params

--- client/lanes/models/Sync.coffee
+++ client/lanes/models/Sync.coffee
@@ -61,7 +61,7 @@

     perform: (method, options = {}) ->
-        query = {}
+        query = options.queryParams || {}
         for key, value of options
             query[ this.paramsMap[key] ] = value if this.paramsMap[key]


need to add stimulus to teacher review

need to add stimulus to teacher review

--- src/components/task-teacher-review/exercise.cjsx
+++ src/components/task-teacher-review/exercise.cjsx
@@ -122,6 +122,10 @@
     exercise = _.map questions, @renderQuestion

     <CardBody className='task-step openstax-exercise openstax-exercise-card' pinned={false}>
+      <ArbitraryHtmlAndMath
+        className='exercise-stimulus openstax-question'
+        block={true}
+        html={content.stimulus_html} />
       {exercise}
       <ExerciseGroup
           key='step-exercise-group'


Use Name comp for teacher viewing student tasks

--- src/components/task/viewing-as-student-name.cjsx
+++ src/components/task/viewing-as-student-name.cjsx
@@ -1,6 +1,6 @@
 React = require 'react'
 LoadableItem = require '../loadable-item'
-
+Name = require '../name'
 {PerformanceStore, PerformanceActions} = require '../../flux/performance'

 ViewingAsStudentName = React.createClass
@@ -18,7 +18,7 @@
     student = PerformanceStore.getStudentOfTask(courseId, taskId)

     studentName = <div className={className}>
-      {student.first_name} {student.last_name}
+      <Name {...student} />
     </div> if student?

     studentName
@@ -39,4 +39,4 @@
       renderItem={=> <ViewingAsStudentName {...@props}/>}
     />

-module.exports = {ViewingAsStudentName, ViewingAsStudentNameShell}+module.exports = {ViewingAsStudentName, ViewingAsStudentNameShell}


fixed ws

fixed ws

--- app/assets/javascripts/model.coffee
+++ app/assets/javascripts/model.coffee
@@ -239,7 +239,7 @@

 			_preloadRadius = @PRELOAD_RADIUS

-			if @preloadTest(matrix, -_preloadRadius, -_preloadRadius, 2 * _preloadRadius) < @PRELOAD_TOLERANCE
+			if @preloadTest(matrix, -_preloadRadius, -_preloadRadius, 0, 2 * _preloadRadius) < @PRELOAD_TOLERANCE
 				@pull(matrix, (err) ->
 					done()
 					callback() if !err and callback
@@ -319,10 +319,11 @@
 			socket.removeEventListener("message", socketCallback, false)
 			socket.removeEventListener("error", socketErrorCallback, false)

-		socketCallback = (buffer) ->
+		socketCallback = (event) ->
+			buffer = event.data
 			handle = new Float32Array(buffer, 0, 1)[0]
-			if handle == workerHandle
-				callback(null, new Uint8Array(data, 4)) if callback
+			if handle == socketHandle
+				callback(null, new Uint8Array(buffer, 4)) if callback
 				socket.removeEventListener("message", socketCallback, false)
 				socket.removeEventListener("error", socketErrorCallback, false)

@@ -333,12 +334,13 @@
 		transmitPackage = new Float32Array(17)
 		transmitPackage[0] = socketHandle
 		transmitPackage.set(matrix, 1)
+		socketHandle = transmitPackage[0]

 		if socket.readyState == _WebSocket.OPEN
-			socket.send(transmitPackage)
+			socket.send(transmitPackage.buffer)
 		else
 			socketOpenCallback = ->
-				socket.send(transmitPackage)
+				socket.send(transmitPackage.buffer)
 				socket.removeEventListener("open", socketOpenCallback, false)
 			socket.addEventListener("open", socketOpenCallback, false)


fixed small bug

fixed small bug

--- app/assets/javascripts/model.coffee
+++ app/assets/javascripts/model.coffee
@@ -375,7 +375,7 @@

 		@calcVertices(matrix).done ({ vertices }) ->

-			transmitPackage = new Float32Array(17 * vertices.length)
+			transmitPackage = new Float32Array(17 + vertices.length)
 			transmitPackage[0] = socketHandle
 			transmitPackage.set(matrix, 1)


adding

adding

--- app/both/collections/Battles.coffee
+++ app/both/collections/Battles.coffee
@@ -82,8 +82,8 @@
     item = MapItems.create({
       battleId: this.battleId
       type: "gold"
-      top: evt.pageY
-      left: evt.pageX
+      top: evt.pageY - 50
+      left: evt.pageX - 50
     })
     @msg("addItem", item)
     # loot = @items.push(item)


share the document lock between archiving and packing

--- app/coffee/DocArchiveManager.coffee
+++ app/coffee/DocArchiveManager.coffee
@@ -22,7 +22,7 @@
 	archiveDocChangesWithLock: (project_id, doc_id, callback = (error) ->) ->
 		job = (releaseLock) ->
 			DocArchiveManager.archiveDocChanges project_id, doc_id, releaseLock
-		LockManager.runWithLock("HistoryArchiveLock:#{doc_id}", job, callback)
+		LockManager.runWithLock("HistoryLock:#{doc_id}", job, callback)

 	archiveDocChanges: (project_id, doc_id, callback)->
 		MongoManager.getDocChangesCount doc_id, (error, count) ->


Bug #1772: Update wiki stats on wiki page edit

--- app/coffee/modules/wiki/main.coffee
+++ app/coffee/modules/wiki/main.coffee
@@ -175,6 +175,9 @@

         $scope.$watch $attrs.ngModel, (wikiPage) ->
             return if not wikiPage
+            render(wikiPage)
+
+        $scope.$on "wiki:edit", (event, wikiPage) ->
             render(wikiPage)

         $scope.$on "$destroy", ->
@@ -258,6 +261,7 @@
                 $model.setModelValue = wiki
                 $confirm.notify("success")
                 switchToReadMode()
+                $scope.$broadcast("wiki:edit", wikiPage)

             onError = ->
                 $confirm.notify("error")


KDTokenizedInputView: skip prefix matching if only content is an HTMLElement representing the token

--- components/inputs/KDTokenizedInputView.coffee
+++ components/inputs/KDTokenizedInputView.coffee
@@ -45,6 +45,10 @@
     return  if @tokenInput
     range = @utils.getSelectionRange()
     node  = range.commonAncestorContainer
+
+    if node.children?.length is 1
+      return  node.textContent is node.children[0].textContent
+
     start = range.startOffset - 1
     char  = node.textContent[start]
     for name, rule of @getOptions().rules


Switch to fast scoring system for Fuzzy Finder

--- atom/config.cson
+++ atom/config.cson
@@ -122,6 +122,7 @@
       "LocalDistribution"
     ]
     preserveLastSearch: true
+    scoringSystem: "fast"
     useRipGrep: true
   "git-blame":
     colorCommitAuthors: true


Turn on VCS coloring of tabs

Turn on VCS coloring of tabs

--- atom/config.cson
+++ atom/config.cson
@@ -39,6 +39,8 @@
     fontFamily: "Hack"
   "package-sync":
     forceOverwrite: true
+  tabs:
+    enableVcsColoring: true
   "terminal-plus":
     core: {}
     style: {}


use SF Mono as font

use SF Mono as font

--- atom/config.cson
+++ atom/config.cson
@@ -2,18 +2,16 @@
   "autocomplete-plus":
     backspaceTriggersAutocomplete: true
   core:
+    automaticallyUpdate: false
     disabledPackages: [
       "language-c"
       "language-csharp"
       "language-clojure"
-      "language-go"
       "language-java"
       "language-mustache"
       "language-objective-c"
       "language-perl"
       "language-python"
-      "language-ruby"
-      "language-ruby-on-rails"
       "language-sass"
       "language-source"
       "language-xml"
@@ -21,7 +19,8 @@
     projectHome: "/Users/harouna/repositories"
     telemetryConsent: "limited"
   editor:
-    fontFamily: "Menlo"
+    atomicSoftTabs: false
+    fontFamily: "SF Mono"
     preferredLineLength: 100
     scrollPastEnd: true
     showIndentGuide: true
@@ -38,7 +37,9 @@
   linter: {}
   "linter-eslint": {}
   "linter-ui-default":
-    showPanel: true
+    gutterPosition: "Left"
+    panelHeight: 69
+    showProviderName: true
   "markdown-preview":
     useGitHubStyle: true
   minimap:
@@ -46,10 +47,12 @@
     plugins:
       linter: true
       linterDecorationsZIndex: 0
+  "minimap-linter": {}
   tabs:
     enableVcsColoring: true
   "tree-view":
     hideIgnoredNames: true
+    hideVcsIgnoredFiles: true
   welcome:
     showOnStartup: false
 ".html.php.text":


Revert "Treat *.cnf files as INI files"

Revert "Treat *.cnf files as INI files"

This reverts commit 323af0377dd58c7b4d6cd549f3ecf195c18009ae.

--- atom/config.cson
+++ atom/config.cson
@@ -16,10 +16,6 @@
   core:
     allowPendingPaneItems: false
     closeEmptyWindows: false
-    customFileTypes:
-      "source.ini": [
-        "cnf"
-      ]
     disabledPackages: [
       "archive-view"
       "autocomplete"


atom: don't show linter panel by default, very annoying when empty

--- atom/config.cson
+++ atom/config.cson
@@ -58,6 +58,8 @@
 	"linter-jshint":
 		disableWhenNoJshintrcFileInPath: true
 	"linter-php": {}
+	"linter-ui-default":
+		showPanel: false
 	minimap: {}
 	"one-dark-ui": {}
 	"open-file-in":


Add ._* to the list of ignored files

Add ._* to the list of ignored files

--- atom/config.cson
+++ atom/config.cson
@@ -25,6 +25,7 @@
     '.svn'
     '.DS_Store'
     '.keep'
+    '._*'
   ]
   'themes': [
     'soda-light-ui'


Added shortcut options and check for unkown options

--- lib/command.coffee
+++ lib/command.coffee
@@ -3,11 +3,22 @@
 clc      = require 'cli-color'
 argv     = require('optimist')
     .boolean(['help', 'mozrepl-greeting', 'color', 'cs', 'js'])
+    .string(['eval', 'host', 'port'])
+    .alias(e: 'eval', h: 'help')
     .default('color', true)
     .default('mozrepl-greeting', false)
     .default('cs', false)
-    .default('js', false)
+    .default('js', false)
     .argv
+
+ableOptions = [
+    'help', 'mozrepl-greeting', 'color', 'cs', 'js',
+    'eval', 'host', 'port',
+    'e', 'h'
+]
+
+unkownOptions = (argv)->
+    k for own k of argv when !(k =='_' || k =='$0' || k in ableOptions)

 usage = """
 mozfee [OPTIONS...]
@@ -15,17 +26,24 @@
 OPTIONS:
   --cs                     Uses CoffeeScript (default).
   --js                     Uses JavaScript.
-  --eval <code>            Eval code and exit.
+  -e, --eval <code>        Eval code and exit.
   --host <host>            Host (default: localhost)
   --port <port>            Port (default: 4242)
   --[no-]mozrepl-greeting  Shows greeting from Mozrepl (defualt: false).
   --[no-]color             Colorize the output (default: true).
-  --help                   Show this message.
+  -h, --help               Show this message.
 """

 _error = if argv.color then clc.red.bold else (x)->x

 run = ->
+    unkowns = unkownOptions argv
+    if unkowns.length > 0
+        unkowns = unkowns.map (i)-> if i.length == 1 then "-#{i}" else "--#{i}"
+        unkowns = unkowns.join ', '
+        console.log _error("Unkown options : #{unkowns}\n")
+        console.log usage
+        process.exit 1
     if argv.help
         console.log usage
         return


updtaed

updtaed

--- filter.coffee
+++ filter.coffee
@@ -1,6 +1,6 @@
 class GlobalSearch
-  $search_div =   "[data-role='project-search']"
-  $search_field = "[data-role='project-name']"
+  $search_div =   "[data-role='list-search']"
+  $search_field = "[data-role='search-field']"

   constructor: (term_length)->
     @term_length = term_length


another undefined reference

another undefined reference

--- subtree/bokehjs/static/coffee/container.coffee
+++ subtree/bokehjs/static/coffee/container.coffee
@@ -489,7 +489,7 @@

 class GridPlotContainer extends Continuum.HasParent
   type : 'GridPlotContainer'
-  default_view : Bokeh.GridPlotContainerView
+  default_view : GridPlotContainerView

 GridPlotContainer::defaults = _.clone(GridPlotContainer::defaults)
 _.extend(GridPlotContainer::defaults


Specify --to-destination in target_options

--- src/app.coffee
+++ src/app.coffee
@@ -19,7 +19,7 @@
 		interface: 'tether'
 		dport: '80'
 		jump: 'DNAT'
-		'to-destination': '0.0.0.0:8080'
+		target_options: 'to-destination': '0.0.0.0:8080'
 	,
 		table: 'nat'
 		chain: 'PREROUTING'
@@ -27,7 +27,7 @@
 		interface: 'tether'
 		dport: '443'
 		jump: 'DNAT'
-		'to-destination': '0.0.0.0:8080'
+		target_options: 'to-destination': '0.0.0.0:8080'
 	,
 		table: 'nat'
 		chain: 'POSTROUTING'


Serve minidumps as static files

Serve minidumps as static files

--- src/app.coffee
+++ src/app.coffee
@@ -16,6 +16,9 @@
 breakpad.use methodOverride()
 breakpad.use (err, req, res, next) ->
   res.send 500, "Bad things happened:<br/> #{err.message}"
+
+# serve minidumps as files
+breakpad.use '/minidumps', express.static('pool/files/minidump')

 breakpad.post '/webhook', (req, res, next) ->
   webhook.onRequest req


Fix syntax error

Fix syntax error

--- src/app.coffee
+++ src/app.coffee
@@ -51,7 +51,7 @@
   catch err
     if err.code != "ENOENT"
       console.error "Error parsing JSON file #{file}: #{err.message}"
-        process.exit 1
+      process.exit 1

 getConfig file for file in CONFIG_FILES


Make bounce use a generic HTTP server

Make bounce use a generic HTTP server

Squashes express.createServer() deprecation warnings.

--- src/app.coffee
+++ src/app.coffee
@@ -48,9 +48,11 @@
       cert: fs.readFileSync(config.cert),
       app
     )
-    bounce = express.createServer((req, res, next) ->
-      host = req.header("Host")
-      res.redirect "https://" + host + req.url, 301
+    bounce = http.createServer((req, res) ->
+      host = req.headers.host;
+      res.statusCode = 301;
+      res.setHeader("Location", "https://"+host+req.url);
+      res.end();
     )
   else
     appServer = http.createServer(app)


Fix error cases.

Fix error cases.

--- src/coffee/import.coffee
+++ src/coffee/import.coffee
@@ -27,9 +27,9 @@
         @getAllExistingProducts().then (existingProducts) =>
           @initMatcher existingProducts
           @createOrUpdate products, callback
-        .fail (msg) ->
+        .fail (msg) =>
           @returnResult false, msg, callback
-      .fail (msg) ->
+      .fail (msg) =>
         @returnResult false, msg, callback

   getAllExistingProducts: ->


adding ability to clear entire MemoryStore cache

--- shared/memory_store.coffee
+++ shared/memory_store.coffee
@@ -26,4 +26,7 @@

   clear: (key) ->
-    delete @cache[key]
+    if key?
+      delete @cache[key]
+    else
+      @cache = {}


Focus trap continued

Focus trap continued

Small hack to guarantee a focusable element exists after the modal
element to ensure focus gets trapped.

--- src/backbone.modal.coffee
+++ src/backbone.modal.coffee
@@ -30,6 +30,7 @@
       @modalEl.addClass("#{@prefix}-modal").attr('tabindex', '-1')
       @modalEl.html @buildTemplate(@template, data) if @template
       @$el.html(@modalEl)
+      @modalEl.after(Backbone.$('<div tabindex="0" />'))

       if @viewContainer
         @viewContainerEl = @modalEl.find(@viewContainer)
@@ -155,7 +156,7 @@

     blurModal: =>
       setTimeout =>
-        if @active and !Backbone.$.contains(@el, document.activeElement)
+        if @active and !Backbone.$.contains(@modalEl.get(0), document.activeElement)
           @modalEl.focus()

     buildTemplate: (template, data) ->


fix to not kill tasks when pausing

fix to not kill tasks when pausing

--- SingularityUI/app/models/Request.coffee
+++ SingularityUI/app/models/Request.coffee
@@ -46,8 +46,6 @@
             type: 'POST'

     pause: (killTasks) =>
-        killTasks = if not killTasks then undefined else killTasks
-
         $.ajax
             url:         "#{ @url() }/pause"
             type:        'POST'


Changed subfilter check

Changed subfilter check

--- SingularityUI/app/views/requests.coffee
+++ SingularityUI/app/views/requests.coffee
@@ -68,9 +68,9 @@
                 request.name.toLowerCase().indexOf(@searchFilter.toLowerCase()) isnt -1

         # Only show requests that match the clicky filters
-        if @requestsFilter in @haveSubfilter
+        if @requestsFilter in @haveSubfilter and @subFilter isnt 'all'
             requests = _.filter requests, (request) =>
-                filter = @requestsSubFilter is 'all' then true else false
+                filter = false

                 if @requestsSubFilter.indexOf('daemon') isnt -1
                     filter = filter or request.daemon


or ""

or ""

--- coffee/views/single_post.coffee
+++ coffee/views/single_post.coffee
@@ -69,7 +69,7 @@
       replyTo = $("#comment-form-reply-to")
       messageId = @model.get "id"
       messageId += "/" + replyTo.val() if replyTo.val().length
-      anonymous = $("#comment-form-anonymous").prop("checked") || '';
+      anonymous = $("#comment-form-anonymous").prop("checked") or ""
       submit = $("#comment-form-submit").prop("disabled", true)
       i = submit.children("i").toggleClass("icon-refresh icon-spin")
       clear = $("#comment-form-clear").prop("disabled", true)


Calculation of factorMaxSlip

Calculation of factorMaxSlip

--- coffeescript/lib/bot/bot.coffee
+++ coffeescript/lib/bot/bot.coffee
@@ -11,7 +11,8 @@
       logHeader()
       @physics = physics.create()
       @throttles = []
-      @curvedVelocityFactor = 0.48
+      @maxSlipInitialized = no
+      @factorMaxSlip = 0.5

 #      if @color is 'red'
 #        @throttle = 0.7
@@ -31,6 +32,7 @@
     # msgType
     carPositions: (data, control) ->
       @initPhysicsParameters()
+      @initMaxSlipFactor()

       @adjustVelocity()

@@ -46,13 +48,13 @@
     crash: (data, control) ->
       winston.verbose data
       if data.data.color is @color
-        winston.info "#{@logPrefix()};Crash, Curved vel old #{@curvedVelocityFactor}"
-        @reduceCurvedVelocityFactor()
-        winston.info "#{@logPrefix()};Crash, Curved vel new #{@curvedVelocityFactor}"
+        winston.info "#{@logPrefix()};Crash, Curved vel old #{@factorMaxSlip}"
+        @reduceFactorMaxSlip()
+        winston.info "#{@logPrefix()};Crash, Curved vel new #{@factorMaxSlip}"
       control.ignore()

-    reduceCurvedVelocityFactor: ->
-      @curvedVelocityFactor = 0.985 * @curvedVelocityFactor
+    reduceFactorMaxSlip: ->
+      @factorMaxSlip = 0.975 * @factorMaxSlip

     switchDirection: ->
       if @switchRight
@@ -75,6 +77,19 @@
       @physics.initThrottleAndAccelerationRatio dataPoint(tick - 2), dataPoint(tick - 1)
       winston.verbose @physics.throttleAndAccelerationRatio

+    initMaxSlipFactor: ->
+      return if @maxSlipInitialized or @race.getCarAngle(@color) is 0
+
+      a = Math.abs @race.getCarAngle @color
+      v = @race.getVelocity @color
+      r = @getRadius()
+      b = v / r * 180 / Math.PI
+      bslip = b - a
+      vslip = r * b * Math.PI / 180
+      @factorMaxSlip = vslip * vslip / r
+      console.log @logPrefix()  + ";initMaxSlipFactor;a:#{a}, v:#{v}, r:#{r}, b:#{b}, bs:#{bslip}, vs:#{vslip}, fac:#{@factorMaxSlip}"
+      @maxSlipInitialized = yes
+
     setThrottle: (throttle) ->
       if throttle < 0
         @throttle = 0.0
@@ -94,7 +109,7 @@
       velocity = @race.getVelocity @color
       acceleration = @race.getAcceleration @color
       angle = @race.getCarAngle @color
-      radius = @race.getPiece(@color).radius ? 999999
+      radius = @getRadius()
       throttle = @throttle
       predict = @prediction throttle
       angleS = @race.getAngularSpeed @color
@@ -110,6 +125,16 @@
       {pieceIndex, inPieceDistance, lap} = @myPosition()
       ";#{gameTick};#{@color};#{lap};#{pieceIndex};#{normalizedIndex};#{inPieceDistance}"

+    onBendedPiece: -> @race.getPiece(@color).radius?
+    getRadius: ->
+      if @onBendedPiece()
+        pieceIndex = @race.getNormalizedPieceIndex @color
+        @getRadiusOnLane pieceIndex
+      else
+        999999
+
+    getRadiusOnLane: (pieceIndex) -> @race.getRadiusOnLane pieceIndex, @race.getCarLane(@color)
+
     adjustVelocity: ->
       if @race.straightToFinish @color
         @targetVelocity = 50
@@ -118,7 +143,6 @@

       straightDistance = @race.straightDistanceAhead(@color)
       bendedPieceIndex = @race.nextBendedPieceIndex(@color)
-      bendedPiece = @race.getPieceAt bendedPieceIndex
       radius = @race.getRadiusOnLane bendedPieceIndex, @race.getCarLane(@color)
       @targetVelocity = @maxVelocityForRadius radius
       @adjustThrottle straightDistance
@@ -126,7 +150,7 @@
     maxVelocityForRadius: (radius) ->
       # v^2 / r = 0.39

-      Math.sqrt(@curvedVelocityFactor * radius)
+      Math.sqrt(@factorMaxSlip * radius)

     adjustThrottle: (distance = 0) ->


Adds some predicates for differentiating auction types

--- models/auction.coffee
+++ models/auction.coffee
@@ -43,3 +43,12 @@
       "#{start.format(format)}–#{end.format('h:mma')}"
     else
       "#{start.format(format)}–#{end.format(format)}"
+
+  isPreliminaryAuction: ->
+    # https://github.com/artsy/gravity/pull/8848
+    # Replace with: # @get('sale_type') is 'preliminary auction'
+    @get('is_auction') and @get('is_preliminary')
+
+  isAuctionPromo: ->
+    # https://github.com/artsy/gravity/pull/8848
+    @get('sale_type') is 'auction promo'


Update news.coffee

Update news.coffee

--- server/materials/data/news.coffee
+++ server/materials/data/news.coffee
@@ -3,6 +3,10 @@

 export default allNews = () ->
     return news([
+        newsItem("Стажировка на алгопроге", String.raw"""
+            В этом году летом опять будет проходить стажировка на алгопроге. Кто хочет поучаствовать, пишите.
+        """),
+
         newsItem("Занятия в майские праздники", String.raw"""
             1, 8 и 15 мая занятий во ВШЭ не будет. 22 мая занятие будет.

             4 и 11 мая занятий в лицее 40 не будет. 27 апреля занятие будет, 18 мая тоже, скорее всего, будет.


Fix Category.byId.

Fix Category.byId.

--- server/models/bankcategory.coffee
+++ server/models/bankcategory.coffee
@@ -7,5 +7,7 @@
 Category.all = (cb) ->
     Category.request "all", cb

-Category.byId = (params, cb) ->
-    Category.request 'byId', keys: [cat.parentId], cb
+Category.byId = (id, cb) ->
+    param =
+        key: [id]
+    Category.request 'byId', param, cb


Update title for system tray (#2041)

Update title for system tray (#2041)

Using only Menu Bar confuses Linux users because ''Menu Bar'' word is only used withing Mac OS. So, also use "System Tray" word.

--- src/config-schema.coffee
+++ src/config-schema.coffee
@@ -12,7 +12,7 @@
           systemTray:
             type: 'boolean'
             default: true
-            title: "Show icon in menu bar"
+            title: "Show icon in menu bar / system tray"
             platforms: ['darwin', 'linux']
           showImportant:
             type: 'boolean'


Nodejs Web Server: add checkAuthorizationBearerHeader helper function

--- servers/lib/server/helpers.coffee
+++ servers/lib/server/helpers.coffee
@@ -257,6 +257,19 @@
   options.expires = new Date(Date.now() + sessionCookie.maxAge)

   res.cookie 'clientId', sessionId, options
+
+
+checkAuthorizationBearerHeader = (req) ->
+
+  return null  unless req.headers?.authorization
+  parts = req.headers.authorization.split ' '
+
+  return null  unless parts.length is 2 and parts[0] is 'Bearer'
+  token = parts[1]
+
+  return null  unless typeof token is 'string' and token.length > 0
+
+  return token

 module.exports = {
@@ -280,4 +293,5 @@
   isInAppRoute
   isMainDomain
   setSessionCookie
+  checkAuthorizationBearerHeader
 }


add support for new data structure

add support for new data structure

--- views/importTab.cjsx
+++ views/importTab.cjsx
@@ -8,6 +8,47 @@
 i18n = require '../node_modules/i18n'
 {__} = i18n

+emptyShip = [null, [null, -1], [], [], []]
+
+handleSlot = (slots) ->
+  slotId = []
+  slotLv = []
+  for i in [1..4]
+    slot = 'i' + i.toString()
+    if slots.hasOwnProperty slot
+      slotId.push slots[slot].id
+      if slots[slot].rf > 0
+        slotLv.push slots[slot].rf
+      else
+        slotLv.push null
+  if slots.ix?.id?
+    slotId.push slots.ix.id
+    slotLv.push null
+
+  ids: slotId
+  lvs: slotLv
+
+
+
+handleFleet = (fleet) ->
+  ship = Object.clone emptyShip
+  ship[0] = parseInt fleet.id
+  ship[1][0] = fleet.lv
+  ship[1][1] = fleet.luck
+  slots = handleSlot fleet.items
+  ship[2] = slots.ids
+  ship[3] = slots.lvs
+
+  ship
+
+codeConversion = (code) ->
+  fleet = []
+  for i in [1..6]
+    ship = 's' + i.toString()
+    if code.hasOwnProperty(ship) and code[ship].id?
+      fleet[i - 1] = handleFleet code[ship]
+
+  fleet

 getTyku = (deck) ->
   {$ships, $slotitems} = window
@@ -93,38 +134,46 @@
   #],[],[],[]]
   importLogHandle: ->
     {importCode, inputTitle} = @state
+    importCode = JSON.parse importCode
     try
-      importCode = JSON.parse importCode
-      deck = {}
-      flag = true
-      for fleets, index in importCode
-        continue if index is 0
-        if fleets.length isnt 0
-          flag = false
-      if flag
-        for fleets in importCode
-          continue if fleets.length is 0
-          for ship in fleets
-            ship[0] = parseInt ship[0]
-            if ship.length is 4
-              ship.push []
-          if !checkData(fleets)
-            toggleModal __('Error'), __('Incorrect code.')
-          else
-            deck.ships = fleets
-            deck.details = []
-            for item in getDetails fleets
-              deck.details.push item
-            deck.comment = ''
-            deck.tags = ''
-            @props.handleAddData inputTitle, deck
+      if importCode.version?
+        if importCode.f2?
+          toggleModal __('Error'), __('Not support combie fleet.')
+        else
+          fleet = codeConversion importCode.f1
       else
-        toggleModal __('Error'), __('Not support combie fleet.')
+        flag = true
+        for fleet, index in importCode
+          continue if index is 0
+          if fleet.length isnt 0
+            flag = false
+        if flag
+          for fleet in importCode
+            continue if fleet.length is 0
+            for ship in fleet
+              ship[0] = parseInt ship[0]
+              if ship.length is 4
+                ship.push []
+        else
+          toggleModal __('Error'), __('Not support combie fleet.')
+      if !checkData(fleet)
+        toggleModal __('Error'), __('Incorrect code.')
+      else
+        deck = {}
+        deck.ships = fleet
+        deck.details = []
+        for item in getDetails fleet
+          deck.details.push item
+        deck.comment = ''
+        deck.tags = ''
+        @props.handleAddData inputTitle, deck
+
     catch e
       throw e
     @setState
       inputTitle: ''
       importCode: ''
+
   handleInputTitleChange: ->
     inputTitle = @refs.inputTitle.getValue()
     if inputTitle? and inputTitle.length > 0 and @state.importCode.length > 0


improve logging by avoiding string escapes

--- services/track-changes/app.coffee
+++ services/track-changes/app.coffee
@@ -7,16 +7,18 @@

 # log updates as truncated strings
 truncateFn = (updates) ->
-		JSON.stringify updates, (key, value) ->
-			if typeof value == 'string' && (len = value.length) > 80
-				return value.substr(0,32) + "...(message of length #{len} truncated)..." + value.substr(-32)
-			else
-				return value
+		JSON.parse(
+			JSON.stringify updates, (key, value) ->
+				if typeof value == 'string' && (len = value.length) > 80
+					return value.substr(0,32) + "...(message of length #{len} truncated)..." + value.substr(-32)
+				else
+					return value
+		)

 TrackChangesLogger.addSerializers {
+	rawUpdate: truncateFn
 	rawUpdates: truncateFn
 	newUpdates: truncateFn
-	rawUpdate: truncateFn
 	lastUpdate: truncateFn
 }


Only initializes the TopBar when there is one there

--- components/layout/client.coffee
+++ components/layout/client.coffee
@@ -95,8 +95,9 @@
 setupViews = ->
   new BodyView el: $('body')

-  scheme = if sd.IS_GROUP_PAGE then 'GROUP' else 'DEFAULT'
-  mountWithApolloProvider(TopBar, { scheme: scheme }, $('.js-topbar'))
+  if ($topBar = $('.js-topbar')).length
+    scheme = if sd.IS_GROUP_PAGE then 'GROUP' else 'DEFAULT'
+    mountWithApolloProvider(TopBar, { scheme: scheme }, $topBar)

 # TODO: Extract
 setupAjaxHeaders = ->


Add broadsheet.ie.

Add broadsheet.ie.

--- config/jkn2-news-ireland.coffee
+++ config/jkn2-news-ireland.coffee
@@ -25,5 +25,13 @@
   selectors: "div.river.span-8 div.post"
   activators: "h4 a"

+configs.push
+  name: "The Journal IE Home Page"
+  regexps: [
+    "^http://www\\.broadsheet\\.ie/?$"
+    "^http://www\\.broadsheet\\.ie/page/[0-9]+/?$"
+  ]
+  selectors: "div#primary div#content article.post"
+
 require("../common.js").Common.mkConfigs configs, name: "Irish News Sites"


fixed a typo in dependency check message

--- src/coffee/dom-box.coffee
+++ src/coffee/dom-box.coffee
@@ -1,5 +1,5 @@
 # external dependency check
-throw new Error 'Popover requires AngleJS library to operate.' unless Angle?
+throw new Error 'DomBox requires AngleJS library to operate.' unless Angle?

 DomBox =


[aesthetic]: Prefixed private variables with underscore for readabliity

--- src/thingy.coffee
+++ src/thingy.coffee
@@ -11,15 +11,16 @@
         root["Thingy"] = factory()
 )(@, ->
     Magic = ->
-        spells = {}
+        _spells = {}
+
         return {
             listen: (name, fn, scope) ->
-                spells[name] = spells[name] or []
-                spells[name].push
+                _spells[name] = _spells[name] or []
+                _spells[name].push
                     fn: fn
                     scope: scope
             silence: (name, fn) ->
-                spell = spells[name]
+                spell = _spells[name]
                 if spell
                     for i in [0...spell.length]
                         if spell[i].fn is fn
@@ -28,31 +29,31 @@
             cast: (runes) ->
                 name = runes.name
                 data = runes.data
-                spell = spells[name]
+                spell = _spells[name]
                 ( spell.forEach (obj) -> obj.fn.call(obj.scope, data) ) if spell
         }

     Thingy = ->
-        things = {}
+        _things = {}

         Magic: new Magic()
         add: (thingId, fn) ->
-            things[thingId] =
+            _things[thingId] =
                 fn: fn
                 instance: null
         start: (thingId) ->
-            thing = things[thingId]
+            thing = _things[thingId]
             thing.instance = new thing.fn(@Magic)
             Promise.resolve(thing.instance.init()) if thing.instance.init
         stop: (thingId) ->
-            thing = things[thingId]
+            thing = _things[thingId]
             if thing.instance
                 thing.instance.destroy()
                 thing.instance = null
         all: (mode) ->
             return null unless mode is "start" or mode is "stop"
-            for thing in things
-                @[mode](thing) if things.hasOwnProperty(thing)
+            for thing in _things
+                @[mode](thing) if _things.hasOwnProperty(thing)

     return new Thingy()
 ))


ie needs explicit arguments

ie needs explicit arguments

--- src/dom.coffee
+++ src/dom.coffee
@@ -164,7 +164,7 @@
       else return ""

   getTextNodes: (root) ->
-    walker = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT)
+    walker = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false)
     nodes = []
     while node = walker.nextNode()
       nodes.push(node)


Add comments on the two different paths

Add comments on the two different paths

--- spec/Thenable.coffee
+++ spec/Thenable.coffee
@@ -42,7 +42,9 @@
       .else 'portrait', ->
         return true
       .always (path, val) ->
+        # The real resolved path (always hasn't resolved yet)
         chai.expect(t.path).to.eql ['w-image', 'portrait']
+        # Current path (if this resolves)
         chai.expect(path).to.eql ['w-image', 'portrait', 'always']
         process.nextTick ->
           try


redoing how tests are served up by strata

--- src/hem.coffee
+++ src/hem.coffee
@@ -41,38 +41,49 @@
     cssPath:      '/application.css'
     jsPath:       '/application.js'

-    test:         './test'
     testPublic:   './test/public'
     testPath:     '/test'
     specs:        './test/specs'
-    specsPath:    '/test/specs.js'
+    specsPath:    '/specs.js'

   constructor: (options = {}) ->
     @options[key] = value for key, value of options
     @options[key] = value for key, value of @readSlug()

   server: ->
+    # make sure the old compiled files are removed so its always dynamic
+    @removeOldBuilds()
+
+    # setup strata instance
     strata.use(strata.contentLength)

-    strata.map @options.cssPath, (app) =>
-      app.run @cssPackage().createServer()
-    strata.map @options.jsPath, (app) =>
-      app.run @hemPackage().createServer()
+    # get dynamically compiled javascript/css files
+    strata.get(@options.cssPath, @cssPackage().createServer())
+    strata.get(@options.jsPath, @hemPackage().createServer())

-    if fs.existsSync(@options.specs)
-      strata.map @options.specsPath, (app) =>
-        app.run @specsPackage().createServer()
-
-    if fs.existsSync(@options.testPublic)
-      strata.map @options.testPath, (app) =>
-        app.use(strata.file, @options.testPublic, ['index.html', 'index.htm'])
-
+    # get static public folder
     if fs.existsSync(@options.public)
       strata.use(strata.file, @options.public, ['index.html', 'index.htm'])

+    # handle test directory
+    if fs.existsSync(@options.testPublic)
+      strata.map @options.testPath, (app) =>
+        app.get(@options.specsPath, @specsPackage().createServer())
+        app.get("", (env, callback) ->
+          callback(200, 'Content-Type':'text', 'hmmm'))
+        app.use(strata.file, @options.testPublic, ['index.html', 'index.htm'])
+
+    # start server
     strata.run(port: @options.port)

-  build: ->
+  removeOldBuilds: ->
+    files = [
+      path.join(@options.public, @options.jsPath),
+      path.join(@options.public, @options.cssPath),
+      path.join(@options.testPublic, @options.specsPath)]
+    fs.unlinkSync(filePath) for filePath in files when fs.existsSync(filePath)
+
+  build: (buildTests = false) ->
     source = @hemPackage().compile(not argv.debug)
     fs.writeFileSync(path.join(@options.public, @options.jsPath), source)


Removed another circular dependency.

Removed another circular dependency.

--- src/jvm.coffee
+++ src/jvm.coffee
@@ -3,7 +3,6 @@
 require './runtime'
 util = require './util'
 ClassData = require '../src/ClassData'
-{ReferenceClassData} = ClassData
 fs = node?.fs ? require 'fs'
 {trace,error} = require '../src/logging'
 "use strict"


Remove Chronus constructor to reduce the number of redundant methods

--- src/lib.coffee
+++ src/lib.coffee
@@ -7,12 +7,7 @@
         @param 2 Either a single or list of DOMElements which will be parsed for tminus attributes
         @param 3 See Options
         @return See countdown.StateWindow
-    createCountdownWithChronus: (String, DOMElement | Array(DOMElement) [, Options]) -> countdown.StateWindow
-        @param 1 Target date as a chronus date identifier (hexadecimal encoding of number of minutes since epoch)
-        @param 2 Either a single or list of DOMElements which will be parsed for tminus attributes
-        @param 3 See Options
-        @return See countdown.StateWindow
-    createCountdownWithDate: (Date, DOMElement | Array(DOMElement) [, Options]) -> countdown.StateWindow
+    createCountdownWithDate: (Date | Number, DOMElement | Array(DOMElement) [, Options]) -> countdown.StateWindow
         @param 1 Target date as a native javascript date object
         @param 2 Either a single or list of DOMElements which will be parsed for tminus attributes
         @param 3 See Options
@@ -57,7 +52,4 @@
 module.exports =
     createCountdownWithEpoch: (epoch, elements, options) ->
         createCountdown epoch * 1000, elements, options
-    createCountdownWithChronus: (ident, elements, options) ->
-        if not /^[0-9A-Fa-f]+$/.test(ident) then throw new Error("#{ident} is not a valid Chronus identifier")
-        createCountdownWithEpoch parseInt(ident, 16) * 60, elements, options
     createCountdownWithDate: createCountdown


Fix symbol, \url, macro, paragraphs in LaTeX format. Fix #117

* Don't replace symbols like ` and ' inside HTML tags (via new inTag).
* Now we can support " like LaTeX does (equivalent to '').
* Escape (latexEscape) verbatim and \url argument in text from LaTeX processing.
* Fix paragraph handling: \n\n converts to \par before other stuff;
  then macros can correctly consume newlines; and merge multiple <p>s.
* Replace macros more efficiently, and repeatedly until no more expansion
  (important for \let in particular).
* Support \c{z}.

--- lib/formats.coffee
+++ lib/formats.coffee
@@ -52,41 +52,79 @@
   else
     text

-latex2html = (tex) ->
+inTag = (string, offset) ->
+  open = string.lastIndexOf '<', offset
+  if open >= 0
+    close = string.lastIndexOf '>', offset
+    if close < open  ## potential unclosed HTML tag
+      return true
+  false
+
+latexSymbols =
+  '``': '“'
+  "''": '”'
+  '"': '”'
+  '`': '‘'
+  "'": '’'
+  '~': ' '
+  '---': '—'
+  '--': '–'
+latexSymbolsRe = ///#{_.keys(latexSymbols).join '|'}///g
+latexEscape = (x) ->
+  x.replace /[-`'~\\$%&<>]/g, (char) -> "&##{char.charCodeAt 0};"
+
+@latex2html = (tex) ->
+  ## Parse verbatim first (to avoid contents getting mangled by other parsing).
+  tex = tex.replace /\\begin\s*{verbatim}([^]*?)\\end\s*{verbatim}/g,
+    (match, verb) -> "
#{latexEscape verb}
"
+  ## Also parse URLs first, to allow for weird characters in URLs (e.g. %)
+  tex = tex.replace /\\url\s*{([^{}]*)}/g, (match, url) ->
+    """#{latexEscape url}"""
+  .replace /\\href\s*{([^{}]*)}\s*{((?:[^{}]|{[^{}]*})*)}/g, '$2'
+  ## Now remove comments, stripping newlines from the input.
+  comments = (text) ->
+    text = text.replace /%.*$\n?/mg, (match, offset, string) ->
+      if inTag string, offset
+        ## Potential unclosed HTML tag: leave alone, but process other
+        ## %s on the same line after tag closes.
+        close = match.indexOf '>'
+        if close >= 0
+          match[..close] + comments match[close+1..]
+        else
+          match
+      else
+        ''
+  tex = comments tex
+  ## Paragraph detection must go before any macro expansion (which eat \n's)
+  tex = tex.replace /\n\n+/g, '\n\\par\n'
+  ## Process \def and \let, and expand all macros.
   defs = {}
-  tex = tex.replace /%.*$\n?/mg, ''
   tex = tex.replace /\\def\s*\\([a-zA-Z]+)\s*{((?:[^{}]|{(?:[^{}]|{[^{}]*})*})*)}/g, (match, p1, p2) ->
     defs[p1] = p2
     ''
   tex = tex.replace /\\let\s*\\([a-zA-Z]+)\s*=?\s*\\([a-zA-Z]+)\s*/g, (match, p1, p2) ->
     defs[p1] = "\\#{p2}"
     ''
-  for def, val of defs
-    #console.log "\\#{def} = #{val}"
-    tex = tex.replace ///\\#{def}\s*///g, val
-  tex = tex.replace /\\begin\s*{verbatim}([^]*?)\\end\s*{verbatim}/g, (match, verb) ->
-    verb = verb
-    .replace /&/g, '&'
-    .replace /\\/g, '\'
-    .replace /\$/g, '$'
-    .replace /</g, '<'
-    .replace />/g, '>'
-    #verb = putMathBack verb, math
-    "
#{verb}
"
+  #for def, val of defs
+  #  console.log "\\#{def} = #{val}"
+  r = ///\\(#{_.keys(defs).join '|'})\s*///g
+  while 0 <= tex.search(r)
+    tex = tex.replace r, (match, def) -> defs[def]
   ## After \def expansion and verbatim processing, protect math
   [tex, math] = preprocessKaTeX tex
+  ## Start initial paragraph
   tex = '
' + tex
-  .replace /\\\\/g, '[DOUBLEBACKSLASH]'
+  ## Commands
+  tex = tex.replace /\\\\/g, '[DOUBLEBACKSLASH]'
   .replace /\\(BY|YEAR)\s*{([^{}]*)}/g, '$2'
   .replace /\\protect\s*/g, ''
+  .replace /\\par\s*/g, '
'
   .replace /\\textbf\s*{((?:[^{}]|{[^{}]*})*)}/g, '$1'
   .replace /\\textit\s*{((?:[^{}]|{[^{}]*})*)}/g, '$1'
   .replace /\\textsf\s*{((?:[^{}]|{[^{}]*})*)}/g, '$1'
   .replace /\\emph\s*{((?:[^{}]|{[^{}]*})*)}/g, '$1'
   .replace /\\textsc\s*{((?:[^{}]|{[^{}]*})*)}/g, '$1'
   .replace /\\underline\s*{((?:[^{}]|{[^{}]*})*)}/g, '$1'
-  .replace /\\url\s*{([^{}]*)}/g, '$1'
-  .replace /\\href\s*{([^{}]*)}\s*{((?:[^{}]|{[^{}]*})*)}/g, '$2'
   .replace /\\textcolor\s*{([^{}]*)}\s*{([^{}]*)}/g, '$2'
   .replace /\\colorbox\s*{([^{}]*)}\s*{([^{}]*)}/g, '$2'
   .replace /\\begin\s*{enumerate}/g, '
'
@@ -146,7 +184,7 @@
   .replace /\\=u|\\={u}/g, 'ū'
   .replace /\\=y|\\={y}/g, 'ȳ'
   .replace /\\c\s*{s}/g, 'ş'
-  .replace /\\c\s*{z}/g, 'z'  ## doesn't exist
+  .replace /\\c\s*{z}/g, 'z̧'
   .replace /\\v\s*{C}/g, 'Č'
   .replace /\\v\s*{s}/g, 'š'
   .replace /\\v\s*{n}/g, 'ň'
@@ -154,19 +192,16 @@
   .replace /\\u\s*{a}/g, 'ă'
   .replace /\\v\s*{a}/g, 'ǎ'
   .replace /\\H\s*{o}/g, 'ő'
-  .replace /``/g, '“'
-  .replace /''/g, '”'
-  #.replace /''|"/g, '”'  ## " replacement wreaks havoc with HTML
-  .replace /`/g, '‘'
-  .replace /'/g, '’'
   .replace /\\&/g, '&'
   .replace /\\([${}])/g, '$1'
-  .replace /~/g, ' '
   .replace /\\\s/g, ' '
-  .replace /---/g, '—'
-  .replace /--/g, '–'
-  .replace /\n\n+/g, '\n
\n'
+  .replace latexSymbolsRe, (match, offset, string) ->
+    if inTag string, offset  ## potential unclosed HTML tag; leave alone
+      match
+    else
+      latexSymbols[match]
   .replace /
\s*(<h[1-9]>)/g, '$1'
+  .replace /
(\s*
)+/g, '
'  ## Remove double paragraph breaks
   .replace /\[DOUBLEBACKSLASH\]/g, '\\\\'
   [tex, math]


Fix @username when username has dots and other characters. Fix #144

* escapeRe helper was broken, so characters `\^$*+?.()|{}[]` in usernames
  caused @mentions to fail.
* Also properly handle the case when one username is a prefix of another
  (try for maximal match).

--- lib/formats.coffee
+++ lib/formats.coffee
@@ -283,14 +283,18 @@
         "#{slash}"  ## Add zero-width space after every slash group

 @escapeRe = (string) ->
-  string.replace /[\\^$*+?.()|{}\[\]]/g, "\\$1"
+  string.replace /[\\^$*+?.()|{}\[\]]/g, "\\$&"

 postprocessAtMentions = (text) ->
+  return text unless 0 <= text.indexOf '@'
   users = Meteor.users.find {}, fields: username: 1
   .map (user) -> user.username
   return text unless 0 < users.length
+  ## Reverse-sort by length to ensure maximum-length match
+  ## (to handle when one username is a prefix of another).
+  _.sortBy users, (name) -> -name.length
   users = (escapeRe user for user in users)
-  text.replace ///@(#{users.join '|'})\b///g, (match, user) ->
+  text.replace ///@(#{users.join '|'})(?!\w)///g, (match, user) ->
     "@#{linkToAuthor (routeGroup?() ? wildGroup), user}"

 katex = require 'katex'


Specify values in error messages

Specify values in error messages

Updates the error messages to show the requested value and the maximum allowed.

--- lib/helpers.coffee
+++ lib/helpers.coffee
@@ -61,13 +61,16 @@
     if typeof lineNumber isnt 'number' or lineNumber isnt lineNumber or lineNumber < 0
       return [[0, 0], [0, 1]]
     buffer = textEditor.getBuffer()
-    throw new Error('Line number greater than maximum line') if lineNumber > buffer.getLineCount() - 1
-    lineLength = buffer.lineLengthForRow(lineNumber)
-    throw new Error('Column start greater than line length') if colStart > lineLength
+    maxLine = buffer.getLineCount() - 1
+    if lineNumber > maxLine
+      throw new Error("Line number (#{lineNumber}) greater than maximum line (#{maxLine})")
     if typeof colStart isnt 'number' or colStart < 0
       colStart = (textEditor.indentationForBufferRow(lineNumber) * textEditor.getTabLength())
       if colStart isnt 0
         colStart -= 1
+    lineLength = buffer.lineLengthForRow(lineNumber)
+    if colStart > lineLength
+      throw new Error("Column start (#{colStart}) greater than line length (#{lineLength})")
     colEnd = lineLength
     if colEnd isnt 0
       colEnd -= 1


Increase comment readability.

Increase comment readability.

--- dashboard/assets/javascripts/models.js.coffee
+++ dashboard/assets/javascripts/models.js.coffee
@@ -134,9 +134,7 @@

       return

-    # OK, we have a job.
-
-    # Read the updated job data.
+    # OK, we have a job.  Read the updated job data.
     job.amplify(job_data)

     # Now, update the log buffer.  This part depends on the message type.


Use index instead of location

Use index instead of location

--- lib/motions.coffee
+++ lib/motions.coffee
@@ -285,31 +285,31 @@
     super(@editorView.editor, @state)

   initialize: =>
-    @historyLocation = -1
+    @historyIndex = -1
     @view = new VimCommandModeInputView(@, class: 'search')
     @editor.commandModeInputView = @view
     @view.editor.on('core:move-up', @increaseHistorySearch)
     @view.editor.on('core:move-down', @decreaseHistorySearch)

-  restoreHistory: (location) ->
-    @view.editor.setText(@history(location).searchTerm)
+  restoreHistory: (index) ->
+    @view.editor.setText(@history(index).searchTerm)

   history: (location) ->
     @state.searchHistory[location]

   increaseHistorySearch: =>
-    if @history(@historyLocation + 1)?
-      @historyLocation += 1
-      @restoreHistory(@historyLocation)
+    if @history(@historyIndex + 1)?
+      @historyIndex += 1
+      @restoreHistory(@historyIndex)

   decreaseHistorySearch: =>
-    if @historyLocation <= 0
+    if @historyIndex <= 0
       # get us back to a clean slate
-      @historyLocation = -1
+      @historyIndex = -1
       @view.editor.setText('')
     else
-      @historyLocation -= 1
-      @restoreHistory(@historyLocation)
+      @historyIndex -= 1
+      @restoreHistory(@historyIndex)

   reversed: =>
     @initiallyReversed = @reverse = true


adjust listIncidents for handover purpose

--- lib/pagerv2.coffee
+++ lib/pagerv2.coffee
@@ -295,22 +295,34 @@
     else
       query = {
         time_zone: 'UTC',
-        'include[]': 'first_trigger_log_entry'
+        'include[]': 'first_trigger_log_entry',
+        'urgencies[]': 'high',
+        sort_by: 'created_at'
       }
       if date_since?
         unless date_until?
           date_until = moment().utc()
-        query['date_since'] = moment(date_since).format()
-        query['date_until'] = moment(date_until).format()
+        query['since'] = moment(date_since).utc().format()
+        query['until'] = moment(date_until).utc().format()
       else
         query['date_range'] = 'all'
       if statuses?
         query['statuses[]'] = statuses.split /,/
       query['limit'] = limit
       query['total'] = 'true'
+      console.log query
       @request('GET', '/incidents', query)
       .then (data) ->
         data
+
+  listIncidentsWithNotes: (
+    incidents = '',
+    statuses = 'triggered,acknowledged',
+    date_since = null,
+    date_until = null,
+    limit = 100
+  ) ->
+    @listIncidents(incidents, statuses, date_since, date_until, limit)

   upagerateIncidents: (user, incidents = '', which = 'triggered', status = 'acknowledged') ->
     @getUserEmail(user, user)


attach/detach particles

attach/detach particles

--- src/BaseModel.coffee
+++ src/BaseModel.coffee
@@ -49,3 +49,18 @@
     @mesh.traverse (object) ->
       object.visible = b
     @visible = b
+
+  isPressed: (raycaster) ->
+    raycaster.intersectObject(@mesh).length > 0
+
+  attachParticle: (particle) ->
+    if @mesh?
+      @particle = particle
+      @particle.attached = true
+      @mesh.add particle.mesh
+
+  detachParticle: ->
+    if @mesh? and @particle?
+      @mesh.remove @particle.mesh
+      @particle.attached = false
+      @particle = undefined


Add prediction for the playing boundaries

--- client.coffee
+++ client.coffee
@@ -483,10 +483,9 @@
 					markerDragged = ->
 						if mapReady()
 							Server.sync 'setBounds', window.locationOne.getLatLng(), window.locationTwo.getLatLng(), !->
-								# TODO: fix prediction, does not work yet
-								#log 'predicting bounds change'
-								#Db.shared.set 'game', 'bounds', {one: window.locationOne.getLatLng(), two: window.locationTwo.getLatLng()}
-								#log 'predicted bounds: ', {one: window.locationOne.getLatLng(), two: window.locationTwo.getLatLng()}
+								log 'predicting bounds change'
+								Db.shared.set 'game', 'bounds', {one: {lat: window.locationOne.getLatLng().lat, lng: window.locationOne.getLatLng().lng}, two: {lat: window.locationTwo.getLatLng().lat, lng: window.locationTwo.getLatLng().lng}}
+								log 'predicted bounds: ', {one: {lat: window.locationOne.getLatLng().lat, lng: window.locationOne.getLatLng().lng}, two: {lat: window.locationTwo.getLatLng().lat, lng: window.locationTwo.getLatLng().lng}}
 							checkAllBeacons()
 					# Corner 1
 					lat1 = Db.shared.get('game', 'bounds', 'one', 'lat')
@@ -1136,11 +1135,10 @@
 						if not (Db.shared.peek('game', 'bounds', 'one', 'lat')?)
 							one = L.latLng(latLngObj.lat+0.01,latLngObj.lng-0.02)
 							two = L.latLng(latLngObj.lat-0.01,latLngObj.lng+0.02)
-							Server.sync 'setBounds', one, two,  !->
-								# TODO: fix prediction, does not work yet
-								#log 'predicting bounds change'
-								#Db.shared.set 'game', 'bounds', {one: window.locationOne.getLatLng(), two: window.locationTwo.getLatLng()}
-								#log 'predicted bounds: ', {one: window.locationOne.getLatLng(), two: window.locationTwo.getLatLng()}
+							Server.sync 'setBounds', one, two, !->
+								log 'predicting bounds change'
+								Db.shared.set 'game', 'bounds', {one: {lat: window.locationOne.getLatLng().lat, lng: window.locationOne.getLatLng().lng}, two: {lat: window.locationTwo.getLatLng().lat, lng: window.locationTwo.getLatLng().lng}}
+								log 'predicted bounds: ', {one: {lat: window.locationOne.getLatLng().lat, lng: window.locationOne.getLatLng().lng}, two: {lat: window.locationTwo.getLatLng().lat, lng: window.locationTwo.getLatLng().lng}}
 							map.setView(latLngObj)
 						locationIcon = L.icon({
 							iconUrl: Plugin.resourceUri('location.png'),


Fix command line tests which broke after 6128fe8c5.

--- tests/batman/view/simple_rendering_test.coffee
+++ tests/batman/view/simple_rendering_test.coffee
@@ -104,10 +104,7 @@
       foo: false
     , (node) ->
         helpers.withNodeInDom node, ->
-          if IN_NODE
-            equal node.css('display'), 'none !important'
-          else
-            equal node.css('display'), 'none'
+          equal node.css('display'), 'none'
         QUnit.start()

 asyncTest 'it should allow visibility to be bound on inline elements', 2, ->
@@ -127,10 +124,7 @@
       foo: false
     , (node) ->
         helpers.withNodeInDom node, ->
-          if IN_NODE
-            equal node.css('display'), 'none !important'
-          else
-            equal node.css('display'), 'none'
+          equal node.css('display'), 'none'
         QUnit.start()

 asyncTest 'it should allow arbitrary attributes to be bound', 2, ->


Fix FastClick for webpack compatibility

Fix FastClick for webpack compatibility

https://github.com/ftlabs/fastclick/issues/398

--- project_template/app/application.coffee
+++ project_template/app/application.coffee
@@ -1,10 +1,10 @@
 $               = require('jquery')
-attachFastClick = require('fastclick')
+FastClick       = require('fastclick')
 app             = require('./app')

 $ ->
   app.start()
-  attachFastClick(document.body)
+  FastClick.attach(document.body)

   # Stretch main container height so it's not resized when the viewport is
   # resized. This happens on Android when the keyboard pops up.


More better commentary

More better commentary

--- source/coffee/index.coffee
+++ source/coffee/index.coffee
@@ -112,7 +112,7 @@
             switch jqXHR.status
               when 410 # Gone, cycle transaction
                 updateTransaction null, -> rePoll()
-              when 502 # Bad gateway; proxy failure
+              when 502 # Bad gateway; proxy successfully failed
                 rePoll true
               else # Other errors
                 if textStatus is "timeout"


Polish the things

Polish the things

* Add more comments
* Simplify and cleanup mapped data
* Template DOM change throttling (coalescing)
* Move bindinghandler to remap nodes on drag-drop fail

--- source/coffee/index.coffee
+++ source/coffee/index.coffee
@@ -29,7 +29,7 @@
     @getMappedData = (url, pin, map={}, wrap=true) =>
       @getData url, (data) => @mapData(data, pin, map, wrap)

-    # Parse a flat node list, injecting children arrays for traversal
+    # Parse node array into a flat, keyed boject, injecting children for traversal
     @parseNodes = (data, pin, keyed={}) ->
       root = {}

@@ -66,24 +66,34 @@
         else # Invalid root node!
           delete keyed[id] # Pew Pew!

+      # And run a chillin' parseage
       for id of keyed
         parseChildren keyed[id]

       pin keyed if pin? # Update pin with keyed
-      children: root # Return in format appropriate for mapping
-
+      root # Return root for mapping
+
+    # Process nodes and map to pin
     @updateNodes = (data, pin, keys) =>
       @mapData @parseNodes(data, pin, keys), pin

+    # Get and process nodes from url
     @getNodes = (url, pin, keys) =>
       @getData url, (data) =>
         @updateNodes data, pin, keys

+    # Temp storage for node mapping
     @wsTemp = ko.observableArray()
-    @wsItems = ko.computed(=>
-      @wsTemp()?[0]?.children ? []).extend throttle: 100 # Coalesce node changes
-
+    # Computed wrapper for coalescing changes
+    @wsItems = ko.computed =>
+      @wsTemp()
+    # Enable coalesced node changes (x msec settling period)
+    @wsItems.extend throttle: config?.coalesce ? 500
+
+    # Execution plans
     @wsPlans = ko.observableArray()
+
+    # Flat node list, keyed by id
     @wsKeys = {}

     @poll = (url, cb, timeout=@config?.timeout?.long ? 30000) =>
@@ -211,6 +221,19 @@
           $("#indexInputModal").modal "hide"
           console.log "Error (#{jqXHR.status}): errorThrown"

+    ko.bindingHandlers.sortable.afterMove = (options) =>
+      parent = options.sourceParentNode.attributes["data-id"].value
+      ntrapy.post "/roush/facts/",
+        JSON.stringify
+          key: "parent_id"
+          value: parent
+          node_id: options.item.id()
+      , (data) ->
+        null # TODO: Do something with success?
+      , (jqXHR) =>
+        console.log "Error: ", jqXHR
+        @updateNodes null, @wsTemp, @wsKeys # Remap from keys on fails
+
     @ # Return ourself

   popoverOptions =
@@ -252,17 +275,6 @@
       null
       # TODO: Do something on sort stop?

-  ko.bindingHandlers.sortable.afterMove = (options) ->
-    parent = options.sourceParentNode.attributes["data-id"].value
-    ntrapy.post "/roush/facts/",
-      JSON.stringify
-        key: "parent_id"
-        value: parent
-        node_id: options.item.id()
-    , (data) ->
-      null # TODO: Do something with success?
-    , (jqXHR) ->
-      console.log "Error: ", jqXHR

   ntrapy.indexModel = new IndexModel()
   ko.applyBindings ntrapy.indexModel


Now asking to save scratch when attempting to close an edited scratch

--- lib/scratch.coffee
+++ lib/scratch.coffee
@@ -10,21 +10,22 @@
 		if @isScratchOpen()
 			@closeScratch()
 		else
-			atom.workspace.open(@dirPath + "/" + @fileName)
+			@openScratch()
+
+	getScratchEditor: ->
+		editors = atom.workspace.getEditors()
+		for editor in editors
+			return editor if editor.getTitle() is @fileName

 	isScratchOpen: ->
-		editors = atom.workspace.getEditors()
-		for editor in editors
-			return true if editor.getTitle() is @fileName
-		return false
+		scratchEditor = @getScratchEditor()
+		scratchEditor isnt undefined
+
+	openScratch: ->
+		atom.workspace.open(@dirPath + "/" + @fileName)

 	closeScratch: ->
-		editors = atom.workspace.getEditors()
-		scratch = null
-		for editor in editors
-			do (editor) ->
-				if editor.getTitle() is @fileName
-					scratch = editor
-		if scratch && scratch.shouldPromptToSave
-			atom.workspace.getActivePane().promptToSaveItem(scratch)
-		editor.destroy()
+		scratchEditor = @getScratchEditor()
+		if scratchEditor.shouldPromptToSave
+			atom.workspace.getActivePane().promptToSaveItem(scratchEditor)
+		scratchEditor.destroy()


Fix bug in committing vparam update

Fix bug in committing vparam update

--- lib/session.coffee
+++ lib/session.coffee
@@ -788,6 +788,9 @@
   rev = sessionContext.revisions[sessionContext.revisions.length - 1] or 0
   sessionContext.deviceData.timestamps.collapse(rev + 1)
   sessionContext.deviceData.attributes.collapse(rev + 1)
+  sessionContext.deviceData.timestamps.revision = rev + 1
+  sessionContext.deviceData.attributes.revision = rev + 1
+
   for k of sessionContext.extensionsCache
     if rev < Number(k.split(':', 1)[0])
       delete sessionContext.extensionsCache[k]


debug draw

debug draw

--- src/engine_physics.coffee
+++ src/engine_physics.coffee
@@ -1,69 +1,77 @@
-Engine.Box2D = (I, self) ->
-  $.reverseMerge I,
-    scale: 0.1
-    gravity: Point(0, 98)
-    PHYSICS_DEBUG_DRAW: false
+( ($)->
+  {b2World, b2DebugDraw} = Box2D.Dynamics

-  world = new Box2D.Dynamics.b2World(new Box2D.Common.Math.b2Vec2(I.gravity.x, I.gravity.y), true)
+  Engine.Box2D = (I, self) ->
+    $.reverseMerge I,
+      scale: 0.1
+      gravity: Point(0, 98)
+      PHYSICS_DEBUG_DRAW: false

-  pendingCollisions = []
-  pendingDestructions = []
-
-  world.SetContactListener
-    BeginContact: (contact) ->
-      a = contact.GetFixtureA().GetBody().GetUserData()
-      b = contact.GetFixtureB().GetBody().GetUserData()
-
-      pendingCollisions.push([a, b])
-
-    EndContact: (contact) ->
-    PreSolve: (contact, oldManifold) ->
-    PostSolve: (contact, impulse) ->
-
-  fireCollisionEvents = () ->
-    pendingCollisions.each (event) ->
-      [a, b] = event
-
-      a.trigger "collision", b
-      b.trigger "collision", a
+    world = new b2World(new Box2D.Common.Math.b2Vec2(I.gravity.x, I.gravity.y), true)
+    debugDraw = null
+    debugElement = null
+    debugCanvas = null

     pendingCollisions = []
-
-  destroyPhysicsBodies = () ->
-    pendingDestructions.each (body) ->
-      world.DestroyBody(body)
-
     pendingDestructions = []

-  self.bind "update", ->
-    world.Step(1 / I.FPS, 10, 10)
-    world.ClearForces()
+    world.SetContactListener
+      BeginContact: (contact) ->
+        a = contact.GetFixtureA().GetBody().GetUserData()
+        b = contact.GetFixtureB().GetBody().GetUserData()

-    fireCollisionEvents()
-    destroyPhysicsBodies()
+        pendingCollisions.push([a, b])

-  self.bind "draw", (canvas) ->
-    if I.PHYSICS_DEBUG_DRAW
-      unless debugDraw
-        debugElement = $("<canvas width=640 height=480 />").get(0)
-        debugCanvas = debugElement.getContext("2d")
-        debugDraw = new b2DebugDraw()
-        debugDraw.SetSprite($("<canvas width=640 height=480 />").get(0).getContext("2d"))
-        debugDraw.SetDrawScale(10)
-        debugDraw.SetFillAlpha(0.3)
-        debugDraw.SetLineThickness(1.0)
-        debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit)
-        world.SetDebugDraw(debugDraw)
+      EndContact: (contact) ->
+      PreSolve: (contact, oldManifold) ->
+      PostSolve: (contact, impulse) ->

-      world.DrawDebugData()
-      canvas.drawImage(debugElement, 0, 0, debugElement.width, debugElement.height, 0, 0, debugElement.width, debugElement.height)
+    fireCollisionEvents = () ->
+      pendingCollisions.each (event) ->
+        [a, b] = event

-  self.bind "beforeAdd", (entityData) ->
-    entityData.world = world
+        a.trigger "collision", b
+        b.trigger "collision", a

-  self.bind "afterAdd", (object) ->
-    object.bind "destroy", ->
-      pendingDestructions.push object.body()
+      pendingCollisions = []

-  return {}
+    destroyPhysicsBodies = () ->
+      pendingDestructions.each (body) ->
+        world.DestroyBody(body)

+      pendingDestructions = []
+
+    self.bind "update", ->
+      world.Step(1 / I.FPS, 10, 10)
+      world.ClearForces()
+
+      fireCollisionEvents()
+      destroyPhysicsBodies()
+
+    self.bind "draw", (canvas) ->
+      if I.PHYSICS_DEBUG_DRAW
+        unless debugDraw
+          debugElement = $("<canvas width=640 height=480 />").get(0)
+          debugCanvas = debugElement.getContext("2d")
+          debugDraw = new b2DebugDraw()
+          debugDraw.SetSprite(debugCanvas)
+          debugDraw.SetDrawScale(10)
+          debugDraw.SetFillAlpha(0.3)
+          debugDraw.SetLineThickness(1.0)
+          debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit)
+          world.SetDebugDraw(debugDraw)
+
+        world.DrawDebugData()
+        canvas.drawImage(debugElement, 0, 0, debugElement.width, debugElement.height, 0, 0, debugElement.width, debugElement.height)
+
+    self.bind "beforeAdd", (entityData) ->
+      entityData.world = world
+
+    self.bind "afterAdd", (object) ->
+      object.bind "destroy", ->
+        pendingDestructions.push object.body()
+
+    return {}
+
+)(jQuery)
+


Terminal: cleanup

Terminal: cleanup

--- client/Terminal/src/Terminal.coffee
+++ client/Terminal/src/Terminal.coffee
@@ -24,25 +24,25 @@

     @keyInput.appendToDomBody()

-    @pixelWidth = 0
-    @pixelHeight = 0
-    @sizeX = 80
-    @sizeY = 24
-    @currentStyle = WebTerm.StyledText.DEFAULT_STYLE
-    @currentWhitespaceStyle = null
-    @currentCharacterSets = ["B", "A", "A", "A"]
+    @pixelWidth               = 0
+    @pixelHeight              = 0
+    @sizeX                    = 80
+    @sizeY                    = 24
+    @currentStyle             = WebTerm.StyledText.DEFAULT_STYLE
+    @currentWhitespaceStyle   = null
+    @currentCharacterSets     = ["B", "A", "A", "A"]
     @currentCharacterSetIndex = 0

-    @inputHandler = new WebTerm.InputHandler(this)
-    @screenBuffer = new WebTerm.ScreenBuffer(this)
-    @cursor = new WebTerm.Cursor(this)
+    @inputHandler      = new WebTerm.InputHandler(this)
+    @screenBuffer      = new WebTerm.ScreenBuffer(this)
+    @cursor            = new WebTerm.Cursor(this)
     @controlCodeReader = WebTerm.createAnsiControlCodeReader(this)

-    @measurebox = $(document.createElement("div"))
+    @measurebox        = $(document.createElement("div"))
+    @updateSizeTimer   = null
     @measurebox.css "position", "absolute"
     @measurebox.css "visibility", "hidden"
     @container.append @measurebox
-    @updateSizeTimer = null
     @updateSize()

     @outputbox = $(document.createElement("div"))
@@ -98,8 +98,8 @@
     return if x is @sizeX and y is @sizeY

     cursorLineIndex = @screenBuffer.toLineIndex(@cursor.y)
-    @sizeX = x
-    @sizeY = y
+    @sizeX          = x
+    @sizeY          = y
     @screenBuffer.scrollingRegion = [0, y - 1]

     @cursor.moveTo @cursor.x, cursorLineIndex - @screenBuffer.toLineIndex(0)
@@ -108,7 +108,7 @@
   updateSize: (force=no) ->
     return if not force and @pixelWidth is @container.prop("clientWidth") and @pixelHeight is @container.prop("clientHeight")
     @container.scrollTop @container.scrollTop() + @pixelHeight - @container.prop("clientHeight") + 1 if @container.prop("clientHeight") < @pixelHeight
-    @pixelWidth = @container.prop("clientWidth")
+    @pixelWidth  = @container.prop("clientWidth")
     @pixelHeight = @container.prop("clientHeight")

     width = 1


Terminal: Cancel resize if parent does not exist

--- client/Terminal/src/Terminal.coffee
+++ client/Terminal/src/Terminal.coffee
@@ -190,7 +190,11 @@

   updateSize: (force = no) ->

+    return  unless @parent
+
     @updateAppSize()
+
+    return  unless @parent

     [swidth, sheight] = [@parent.getWidth(), @parent.getHeight()]

@@ -211,6 +215,8 @@

     { appView } = @getOptions()
     {width: charWidth, height: charHeight} = @getCharSizes()
+
+    return  unless appView.parent

     height = appView.parent.getHeight() - 24 # padding


Fix saveAs for multiple VMs

Fix saveAs for multiple VMs

--- client/app/MainApp/fs/fsfile.coffee
+++ client/app/MainApp/fs/fsfile.coffee
@@ -32,15 +32,19 @@

   saveAs:(contents, name, parentPath, callback)->

+    @vmName = FSHelper.getVMNameFromPath parentPath  if parentPath
     newPath = FSHelper.plainPath "#{parentPath}/#{name}"
     @emit "fs.saveAs.started"

-    FSHelper.ensureNonexistentPath "#{newPath}", @vmName, (err, response)=>
+    FSHelper.ensureNonexistentPath "#{newPath}", @vmName, (err, path)=>
       if err
-        callback? err, response
+        callback? err, path
         warn err
       else
-        newFile = FSHelper.createFileFromPath response
+        newFile = FSHelper.createFile
+          type   : 'file'
+          path   : path
+          vmName : @vmName
         newFile.save contents, (err, res)=>
           if err then warn err
           else


Kite: Added an event emitter into handleError.

--- client/app/MainApp/kite/kite.coffee
+++ client/app/MainApp/kite/kite.coffee
@@ -88,7 +88,9 @@
     @channel.exchange = resourceName
     @emit 'ready'

-  handleError: (err) -> error err
+  handleError: (err) ->
+    error err
+    @emit "KiteError", err

   handlePing: ->
     @channel.publish JSON.stringify


Fix memory leak in GitRepository and convert to CompositeDisposables

We were calling @unsubscribe with the TextBuffer, which previously
unsubscribed from that object. The problem is that we were no longer
subscribing to that object directly, but only adding subscriptions to
that object. This caused us to never unsubscribe from buffers.

--- src/git-repository.coffee
+++ src/git-repository.coffee
@@ -1,9 +1,8 @@
 {basename, join} = require 'path'

 _ = require 'underscore-plus'
-{Subscriber} = require 'emissary'
 EmitterMixin = require('emissary').Emitter
-{Emitter} = require 'event-kit'
+{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
 fs = require 'fs-plus'
 GitUtils = require 'git-utils'
 {deprecate} = require 'grim'
@@ -45,7 +44,6 @@
 module.exports =
 class GitRepository
   EmitterMixin.includeInto(this)
-  Subscriber.includeInto(this)

   @exists: (path) ->
     if git = @open(path)
@@ -75,6 +73,8 @@

   constructor: (path, options={}) ->
     @emitter = new Emitter
+    @subscriptions = new CompositeDisposable
+
     @repo = GitUtils.open(path)
     unless @repo?
       throw new Error("No Git repository found searching path: #{path}")
@@ -88,13 +88,15 @@

     refreshOnWindowFocus ?= true
     if refreshOnWindowFocus
-      {$} = require './space-pen-extensions'
-      @subscribe $(window), 'focus', =>
+      onWindowFocus = =>
         @refreshIndex()
         @refreshStatus()

+      window.addEventListener 'focus', onWindowFocus
+      @subscriptions.add new Disposable(-> window.removeEventListener 'focus', onWindowFocus)
+
     if @project?
-      @subscribe @project.eachBuffer (buffer) => @subscribeToBuffer(buffer)
+      @subscriptions.add @project.eachBuffer (buffer) => @subscribeToBuffer(buffer)

   # Public: Destroy this {GitRepository} object.
   #
@@ -109,7 +111,7 @@
       @repo.release()
       @repo = null

-    @unsubscribe()
+    @subscriptions.dispose()

   ###
   Section: Event Subscription
@@ -403,10 +405,13 @@
       if path = buffer.getPath()
         @getPathStatus(path)

-    @subscribe buffer.onDidSave(getBufferPathStatus)
-    @subscribe buffer.onDidReload(getBufferPathStatus)
-    @subscribe buffer.onDidChangePath(getBufferPathStatus)
-    @subscribe buffer.onDidDestroy => @unsubscribe(buffer)
+    bufferSubscriptions = new CompositeDisposable
+    bufferSubscriptions.add buffer.onDidSave(getBufferPathStatus)
+    bufferSubscriptions.add buffer.onDidReload(getBufferPathStatus)
+    bufferSubscriptions.add buffer.onDidChangePath(getBufferPathStatus)
+    bufferSubscriptions.add buffer.onDidDestroy =>
+      bufferSubscriptions.dispose()
+      @subscriptions.remove(bufferSubscriptions)

   # Subscribes to editor view event.
   checkoutHeadForEditor: (editor) ->


Glob it.

Glob it.

--- src/git-repository.coffee
+++ src/git-repository.coffee
@@ -485,6 +485,7 @@
       relativeProjectPaths = @project?.getPaths()
         .map (path) => @relativize(path)
         .filter (path) -> path.length > 0
+        .map (path) -> path + '/**'

       @statusTask?.terminate()
       @statusTask = Task.once @handlerPath, @getPath(), relativeProjectPaths, ({statuses, upstream, branch, submodules}) =>


Update location X text

Update location X text

--- static/src/scripts/app/views/TeamsMapView.coffee
+++ static/src/scripts/app/views/TeamsMapView.coffee
@@ -77,7 +77,7 @@
           scale: 6
           strokeColor: '#b21c26'
         title: "Location X"
-        html: """

Location X

The mystery Location X is somewhere in the Balkans!

"""
+        html: """

Location X

The mystery Location X is either Croatia or Slovenia!

"""

       google.maps.event.addListener @endMarker, 'click', (endMarker) =>
         @infowindow.setContent @endMarker.html


Solved some animation issues where it would not animate after stop

--- src/animation.coffee
+++ src/animation.coffee
@@ -51,9 +51,10 @@
 		# Set all the defaults
 		@time ?= 1000
 		@curve ?= "linear"
-		@precision ?= 40
+		@precision ?= 30

 		@curveValues = @_parseCurve @curve
+		@totalTime = @curveValues.length / @precision
 		@count = 0
 		# @animationId = utils.uuid()[..8]

@@ -66,7 +67,7 @@
 		@count++
 		@animationName = "framer-animation-#{@animationId}-#{@count}"

-		console.log "Animation.start #{@animationName}" if @debug
+

 		# See if we have other animations running on this view
 		# if @view._currentAnimations.length > 0
@@ -78,14 +79,13 @@
 		# this or give an option to disable it, but for now it makes sens because 1)
 		# you almost always want this and 2) we don't support simultaneous animations.

-		# TODO: This breaks stuff
-		# @view.animateStop()
+		# TODO: This doesn't work
+
+		@view.animateStop()
+
+		console.log "Animation.start #{@animationName} time = #{@a}" if @debug
+
 		@view._currentAnimations.push @
-
-		# if @_running is true
-		# 	throw Error "Animation.start #{@animationName} already running"
-		#
-		# @_running = true

 		# TODO: test if we are trying to animate something that cannot animate

@@ -129,9 +129,6 @@
 				if @propertiesA[k] isnt @propertiesB[k]
 					console.log " .#{k} #{@propertiesA[k]} -> #{@propertiesB[k]}"

-		totalTime = @curveValues.length / @precision
-
-		console.log "Animation.time = #{totalTime}" if @debug

 		# css.addStyle @keyFrameAnimationCSS
 		#
@@ -145,18 +142,18 @@
 			#{@keyFrameAnimationCSS}

 			.#{@animationName} {
-				-webkit-animation-duration: #{totalTime}s;
+				-webkit-animation-duration: #{@totalTime}s;
 				-webkit-animation-name: #{@animationName};
 				-webkit-animation-timing-function: linear;
 				-webkit-animation-fill-mode: both;
 			}"

 		@view.addClass @animationName
-
-		# if @graph
-		# 	@_graphView = @graphView @, 10, 20, 20, @time

 		finalize = =>
+
+			if @_canceled is true
+				return

 			# Copy over the end state properties for this animation so we can safely
 			# remove the animation.
@@ -171,6 +168,8 @@
 			callback?()
 			@_cleanup()

+			console.log "Animation.end #{@animationName}" if @debug
+
 			# console.profileEnd "Animation.start"

 		@view.once "webkitAnimationEnd", finalize
@@ -178,9 +177,10 @@

 	stop: =>

-		# console.log "Animation.stop #{@animationName}"
+		console.log "Animation.stop #{@animationName}" if @debug

 		@_running = false
+		@_canceled = true

 		@view.style["-webkit-animation-play-state"] = "paused"

@@ -196,6 +196,9 @@
 		@view.style = calculatedStyles

 		@_cleanup()
+
+
+		@view.style["-webkit-animation-play-state"] = "running"

 	reverse: =>

@@ -220,7 +223,7 @@
 		@view.removeClass @animationName
 		@emit "end"

-		@_graphView?.visible = false
+		# @_graphView?.visible = false

 		# console.log "_cleanup", @view._currentAnimations


Updated test for Parse compatibility

Updated test for Parse compatibility

--- test/knockback/collection-observable/test.coffee
+++ test/knockback/collection-observable/test.coffee
@@ -694,15 +694,15 @@
   it '17. Test auto-generate collections', (done) ->
     kb.statistics = new kb.Statistics() # turn on stats

-    models = (new kb.Contact({id: id}) for id in [0..4])
+    models = (new kb.Contact({id: id}) for id in [1..4])
     class PersonViewModel extends kb.ViewModel
     collection_observable = kb.collectionObservable({view_model: PersonViewModel})

     collection_observable.collection().reset(models)
-    assert.equal(collection_observable.collection().length, 5)
+    assert.equal(collection_observable.collection().length, 4)

     for view_models in collection_observable()
-      assert.ok(view_models.id() in [0..4])
+      assert.ok(!!view_models.date())
       assert.ok(view_models.model() instanceof kb.Contact)

     kb.release(collection_observable)


rating clean-up

rating clean-up

--- src/book-list.coffee
+++ src/book-list.coffee
@@ -7,6 +7,7 @@
 #   hubot booklist lookup <index> - retrieves book information
 #   hubot booklist - displays full booklist
 #   hubot booklist edit <index> <title> - edit book at index with new title
+#   hubot booklist review book <index> stars <rating> - rates the selected book
 #
 # Author:
 #   Thomas Gamble & Paul Gaffney
@@ -53,7 +54,10 @@

   robot.hear /booklist review book (\d{1,5}) stars (\d{1})/i, (res) ->
     index = res.match[1]
-    reviewRating = res.match[2]
+    reviewRating = parseInt(res.match[2], 10)
+
+    if reviewRating > 5
+      return emitString(res,"Ratings must be between 1 and 5")

     maxIndex = getBookList().length - 1
     if index > maxIndex
@@ -172,8 +176,12 @@
     book = getBookAtIndex(index)
     newRating = parseInt(newRating, 10)

-    currentAverage = book[BOOK.RATING].value
-    nbrOfReviews = book[BOOK.REVIEWCOUNT].value
+    currentAverage = 0
+    nbrOfReviews = 0
+
+    if book[BOOK.RATING] and book[BOOK.REVIEWCOUNT]
+      currentAverage = book[BOOK.RATING].value
+      nbrOfReviews = book[BOOK.REVIEWCOUNT].value

     newTotalOfAllRatings = currentAverage * nbrOfReviews + newRating

@@ -183,8 +191,6 @@

     book[BOOK.RATING].value = newAverage
     book[BOOK.REVIEWCOUNT].value = nbrOfReviews
-
-

   getLastBook = ->
     booklist = getBookList()
@@ -205,7 +211,7 @@
       fields: [
         { short: true, title: "Author", value: book[BOOK.AUTHOR].value }
         { short: true, title: "Category", value: book[BOOK.CATEGORY].value }
-        { short: true, title: "Average Rating", value: book[BOOK.RATING].value }
+        { short: true, title: "Average Rating", value: if book[BOOK.RATING] then book[BOOK.RATING].value else "0"}
       ]

     cb(payload, null)


Equation builder, equals sign replaces operators Fixes #4713610

--- math/expression_components_spec.coffee
+++ math/expression_components_spec.coffee
@@ -20,6 +20,7 @@
       @comp.isVariable()
       @comp.isFraction()
       @comp.isExponentiation()
+      @comp.isRoot()

 describe "Expression Components", ->
   beforeEach ->


Search completion; add space after inserted text.

--- pages/vomnibar.coffee
+++ pages/vomnibar.coffee
@@ -81,7 +81,7 @@
     # and revert when not.  This allows the user to select a suggestion and then continue typing.
     if 0 <= @selection and @completions[@selection].insertText?
       @previousInputValue ?= @input.value
-      @input.value = @completions[@selection].insertText
+      @input.value = @completions[@selection].insertText + " "
     else if @previousInputValue?
         @input.value = @previousInputValue
         @previousInputValue = null


Fixes kill task being ran on reload

Fixes kill task being ran on reload

--- tasks/kill.coffee
+++ tasks/kill.coffee
@@ -5,33 +5,21 @@
 killed = false

 gulp.task 'kill:darwin64', (done) ->
-  # Skip if already killed and is a watch task
-  if killed and 'watch:' in process.argv
-    return done()
-
+  return done() if killed
   cp.exec 'pkill -9 ' + manifest.productName, -> done()
   killed = true

 gulp.task 'kill:linux32', (done) ->
-  # Skip if already killed and is a watch task
-  if killed and 'watch:' in process.argv
-    return done()
-
+  return done() if killed
   cp.exec 'pkill -9 ' + manifest.name, -> done()
   killed = true

 gulp.task 'kill:linux64', (done) ->
-  # Skip if already killed and is a watch task
-  if killed and 'watch:' in process.argv
-    return done()
-
+  return done() if killed
   cp.exec 'pkill -9 ' + manifest.name, -> done()
   killed = true

 gulp.task 'kill:win32', (done) ->
-  # Skip if already killed and is a watch task
-  if killed and 'watch:' in process.argv
-    return done()
-
+  return done() if killed
   cp.exec 'taskkill /F /IM ' + manifest.productName + '.exe', -> done()
   killed = true


Fix issues with the time that the night mode kicks in

--- source/scripts/views/night.coffee
+++ source/scripts/views/night.coffee
@@ -30,14 +30,12 @@
         # turn it off 7pm today
         nextRun.setHours 21

-      console.log nextRun
-
       setTimeout(=>
         @el.toggleClass 'dark'
         setInterval(=>
           @el.toggleClass 'dark'
         , 43200000)
-      , nextRun.getTime())
+      , nextRun.getTime() - new Date().getTime())

     if value is 'dark' or value is 'auto' and (hours > 19 or hours < 7)
       @el.addClass 'dark'


disable sprite apply / direct action

disable sprite apply / direct action

--- src/layout/sprites.coffee
+++ src/layout/sprites.coffee
@@ -44,7 +44,7 @@
     for sprite_dict in sprites.all()
       set_shape(sprite_dict, shapes)
       child = world.add_child sprite_dict
-      child.handle_event 'apply'
+      # hild.handle_event 'apply'
       world.send 'inspect', child

   # selection


added shorten operation for #64 (appears to have broken OAuth but not sure why)

--- src/lib/background.coffee
+++ src/lib/background.coffee
@@ -19,17 +19,17 @@
     key:      'PREDEFINED.00001'
     readOnly: yes
     shortcut: 'U'
-    title:    i18n.get 'copy_url'
+    title:    i18n.get 'default_template_url'
     usage:    0
   ,
-    content:  '{short}'
+    content:  '{#shorten}{url}{/shorten}'
     enabled:  yes
     image:    'tmpl_link'
     index:    1
     key:      'PREDEFINED.00002'
     readOnly: yes
     shortcut: 'S'
-    title:    i18n.get 'copy_short'
+    title:    i18n.get 'default_template_short'
     usage:    0
   ,
     content:  "<a href=\"{{url}}\"{#anchorTarget}
@@ -41,7 +41,7 @@
     key:      'PREDEFINED.00003'
     readOnly: yes
     shortcut: 'A'
-    title:    i18n.get 'copy_anchor'
+    title:    i18n.get 'default_template_anchor'
     usage:    0
   ,
     content:  '{#encode}{url}{/encode}'
@@ -51,7 +51,7 @@
     key:      'PREDEFINED.00004'
     readOnly: yes
     shortcut: 'E'
-    title:    i18n.get 'copy_encoded'
+    title:    i18n.get 'default_template_encoded'
     usage:    0
   ,
     content:  '[url={url}]{title}[/url]'
@@ -61,7 +61,7 @@
     key:      'PREDEFINED.00005'
     readOnly: yes
     shortcut: 'B'
-    title:    i18n.get 'copy_bbcode'
+    title:    i18n.get 'default_template_bbcode'
     usage:    0
   ,
     content:  '[{title}]({url})'
@@ -71,7 +71,7 @@
     key:      'PREDEFINED.00006'
     readOnly: yes
     shortcut: 'M'
-    title:    i18n.get 'copy_markdown'
+    title:    i18n.get 'default_template_markdown'
     usage:    0
 ]
 # Extension ID being used by Template.
@@ -89,6 +89,8 @@
   substring: 'Linux'
   title:     'Linux'
 ]
+# Regular expression used to perform simple URL validation.
+R_VALID_URL       = /^https?:\/\/\S+\.\S+/i
 # Extension ID of the production version of Template.
 REAL_EXTENSION_ID = 'dcjnfaoifoefmnbhhlbppaebgnccfddf'
 # List of URL shortener services supported by Template.
@@ -352,16 +354,15 @@
   # Info requests are simple, just send some useful information back. Done!
   if request.type in ['info', 'version']
     return sendResponse? id: EXTENSION_ID, version: ext.version
-  data             = null
-  output           = null
-  popup            = chrome.extension.getViews(type: 'popup')[0]
-  popupItems       = if popup then $ '#item',    popup.document else $()
-  popupLoader      = if popup then $ '#loadDiv', popup.document else $()
-  shortCalled      = no
-  shortPlaceholder = "short#{utils.random 1000, 99999}"
-  tab              = null
-  template         = null
-  windowId         = null
+  data        = null
+  output      = null
+  popup       = chrome.extension.getViews(type: 'popup')[0]
+  popupItems  = if popup then $ '#item',    popup.document else $()
+  popupLoader = if popup then $ '#loadDiv', popup.document else $()
+  shortenMap  = {}
+  tab         = null
+  template    = null
+  windowId    = null
   # Create a runner to manage this asynchronous mess.
   runner = new utils.Runner()
   runner.push chrome.windows, 'getCurrent', (win) ->
@@ -373,13 +374,25 @@
       runner.next()
     ]
   runner.push null, ->
-    # Called whenever the `short` tag is found to indicate that the URL needs
-    # shortened.
-    # Replace the `short` tag with the unique placeholder so that it can be
-    # rendered again later with the short URL.
-    shortCallback = ->
-      shortCalled = yes
-      "{#{shortPlaceholder}}"
+    # Called whenever the `shorten` (or previously `short`) tag is found to
+    # indicate that a URL needs shortened.
+    # Replace the tag with the unique placeholder so that it can be rendered
+    # again later with the short URL.
+    shortCallback = (text, render) ->
+      text = if text then render text else @url
+      url  = text.trim()
+      # If `url` doesn't appear to be a valid URL so just return the rendered
+      # `text`.
+      return text unless R_VALID_URL.test url
+      placeholder = key for own key, value of shortenMap when value is url
+      unless placeholder?
+        placeholder = utils.keyGen '', null, 's', no
+        shortenMap[placeholder] = url
+        # Sections are re-rendered so the context must have a property for the
+        # placeholder the replaces itself with itself so that it still when it
+        # comes to replacing with the shortened URL.
+        @[placeholder] = "{#{placeholder}}"
+      "{#{placeholder}}"
     # If the popup is currently displayed, hide the template list and show a
     # loading animation.
     if popup
@@ -413,16 +426,16 @@
       $.extend data, template: template
       if template.content
         output = Mustache.to_html template.content, data
-        # If any `short` tags are found and replaced, the URL shortener service
-        # needs to be called in order to replace the placeholder with the real
-        # short URL.
-        if shortCalled
-          runner.next()
-        else
+        # If any `shorten` (or old `short`) tags are found and replaced, the
+        # URL shortener service needs to be called in order to replace any
+        # placeholders with their corresponding short URL.
+        if $.isEmptyObject shortenMap
           runner.finish
             contents: output
             success:  yes
             template: template
+        else
+          runner.next()
       else
         # Display the *empty contents* error message if the contents of the
         # template itself is empty.
@@ -431,24 +444,22 @@
           success:  yes
           template: template
     ]
-  runner.pushPacked null, callUrlShortener, ->
-    [data.url, (response) ->
-      if response.success and response.shortUrl
-        # Short URL was successfully returned so re-render the output.
-        newData = {}
-        newData[shortPlaceholder] = response.shortUrl
-        runner.finish
-          contents: Mustache.to_html output, newData
-          success:  yes
-          template: template
-      else
-        # Aw man, something went wrong. Let the user down gently.
-        response.message ?= i18n.get 'shortener_error', response.shortener
-        runner.finish
-          message:  response.message
-          success:  no
-          template: template
-    ]
+  runner.push null, callUrlShortener, shortenMap, (response) ->
+    if response.success
+      updateUrlShortenerUsage response.service.name, response.oauth
+      # Request to the URL shortener service was successful so re-render the
+      # output.
+      runner.finish
+        contents: Mustache.to_html output, shortenMap
+        success:  yes
+        template: template
+    else
+      # Aw man, something went wrong. Let the user down gently.
+      response.message ?= i18n.get 'shortener_error', response.service.title
+      runner.finish
+        message:  response.message
+        success:  no
+        template: template
   runner.run (result) ->
     type  = request.type[0].toUpperCase() + request.type.substr 1
     value = request.data.key.charCodeAt 0 if request.type is 'shortcut'
@@ -750,9 +761,12 @@
       (text, render) ->
         url.segment(parseInt render(text), 10) ? ''
     segments:              url.segment()
+    # Deprecated since 1.0.0, use `shorten` instead.
     short:                 ->
-      shortCallback?()
+      shortCallback
     shortcuts:             store.get 'shortcuts'
+    shorten:               ->
+      shortCallback
     title:                 title or url.attr 'source'
     toolbarclose:          toolbar.close
     # Deprecated since 1.0.0, use the inverse of `toolbarPopup` instead.
@@ -1085,65 +1099,62 @@
 # URL shortener functions
 # -----------------------

-# Call the active URL shortener service for `url` in order to obtain the
-# relevant short URL.
-# `callback` will be called with the result once it has been received from the
-# URL shortener service.
-callUrlShortener = (url, callback) ->
-  service = getActiveUrlShortener()
+# Call the active URL shortener service for each URL in `map` in order to
+# obtain their corresponding short URLs.
+# `callback` will be called with the result once all URLs have been shortened
+# or an error is encountered.
+callUrlShortener = (map, callback) ->
+  service  = getActiveUrlShortener()
+  title    = service.title
+  endpoint = service.url()
+  # Ensure the service URL exists in case it is user-defined (e.g. YOURLS).
+  unless endpoint
+    return callback
+      message: i18n.get 'shortener_config_error', title
+      service: service
+      success: no
   # Create a runner to control the dependencies in the asynchronous processes.
   runner = new utils.Runner()
-  if service.oauth and service.isOAuthEnabled()
+  if service.oauth? and service.isOAuthEnabled()
     runner.push service.oauth, 'authorize', ->
-      runner.next();
-  runner.push null, ->
-    oauth = no
-    sUrl  = service.url()
-    title = service.title
-    unless sUrl
-      # Should never happen... Really just a sanity check.
-      return runner.finish
-        message:   i18n.get 'shortener_config_error', title
-        shortener: title
-        success:   no
-    try
-      params = service.getParameters(url) or {}
-      # Build the HTTP request for the URL shortener service.
-      req = new XMLHttpRequest()
-      req.open service.method, "#{sUrl}?#{$.param params}", yes
-      req.setRequestHeader 'Content-Type', service.contentType
-      # Setup the OAuth header, if required.
-      if service.oauth and service.isOAuthEnabled()
-        oauth = yes
-        req.setRequestHeader 'Authorization',
-          service.oauth.getAuthorizationHeader sUrl, service.method, params
-      # Wait for the response and let the service handle it before passing the
-      # result to `callback` via the runner.
-      req.onreadystatechange = ->
-        if req.readyState is 4
-          if req.status is 200
-            updateUrlShortenerUsage service.name, oauth
-            runner.finish
-              shortUrl:  service.output req.responseText
-              shortener: title
-              success:   yes
-          else
-            # Something went wrong so let's tell the user.
-            runner.finish
-              message:   i18n.get 'shortener_error', title
-              shortener: title
-              success:   no
-      # Finally, send the HTTP request.
-      req.send service.input url
-    catch error
-      # Aw snap! Notify the user via `callback`.
-      log.error error
-      runner.finish
-        message:   i18n.get 'shortener_error', title
-        shortener: title
-        success:   no
+      runner.next()
+  for own placeholder, url of map
+    do (placeholder, url) ->
+      runner.push null, ->
+        oauth  = no
+        params = service.getParameters(url) or {}
+        # Build the HTTP request for the URL shortener service.
+        xhr = new XMLHttpRequest()
+        xhr.open service.method, "#{endpoint}?#{$.param params}", yes
+        xhr.setRequestHeader 'Content-Type', service.contentType
+        # Setup OAuth for the request when required.
+        if service.oauth and service.isOAuthEnabled()
+          oauth = yes
+          xhr.setRequestHeader 'Authorization',
+            service.oauth.getAuthorizationHeader(endpoint,
+              service.method, params)
+        xhr.onreadystatechange = ->
+          # Wait for the response and let the service handle it before passing
+          # the result to `callback` via the runner.
+          if xhr.readyState is 4
+            if xhr.status is 200
+              map[placeholder] = service.output xhr.responseText
+              runner.next
+                oauth:   oauth
+                success: yes
+            else
+              # Something went wrong so let's tell the user.
+              runner.finish
+                message: i18n.get 'shortener_detailed_error', [title, url]
+                success: no
+        # Finally, send the HTTP request.
+        xhr.send service.input url
   runner.run (result = {}) ->
-    callback? result
+    callback
+      message: result.message
+      oauth:   result.oauth
+      service: service
+      success: result.success

 # Retrieve the active URL shortener service.
 getActiveUrlShortener = ->