Move items around, comment correctly

Move items around, comment correctly

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -93,6 +93,8 @@
         pretty: true
         data:
           timestamp: "<%= new Date().getTime() %>"
+
+      # Local build
       build:
         options:
           data:
@@ -107,13 +109,6 @@
           ext: '.html'
         }]

-      pages:
-        options:
-          data:
-            sitepath: '/UI.Library/css'
-
-        files: '<%= jade.build.files %>'
-
       styleguide:
         options:
           data:
@@ -121,6 +116,15 @@
             stylepath: '/styleguide'
         files:
           'src/styleguide-template/index.html' : 'src/jade/styleguide.jade'
+
+      # Github pages build
+      pages:
+        options:
+          data:
+            sitepath: '/UI.Library/css'
+
+        files: '<%= jade.build.files %>'
+

       styleguidePages:
         options:


fix(grunt): update config for new version

BREAKING CHANGE: Now it will no longer bust favicons and other assets outside the `<%= amsf.user.assets %>`.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -267,25 +267,18 @@

     cacheBust:
       options:
+        algorithm: "md5"
+        assets: ["<%= amsf.user.assets %>/**/*"]
+        baseDir: "<%= config.dist %>"
+        deleteOriginals: true
         encoding: "utf8"
-        filters: {
-          "link[rel*=icon]": ->
-            @attribs.href
-        }
-        ignorePatterns: [
-          "no-cache-bust"
-        ]
-        algorithm: "md5"
         length: 8
-        deleteOriginals: true
-
-      dist:
-        files: [
-          expand: true
-          baseDir: "<%= config.dist %>"
+
+      dist:
+        files: [
+          expand: true
           cwd: "<%= config.dist %>"
           src: "**/*.html"
-          dest: "<%= config.dist %>"
         ]

     usebanner:


@_responseSent handled by original ::_requestFinished

--- chrome/content/zotero-better-bibtex/zotero-better-bibtex.coffee
+++ chrome/content/zotero-better-bibtex/zotero-better-bibtex.coffee
@@ -466,10 +466,6 @@
       try
         Zotero.debug("Zotero.Server.DataListener::_requestFinished: handling #{typeof promise} promise? #{typeof promise?.then}")
         if typeof promise?.then == 'function'
-          #if @_responseSent
-          #  Zotero.debug("Request already finished; not sending another response")
-          #  return
-          #@_responseSent = true
           promise.then((response) =>
             throw new Error("Zotero.Server.DataListener::_requestFinished: circular promise!") if typeof response?.then == 'function'
             original.apply(@, [response])


Fix Grunt start task

Fix Grunt start task

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -58,7 +58,7 @@
   ]

   grunt.registerTask 'start', [
-    'build'
+    'dev'
     'publish'
     'coffee:server'
     'express:dev'


transaction around schema update

transaction around schema update

--- chrome/content/zotero-better-bibtex/zotero-better-bibtex.coffee
+++ chrome/content/zotero-better-bibtex/zotero-better-bibtex.coffee
@@ -198,6 +198,8 @@
   Zotero.DB.query("insert or replace into betterbibtex.schema (lock, version) values ('schema', ?)", [@release])

   return if installed == installing && !(['cache', 'keys', 'autoexport', 'exportoptions'].some((table) => !@SQLColumns(table)))
+
+  Zotero.DB.beginTransaction()

   @flash('Better BibTeX: updating database', 'Updating database, this could take a while')

@@ -287,7 +289,7 @@

   #@foreign_keys(true)

-  return
+  Zotero.DB.commitTransaction()

 Zotero.BetterBibTeX.findKeysSQL = "select i.itemID as itemID, idv.value as extra
                   from items i


Remove the more cache dirs

Remove the more cache dirs

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -88,6 +88,11 @@
                 options:
                     timestamp: true
                     exclude: [
+                      'css'
+                      'js'
+                      'images'
+                      'video'
+                      'slides'
                       'bower_components'
                       'bower_components/reveal.js/plugin/leap'
                       'bower_components/reveal.js/test'
@@ -95,6 +100,29 @@
                       'bower_components/reveal.js/test/examples/assets'
                       'bower_components/reveal.js/plugin/math'
                       'bower_components/reveal.js/plugin/print-pdf'
+                      'bower_components/reveal-highlight-themes'
+                      'bower_components/reveal-highlight-themes/styles'
+                      'bower_components/reveal.js'
+                      'bower_components/reveal.js/css'
+                      'bower_components/reveal.js/css/print'
+                      'bower_components/reveal.js/css/theme'
+                      'bower_components/reveal.js/css/theme/source'
+                      'bower_components/reveal.js/css/theme/template',
+                      'bower_components/reveal.js/js'
+                      'bower_components/reveal.js/lib'
+                      'bower_components/reveal.js/lib/css'
+                      'bower_components/reveal.js/lib/font'
+                      'bower_components/reveal.js/lib/js'
+                      'bower_components/reveal.js/plugin'
+                      'bower_components/reveal.js/plugin/highlight'
+                      'bower_components/reveal.js/plugin/markdown'
+                      'bower_components/reveal.js/plugin/multiplex'
+                      'bower_components/reveal.js/plugin/notes'
+                      'bower_components/reveal.js/plugin/notes-server'
+                      'bower_components/reveal.js/plugin/postmessage'
+                      'bower_components/reveal.js/plugin/remotes'
+                      'bower_components/reveal.js/plugin/search'
+                      'bower_components/reveal.js/plugin/zoom-js'
                     ]
                 src: [
                     'css/**'


bypass Zotero translator save for #238

bypass Zotero translator save for #238

--- chrome/content/zotero-better-bibtex/zotero-better-bibtex.coffee
+++ chrome/content/zotero-better-bibtex/zotero-better-bibtex.coffee
@@ -10,6 +10,8 @@

 Zotero.BetterBibTeX.error = (msg...) ->
   @_log.apply(@, [0].concat(msg))
+Zotero.BetterBibTeX.warn = (msg...) ->
+  @_log.apply(@, [1].concat(msg))

 Zotero.BetterBibTeX.debug_off = ->
 Zotero.BetterBibTeX.debug = Zotero.BetterBibTeX.debug_on = (msg...) ->
@@ -944,20 +946,18 @@
     destFile = Zotero.getTranslatorsDirectory()
     destFile.append(fileName)

-    # JSON.stringify (FF 3.5.4 and up) has the benefit of indenting JSON
     metadataJSON = JSON.stringify(header, null, "\t")

     if translator and destFile.equals(translator.file) and destFile.exists()
-      msg = 'Overwriting translator with same filename \'' + fileName + '\''
-      Zotero.debug(msg, 1)
-      Zotero.debug(header, 1)
+      msg = "Overwriting translator with same filename '#{fileName}'"
+      Zotero.BetterBibTeX.warn(msg, , header)
       Components.utils.reportError(msg + ' in Zotero.BetterBibTeX.load()')

     translator.file.remove(false) if translator and translator.file.exists()

-    Zotero.debug 'Saving translator \'' + metadata.label + '\''
-
-    Zotero.File.putContents(destFile, metadataJSON + '\n\n' + code)
+    Zotero.BetterBibTeX.log("Saving translator '#{header.label}'"
+
+    Zotero.File.putContents(destFile, metadataJSON + "\n\n" + code)

     @debug('translator.load', translator, 'succeeded')
   catch err


fixes less livereload

fixes less livereload

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -14,6 +14,36 @@
       base: 'build/<%= relativePath %>'
     src: ['**']

+  config.less =
+    main:
+      files: [
+        expand: true
+        cwd: 'src/assets/stylesheets/'
+        src: ['academy.less']
+        dest: "build/assets/stylesheets/"
+        ext: '.css'
+      ]
+
+  config.watch.less =
+    options:
+      livereload: false
+    files: ['src/assets/stylesheets/**/*.less']
+    tasks: ['less']
+
+  config.watch.main =
+    files: ['src/assets/script/**/*.js',
+            'src/assets/images/**/*',
+            'src/assets/libs/**/*',
+            'src/index.html']
+    tasks: ['copy:main', 'getTags', 'copy:dev']
+
+  config.copy.main.files = [
+    expand: true
+    cwd: 'src/'
+    src: ['**']
+    dest: "build/"
+  ]
+
   tasks =
     # Building block tasks
     build: ['clean', 'copy:main', 'copy:pkg', 'coffee', 'less']


Add eslint to toolchain

Add eslint to toolchain

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -403,7 +403,7 @@
             options:
                 atBegin: true
             files: ["src/*.js", "test/unit/*.js", "demo/**/*.js"]
-            tasks: ["jshint:beforeConcat"]
+            tasks: ["jshint:beforeConcat", "eslint:dev"]

     yabs:
         release:
@@ -440,6 +440,7 @@
   grunt.registerTask "tabfix", ["exec:tabfix"]
   grunt.registerTask "test", [
       "jshint:beforeConcat",
+      "eslint:dev",
       # "csslint",
       # "htmllint",
       "qunit:develop"


removing phantomjs from travis-ci (times out)

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -475,10 +475,10 @@
   grunt.registerTask cmd, splitTasks "shell:#{cmd}" for cmd of gruntConfig.shell # shortcut to all "shell:cmd"
   grunt.registerTask shortCut, splitTasks tasks for shortCut, tasks of {
      # generic shortcuts
-     "default":   "build dev min test testDev testMin mocha:AMD run"
-     "release":   "build urequire:AMD urequire:AMDunderscore dev min test testDev testMin mocha run"
-     "examples":  "urequire:AMD urequire:UMDplainReplaceDep urequire:AMDunderscore urequire:UMDunderscore urequire:nodejsCompileAndCopy"
-     "all":       "build dev min test testDev testMin mocha examples run"
+     "default":   "build test dev testDev min testMin run"
+     "release":   "build test dev testDev min testMin mocha urequire:AMD urequire:AMDunderscore run"
+     "examples":  "urequire:AMD urequire:AMDunderscore urequire:UMDplainReplaceDep urequire:UMDunderscore urequire:nodejsCompileAndCopy"
+     "all":       "build test dev testDev min testMin mocha examples run"
      "build":     "urequire:UMD"
      "dev":       "urequire:dev concat:dev"
      "min":       "urequire:min concat:min"


assigning server port via configuration

assigning server port via configuration

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -215,7 +215,7 @@
 		# Open the Express app in the default browser
 		open:
 			server:
-				url: 'http://localhost:3005'
+				url: 'http://localhost:<%= express.livereload.options.port %>'

 		# Restart server when server sources have changed, notify all browsers on change.
 		regarde:


Change gruntfile to not compile dependencies

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -6,12 +6,6 @@
       options:
         targetDir: "./vendor"
       install: { }
-
-    curl:
-      'vendor/cartograph.js':     'https://raw.githubusercontent.com/dawanda/cartograph/master/lib/cartograph.js'
-      'vendor/eventspitter.js':   'https://raw.githubusercontent.com/lucaong/eventspitter/master/lib/eventspitter.js'
-      'vendor/loud-accessors.js': 'https://raw.githubusercontent.com/lucaong/loud-accessors/master/lib/loud-accessors.js'
-      'vendor/submachine.js':     'https://raw.githubusercontent.com/lucaong/submachine/master/lib/submachine.js'

     meta:
       banner:
@@ -25,7 +19,7 @@
         options:
           join: true
         files:
-          'dist/lib.js': [
+          'dist/lanejs.js': [
             'src/namespace.js.coffee'
             'src/module.js.coffee'
             'src/timeout.js.coffee'
@@ -43,14 +37,6 @@
             'src/url_builder.js.coffee'
           ]

-    concat:
-      all:
-        files:
-          'dist/lanejs.js': [
-            'vendor/**/*.js'
-            'dist/lib.js'
-          ]
-
     uglify:
       all:
         options:
@@ -62,7 +48,7 @@
       options:
         basePath: ''
         configFile: 'karma.conf.js'
-        loadFiles: [ 'dist/lanejs.js', 'spec/**/*.coffee' ]
+        loadFiles: [ 'vendor/**/*.js', 'dist/lanejs.js', 'spec/**/*.coffee' ]
       continuous: { }

     clean: ["dist/lib.js"]
@@ -83,4 +69,4 @@

   # Run with grunt release, grunt release:minor

-  grunt.registerTask 'build',   ['bower:install', 'curl', 'coffee', 'concat', 'clean', 'uglify', 'karma:continuous']+  grunt.registerTask 'build',   ['bower:install', 'coffee', 'clean', 'uglify', 'karma:continuous']


chore: update jshint config to be more strict

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -65,6 +65,8 @@

       options:
         quotmark: 'single'
+        bitwise: true
+        indent: 2
         camelcase: true
         strict: true
         trailing: true
@@ -73,10 +75,13 @@
         immed: true
         latedef: true
         newcap: true
+        noempty: true
+        unused: true
         noarg: true
         sub: true
         undef: true
-        boss: true
+        maxdepth: 4
+        maxlen: 100
         globals: {}

   grunt.loadTasks 'tasks'


Remove select from gruntfile

Remove select from gruntfile

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -12,7 +12,6 @@
           'drop.js': 'drop.coffee'

           'tooltip.js': 'tooltip.coffee'
-          'select.js': 'select.coffee'

           'markAttachment.js': 'markAttachment.coffee'
           'docs/js/intro.js': 'docs/coffee/intro.coffee'


fix(Grunt): Fixed compiled files not in lower case

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -58,7 +58,7 @@
         src: "<%= buildConf.example %>"

       deployAppJs:
-        dest: "lib/<%= pkg.name %>.js"
+        dest: "lib/<%= pkg.name.toLowerCase() %>.js"
         src: "<%= buildConf.full %>"

       modulesJs:
@@ -178,8 +178,12 @@

     uglify:
       dist:
-        files:
-          "lib/<%= pkg.name %>.min.js": "lib/<%= pkg.name %>.js"
+        files: [
+          {
+            src: "lib/<%= pkg.name.toLowerCase() %>.js"
+            dest: "lib/<%= pkg.name.toLowerCase() %>.min.js"
+          }
+        ]

     #
     # watch section


Increase max cyclomatic complexity to 15

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -98,7 +98,7 @@
           value: 120
         cyclomatic_complexity:
           level: 'error'
-          value: 14
+          value: 15

       source: ['src/**/*.coffee']
       grunt: 'Gruntfile.coffee'


Remove old method of installing local settings

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -79,9 +79,6 @@
 	grunt.registerTask 'run', "Run all of the sharelatex processes", ['concurrent:all']
 	grunt.registerTask 'run:all', 'run'

-	grunt.registerTask 'install:config', "Install a custom config from a git repository (set SHARELATEX_CONFIG_REPO to the repository location)", () ->
-		Helpers.installCustomConfig @async()
-
 	grunt.registerTask 'help', 'Display this help list', 'availabletasks'
 	grunt.registerTask 'default', 'run'

@@ -130,14 +127,3 @@
 		proc.on "close", () ->
 			callback()

-	installCustomConfig: (callback = (error) ->) ->
-		if !process.env.SHARELATEX_CONFIG_REPO?
-			return callback(new Error("Please set the SHARELATEX_CONFIG_REPO enviroment variable to point to a git repository."))
-
-		rimraf "config-local", (error) ->
-			Helpers.cloneGitRepo process.env.SHARELATEX_CONFIG_REPO, "config-local", (error) ->
-				return callback(error) if error?
-				for file in fs.readdirSync("config-local")
-					unless file == ".git"
-						fs.symlinkSync(Path.resolve("config-local/#{file}"), "config/#{file}")
-				callback()


Comment out debug log when loading all site options.

--- src/options.coffee
+++ src/options.coffee
@@ -8,7 +8,7 @@
 	chrome.storage.sync.get null, (items) ->
 		for site, optjson of items
 			continue if site == config.options_key
-			console.log "#{site} #{optjson}"
+			# console.log "#{site} #{optjson}"
 			opt = JSON.parse optjson
 			site_opt.append "
" +
 				"#{site}	" +


Cache incompatible packages based on electron version

This will cause us to re-check all packages when upgrading the version
of Electron.

--- src/package.coffee
+++ src/package.coffee
@@ -665,7 +665,8 @@
     "installed-packages:#{@name}:#{@metadata.version}:build-error"

   getIncompatibleNativeModulesStorageKey: ->
-    "installed-packages:#{@name}:#{@metadata.version}:incompatible-native-modules"
+    electronVersion = process.versions['electron'] ? process.versions['atom-shell']
+    "installed-packages:#{@name}:#{@metadata.version}:electron-#{electronVersion}:incompatible-native-modules"

   # Get the incompatible native modules that this package depends on.
   # This recurses through all dependencies and requires all modules that


Better print output for animations

Better print output for animations

--- framer/Animation.coffee
+++ framer/Animation.coffee
@@ -4,7 +4,7 @@

 {Config} = require "./Config"
 {Defaults} = require "./Defaults"
-{EventEmitter} = require "./EventEmitter"
+{BaseClass} = require "./BaseClass"

 {LinearAnimator} = require "./Animators/LinearAnimator"
 {BezierCurveAnimator} = require "./Animators/BezierCurveAnimator"
@@ -37,7 +37,7 @@

 # Todo: this would normally be BaseClass but the properties keyword
 # is not compatible and causes problems.
-class exports.Animation extends EventEmitter
+class exports.Animation extends BaseClass

 	constructor: (options={}) ->

@@ -65,6 +65,9 @@
 		@_originalState = @_currentState()
 		@_repeatCounter = @options.repeat

+	@define "isAnimating",
+		get: -> @ in @options.layer.context.animations
+
 	start: =>

 		if @options.layer is null
@@ -270,6 +273,10 @@

 		return animatableProperties

+	toInspect: ->
+		return "<#{@constructor.name} id:#{@id} isAnimating:#{@isAnimating} [#{_.keys(@options.properties)}]>"
+
+
 	##############################################################
 	## EVENT HELPERS


Made incorrect guess go to next question

--- private/coffee/game.coffee
+++ private/coffee/game.coffee
@@ -1,6 +1,8 @@
 shuffle = require 'shuffle-array'
 ucfirst = require 'ucfirst'
 sets = require 'simplesets'
+
+delayBetweenQuestions = 500

 class Game
   constructor: (data, @$page) ->
@@ -67,21 +69,29 @@

   # Checks if a guess is correct
   checkGuess: ->
-    @updateGuess()
-    if @guessIsCorrect()
-      @correctGuessIndices.push @currentQuestionIndex
-      # Make name flash green
-      @$name.addClass('correct')
-      # Delay next question
-      setTimeout =>
-        @$page.find('.progress .current').text(++@score)
-        @nextQuestion()
-      , 500
-    else # incorrect
-      if @guess.length == @getQuestion().firstName.length
-        @$name.addClass('incorrect')
-      else
-        @$name.removeClass('incorrect')
+    if !@transitioning
+      @updateGuess()
+      if @guessIsCorrect()
+        @correctGuessIndices.push @currentQuestionIndex
+        # Make name flash green
+        @$name.addClass('correct')
+        @transitioning = true
+        # Delay next question
+        setTimeout =>
+          @transitioning = false
+          @$page.find('.progress .current').text(++@score)
+          @nextQuestion()
+        , delayBetweenQuestions
+      else # incorrect
+        firstName = @getQuestion().firstName
+        if @guess.length == firstName.length
+          @$name.addClass('incorrect')
+          @transitioning = true
+          @$name.text(firstName)
+          setTimeout =>
+            @transitioning = false
+            @nextQuestion()
+          , delayBetweenQuestions * 2

   # Updates the guess state
   updateGuess: ->


sort log entries in reverse order

sort log entries in reverse order

--- packages/robbinsd:simplelog/robbinsd:simplelog-templates.coffee
+++ packages/robbinsd:simplelog/robbinsd:simplelog-templates.coffee
@@ -1,5 +1,5 @@
 Template.simpleLogs.helpers
-    logItems: -> Logs.collection.find()
+    logItems: -> Logs.collection.find({}, sort: timestamp: -1)

 Template.logItem.helpers
     raw: -> JSON.stringify @, null, 2


catch errors in digest

catch errors in digest

--- src/changeDetector.coffee
+++ src/changeDetector.coffee
@@ -397,6 +397,22 @@
     return

+displayError = (e, cd, w, option) ->
+    args =
+        src: w.src
+        scope: cd.scope
+        locals: cd.locals
+    if w.el
+        args.element = w.el
+    if option is 1
+        text = '$scan, error in callback: '
+    else
+        text = '$scan, error in expression: '
+    alight.exceptionHandler e, text + w.src, args
+
+ErrorValue = ->
+
+
 scanCore = (topCD, result) ->
     root = topCD.root
     extraLoop = false
@@ -410,16 +426,17 @@
     index = 0
     cd = topCD
     while cd
-        scope = result.scope = cd.scope
+        scope = cd.scope
         locals = cd.locals

         # default watchers
         total += cd.watchList.length
         for w in cd.watchList.slice()
-            result.src = w.src
-            result.element = w.el
             last = w.value
-            value = w.exp locals
+            try
+                value = w.exp locals
+            catch e
+                value = ErrorValue
             if last isnt value
                 mutated = false
                 if w.isArray
@@ -449,20 +466,27 @@

                 if mutated
                     mutated = false
-                    changes++
-
-                    # fire
-                    if w.el
-                        if w.ea
-                            w.el.setAttribute w.ea, value
-                        else
-                            w.el.nodeValue = value
+
+                    if value is ErrorValue
+                        displayError e, cd, w
                     else
-                        if last is watchInitValue
-                            last = undefined
-                        if w.callback.call(scope, value, last) isnt '$scanNoChanges'
-                            if w.extraLoop
-                                extraLoop = true
+                        changes++
+
+                        try
+                            if w.el
+                                if w.ea
+                                    w.el.setAttribute w.ea, value
+                                else
+                                    w.el.nodeValue = value
+                            else
+                                if last is watchInitValue
+                                    last = undefined
+                                if w.callback.call(scope, value, last) isnt '$scanNoChanges'
+                                    if w.extraLoop
+                                        extraLoop = true
+                        catch e
+                            displayError e, cd, w, 1
+
                 if alight.debug.scan > 1
                     console.log 'changed:', w.src
         queue.push.apply queue, cd.children
@@ -471,10 +495,6 @@
     result.total = total
     result.changes = changes
     result.extraLoop = extraLoop
-
-    result.scope = null
-    result.src = ''
-    result.element = null
     return

@@ -486,40 +506,33 @@
     if alight.debug.scan
         start = get_time()

-    try
-        result =
-            total: 0
-            changes: 0
-            extraLoop: false
-            src: ''
-            scope: null
-            element: null
-
-        while mainLoop
-            mainLoop--
-            root.extraLoop = false
-
-            # take onScanOnce
-            if root.watchers.onScanOnce.length
-                onScanOnce = root.watchers.onScanOnce.slice()
-                root.watchers.onScanOnce.length = 0
-                for callback in onScanOnce
-                    callback.call root
-
-            scanCore @, result
-            totalChanges += result.changes
-
-            if not result.extraLoop and not root.extraLoop
-                break
-        if alight.debug.scan
-            duration = get_time() - start
-            console.log "$scan: loops: (#{10-mainLoop}), last-loop changes: #{result.changes}, watches: #{result.total} / #{duration}ms"
-    catch e
-        alight.exceptionHandler e, '$scan, error in expression: ' + result.src,
-            src: result.src
-            scope: result.scope
-            element: result.element
-            result: result
+    result =
+        total: 0
+        changes: 0
+        extraLoop: false
+        src: ''
+        scope: null
+        element: null
+
+    while mainLoop
+        mainLoop--
+        root.extraLoop = false
+
+        # take onScanOnce
+        if root.watchers.onScanOnce.length
+            onScanOnce = root.watchers.onScanOnce.slice()
+            root.watchers.onScanOnce.length = 0
+            for callback in onScanOnce
+                callback.call root
+
+        scanCore @, result
+        totalChanges += result.changes
+
+        if not result.extraLoop and not root.extraLoop
+            break
+    if alight.debug.scan
+        duration = get_time() - start
+        console.log "$scan: loops: (#{10-mainLoop}), last-loop changes: #{result.changes}, watches: #{result.total} / #{duration}ms"
     result.mainLoop = mainLoop
     result.totalChanges = totalChanges
     result


don't wait until load to find brand, only need hostname (load not needed)

--- src/chrome/content.coffee
+++ src/chrome/content.coffee
@@ -1,7 +1,6 @@
 corporations = require '../shared/corporations'

-window.onload = ->
-  console.log corporations.brands().fromHostname location.hostname
+console.log corporations.brands().fromHostname location.hostname

 # chrome.runtime.onMessage.addListener (message, sender, sendResponse) ->
 #   try


client: stackwizard: back and next button styles updated for new design

--- client/stacks/lib/views/stacks/onboarding/onboardingview.coffee
+++ client/stacks/lib/views/stacks/onboarding/onboardingview.coffee
@@ -109,12 +109,12 @@
   createFooter: ->

     @backButton = new kd.ButtonView
-      cssClass  : 'solid outline medium back'
+      cssClass  : 'outline back'
       title     : 'Back'
       callback  : => @emit 'PageNavigationRequested', 'prev'

     @nextButton = new kd.ButtonView
-      cssClass  : 'solid green medium next'
+      cssClass  : 'outline next'
       title     : 'Next'
       disabled  : yes
       callback  : => @emit 'PageNavigationRequested', 'next'


Add project-manager and rails-open-rspec

--- atom/packages.cson
+++ atom/packages.cson
@@ -5,6 +5,8 @@
   "git-plus"
   "merge-conflicts"
   "package-sync"
+  "project-manager"
+  "rails-open-rspec"
   "ruby-test"
   "synced-sidebar"
   "toggle-quotes"


Latest list of must-have Atom packages

Latest list of must-have Atom packages

--- atom/packages.cson
+++ atom/packages.cson
@@ -4,13 +4,12 @@
   'block-travel'
   'css-color-highlighting'
   'editor-stats'
+  'gist-it'
   'language-haml'
   'language-rspec'
   'package-sync'
   'railscasts-syntax'
-  'status-stats'
+  'sort-lines'
   'symbol-gen'
-  'test-status'
   'toggle-quotes'
-  'travis-ci-status'
 ]


Revert "Add language-generic-config package"

This reverts commit 9644fa222b24ef4f100151ff74bd1f6b2ff5046b.

--- atom/packages.cson
+++ atom/packages.cson
@@ -16,7 +16,6 @@
   "language-babel"
   "language-diff"
   "language-env"
-  "language-generic-config"
   "language-gitattributes"
   "language-gitignore"
   "language-har"


add test#tape snippet (from Eric Elliott "5 Questions Every Unit Test Must Answer")

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -19,3 +19,17 @@
 # If you are unfamiliar with CSON, you can read more about it in the
 # Atom Flight Manual:
 # http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson
+
+'.source.js':
+  'test':
+    'prefix': 'test#tape'
+    'body': '''
+      test('${2:What component aspect are you testing?}', assert => {
+        const actual = ${3:'What is the actual output?'};
+        const expected = ${4:'What is the expected output?'};
+
+        assert.equal(actual, expected, '${1:What should the feature do?}');
+
+        assert.end();
+      });
+      '''


Add more Redux snippets

Add more Redux snippets

* `ract`: define ActionType constants
* `rred`: define a reducer
* `rrspec`: define a reducer spec
* `rsel`: define a selector

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -21,6 +21,16 @@
 # https://atom.io/docs/latest/using-atom-basic-customization#cson

 '.source.js':
+  'Redux ActionType':
+    'prefix': 'ract'
+    'body': """
+            import { createActionTypes } from 'utils'
+
+            export default createActionTypes('$1', [
+              '$2'
+            ])
+
+            """
   'Redux container':
     'prefix': 'rcont'
     'body': """
@@ -29,6 +39,45 @@
             import $1 from '../components/$1'

             export default connect()($1)
+
+            """
+  'Redux reducer':
+    'prefix': 'rred'
+    'body': """
+            // import ActionType from './constants/ActionType'
+            import { createReducer } from 'utils'
+
+            const initialState = {
+            }
+
+            export default createReducer(initialState, {
+            })
+
+            """
+  'Redux reducer spec':
+    'prefix': 'rrspec'
+    'body': """
+            import reducer from '../reducer'
+            import { nullAction } from 'testutils'
+
+            describe('errors reducer', () => {
+              const initialState = reducer(undefined, nullAction)
+
+              describe('$1', () => {
+                $2
+              })
+            })
+
+            """
+  'Redux selectors':
+    'prefix': 'rsel'
+    'body': """
+            import { path } from 'ramda'
+
+            const localState = path(['app', '$1'])
+
+            export default {
+            }

             """
   'Stylesheet (React Native)':


Add rename support to the publish command

usage: apm publish —rename <new-name>

--- src/publish.coffee
+++ src/publish.coffee
@@ -24,6 +24,7 @@

       Usage: apm publish [<newversion> | major | minor | patch | build]
              apm publish --tag <tagname>
+             apm publish --rename <new-name>

       Publish a new version of the package in the current working directory.

@@ -31,6 +32,9 @@
       created and the package.json file is updated with that new version before
       it is published to the apm registry. The HEAD branch and the new tag are
       pushed up to the remote repository automatically using this option.
+
+      If a new name is provided via the --rename flag, the package.json file is
+      updated with the new name and the package's name is updated on Atom.io.

       Run `apm featured` to see all the featured packages or
       `apm view <packagename>` to see information about your package after you
@@ -38,6 +42,7 @@
     """
     options.alias('h', 'help').describe('help', 'Print this usage message')
     options.alias('t', 'tag').string('tag').describe('tag', 'Specify a tag to publish')
+    options.alias('r', 'rename').string('rename').describe('rename', 'Specify a new name for the package')

   showHelp: (argv) -> @parseOptions(argv).showHelp()

@@ -166,7 +171,7 @@
   # tag - The string Git tag of the new version.
   # callback - The callback function to invoke with an error as the first
   #            argument.
-  createPackageVersion: (packageName, tag, callback) ->
+  createPackageVersion: (packageName, tag, options, callback) ->
     Login.getTokenOrLogin (error, token) ->
       if error?
         callback(error)
@@ -177,6 +182,7 @@
         json: true
         body:
           tag: tag
+          rename: options.rename
         headers:
           authorization: token
       request.post requestSettings, (error, response, body={}) ->
@@ -194,9 +200,12 @@
   # tag - The Git tag string of the package version to publish.
   # callback - The callback function to invoke when done with an error as the
   #            first argument.
-  publishPackage: (pack, tag, callback) ->
-    process.stdout.write "Publishing #{pack.name}@#{tag} "
-    @createPackageVersion pack.name, tag, (error) =>
+  publishPackage: (pack, tag, remaining...) ->
+    options = remaining.shift() if remaining.length >= 2
+    callback = remaining.shift()
+
+    process.stdout.write "Publishing #{options.rename || pack.name}@#{tag} "
+    @createPackageVersion pack.name, tag, options, (error) =>
       if error?
         @logFailure()
         callback(error)
@@ -222,6 +231,11 @@
     catch error
       throw new Error("Error parsing package.json file: #{error.message}")

+  saveMetadata: (pack) ->
+    metadataPath = path.resolve('package.json')
+    out = JSON.stringify(pack, null, 2)
+    fs.writeFileSync(metadataPath, out)
+
   loadRepository: ->
     currentDirectory = process.cwd()

@@ -239,12 +253,36 @@

     unless upstreamUrl
       throw new Error('Package must pushed up to GitHub before publishing: https://help.github.com/articles/create-a-repo')
+
+  # Rename package if necessary
+  renamePackage: (pack, name, callback) ->
+    if name?.length > 0
+      message = "Renaming #{pack.name} to #{name}"
+      process.stdout.write("#{message}\n")
+      @setPackageName pack, name, =>
+        @spawn 'git', ['add', 'package.json'], (addCode) =>
+          @spawn 'git', ['commit', '-m', "#{message}"], (code, stderr='', stdout='') =>
+            if code is 0 or addCode is 0
+              callback()
+            else
+              callback('Failed to commit package.json')
+    else
+      # Just fall through if the name is empty
+      callback()
+
+  setPackageName: (pack, name, callback) ->
+    if pack.name == name
+      throw new Error('New package name matches old package name')
+
+    pack.name = name
+    @saveMetadata(pack)
+    callback()

   # Run the publish command with the given options
   run: (options) ->
     {callback} = options
     options = @parseOptions(options.commandArgs)
-    {tag} = options.argv
+    {tag, rename} = options.argv
     [version] = options.argv._

     try
@@ -257,22 +295,35 @@
     catch error
       return callback(error)

-    if version?.length > 0
+    if version?.length > 0 or rename?.length > 0
+      unless version?.length > 0
+        version = 'patch'
+        originalName = pack.name
+
       @registerPackage pack, (error, firstTimePublishing) =>
+        console.log "registering #{pack.name}"
         return callback(error) if error?

-        @versionPackage version, (error, tag) =>
+        @renamePackage pack, rename, (error) =>
           return callback(error) if error?

-          @pushVersion tag, (error) =>
+          @versionPackage version, (error, tag) =>
             return callback(error) if error?

-            @waitForTagToBeAvailable pack, tag, =>
-
-              @publishPackage pack, tag, (error) =>
-                if firstTimePublishing and not error?
-                  @logFirstTimePublishMessage(pack)
-                callback(error)
+            @pushVersion tag, (error) =>
+              return callback(error) if error?
+
+              @waitForTagToBeAvailable pack, tag, =>
+
+                if originalName?
+                  # If we're renaming a package, we have to hit the API with the
+                  # current name, not the new one, or it will 404.
+                  rename = pack.name
+                  pack.name = originalName
+                @publishPackage pack, tag, {rename: rename},  (error) =>
+                  if firstTimePublishing and not error?
+                    @logFirstTimePublishMessage(pack)
+                  callback(error)
     else if tag?.length > 0
       @registerPackage pack, (error, firstTimePublishing) =>
         return callback(error) if error?
@@ -282,4 +333,4 @@
             @logFirstTimePublishMessage(pack)
           callback(error)
     else
-      callback('Missing required tag to publish')
+      callback('Version, tag or rename required to publish')


fix safari auto scrolling to textarea

fix safari auto scrolling to textarea

setting selectionStart causes ios to scroll to that element when an
input mode is active.

also, no point setting selection if element doesn't need focus.

--- resources/assets/coffee/react/_components/comment-editor.coffee
+++ resources/assets/coffee/react/_components/comment-editor.coffee
@@ -25,8 +25,9 @@

   componentDidMount: =>
-    @textarea.current?.selectionStart = -1
-    @textarea.current?.focus() if (@props.focus ? true)
+    if @props.focus ? true
+      @textarea.current?.selectionStart = -1
+      @textarea.current?.focus()

   componentWillUnmount: =>


Added mixpanel tracking to follow users buttons in tour

--- core/app/backbone/views/tour/tour_interesting_users_view.coffee
+++ core/app/backbone/views/tour/tour_interesting_users_view.coffee
@@ -68,10 +68,12 @@
   showPreviousPage: ->
     return unless @hasPreviousPage()
     @showPage @page-1
+    mp_track "Tour: Interesting users - Previous page"

   showNextPage: ->
     return unless @hasNextPage()
     @showPage @page+1
+    mp_track "Tour: Interesting users - Next page"

   hasPreviousPage: ->
     @page > 0


remove stray console.log

remove stray console.log

--- brains.coffee
+++ brains.coffee
@@ -25,7 +25,6 @@
             @line = 0

         nextLine: (stage, user, respond) ->
-            console.log @line, stage, @answers.length
             if @line isnt stage
                 respond '...'
                 return


Support getting attachment by index instead of name

--- src/components/WebService/CouchDB/ReadDocumentAttachment.coffee
+++ src/components/WebService/CouchDB/ReadDocumentAttachment.coffee
@@ -34,19 +34,32 @@

         return headers

-    getRequest: (callback) ->
+    getRequest: (attachment, callback) ->
         options =
             host: @connection.uri.hostname
             method: "GET"
-            path: "#{@connection.uri.pathname}/#{@document['_id']}/#{@attachment}"
+            path: "#{@connection.uri.pathname}/#{@document['_id']}/#{attachment}"
             port: @connection.uri.port
             headers: @getHeaders()

         req = @connection.uri.protocolHandler.request options, callback
         req.end()

+    getAttachmentNameByIndex: (document, index) ->
+        count = 0
+        index = parseInt index
+        for name, value of document['_attachments']
+            return name if count is index
+            count++
+        return index
+
+    getAttachmentName: (document, attachment) ->
+        return attachment if isNaN attachment - 0
+        return @getAttachmentNameByIndex document, attachment
+
     readAttachment: ->
-        @getRequest (response) =>
+        attachment = @getAttachmentName @document, @attachment
+        @getRequest attachment, (response) =>
             response.setEncoding "binary"
             body = ""
             port = @outPorts.out


inspired by a log error, make it so connect_to_locked_socket can't call its callback more than once

--- salvus/misc_node.coffee
+++ salvus/misc_node.coffee
@@ -186,7 +186,8 @@
     timed_out = () ->
         m = "misc_node: timed out trying to connect to locked socket on port #{port}"
         winston.debug(m)
-        cb(m)
+        cb?(m)
+        cb = undefined  # NOTE: everywhere below we set cb to undefined after calling it, and only call it if defined, since the event and timer callback stuff is very hard to do right here without calling cb more than once (which is VERY bad to do).
         socket.end()
         timer = undefined

@@ -199,12 +200,14 @@
             if data.toString() == 'y'
                 if timer?
                     clearTimeout(timer)
-                    cb(false, socket)
+                    cb?(false, socket)
+                    cb = undefined
             else
                 socket.destroy()
                 if timer?
                     clearTimeout(timer)
-                    cb("Permission denied (invalid secret token) when connecting to the local hub.")
+                    cb?("Permission denied (invalid secret token) when connecting to the local hub.")
+                    cb = undefined
         socket.on 'data', listener
         winston.debug("misc_node: connected, now sending secret token")
         socket.write(token)
@@ -213,7 +216,9 @@
     socket.on "error", (err) =>
         if timer?
             clearTimeout(timer)
-        cb(err)
+        cb?(err)
+        cb = undefined
+

 # Compute a uuid v4 from the Sha-1 hash of data.


allow using the same callback for different addresses.

--- src/service.coffee
+++ src/service.coffee
@@ -117,18 +117,22 @@
       # Register a callback handler for the specified address match.
       registerHandler : (address, callback) ->
         return unless typeof callback is 'function'
+        return unless angular.isString(address)
         console.debug("[VertX EB Service] Register handler for #{address}") if debugEnabled
-        return fnWrapperMap[callback] if fnWrapperMap[callback] # already known
-        fnWrapperMap[callback] = (message, replyTo) ->
+        addressSanitized = encodeURIComponent(address);
+        return fnWrapperMap[addressSanitized] if fnWrapperMap[addressSanitized] # already known
+        fnWrapperMap[addressSanitized] = (message, replyTo) ->
           callback(message, replyTo)
           $rootScope.$digest()
-        vertxEventBus.registerHandler address, fnWrapperMap[callback]
+        vertxEventBus.registerHandler address, fnWrapperMap[addressSanitized]
       # Remove a callback handler for the specified address match.
       unregisterHandler : (address, callback) ->
         return unless typeof callback is 'function'
+        return unless angular.isString(address)
         console.debug("[VertX EB Service] Unregister handler for #{address}") if debugEnabled
-        vertxEventBus.unregisterHandler address, fnWrapperMap[callback]
-        fnWrapperMap[callback] = undefined
+        addressSanitized = encodeURIComponent(address);
+        vertxEventBus.unregisterHandler address, fnWrapperMap[addressSanitized]
+        fnWrapperMap[addressSanitized] = undefined
         return #void
       # Send a message to the specified address (using EventBus.send).
       # @param address a required string for the targeting address in the bus


name file according to meaning of content, not format

--- js-library/Gruntfile.coffee
+++ js-library/Gruntfile.coffee
@@ -62,11 +62,11 @@
     sass:
       build:
         files:
-          'build/css/basic_without_base64.css': 'app/css/basic.scss'
+          'build/css/basic_without_embeds.css': 'app/css/basic.scss'
     cssUrlEmbed:
       encodeDirectly:
         files:
-          'build/css/basic.css': ['build/css/basic_without_base64.css']
+          'build/css/basic.css': ['build/css/basic_without_embeds.css']
     cssmin:
       build:
         options:


Let's temporarily rename 'attach' to "attach-conversation"

--- src/sm-cmds.coffee
+++ src/sm-cmds.coffee
@@ -94,7 +94,7 @@
         resp.reply "Updating Incident #{id} on #{ins}<#{Config.get "sm.servers."+ins+".endpoint"}>....."
       attach: (fullCmdLine, resp)->
         room = resp.message.room
-        match = /sm\s+attach\s+incident\s*([\w\d]+)\s*(?:on (.+))?/i.exec fullCmdLine
+        match = /sm\s+attach-conversation\s+incident\s*([\w\d]+)\s*(?:on (.+))?/i.exec fullCmdLine
         if not match
           sendHelp room, helpAttach
           return


dont need to un-fix pinned footer

dont need to un-fix pinned footer

--- test-integration/helpers/ui/user.coffee
+++ test-integration/helpers/ui/user.coffee
@@ -104,8 +104,7 @@
       hider = document.createElement('style')
       hider.textContent = '''
         .navbar-fixed-bottom, .navbar-fixed-top,
-        .pinned-on .pinned-header,
-        .pinned-footer {
+        .pinned-on .pinned-header {
           z-index: initial !important;
           position: initial !important;
         }


display response

display response

--- BackofficeBundle/Resources/public/coffee/FullPageFormView.coffee
+++ BackofficeBundle/Resources/public/coffee/FullPageFormView.coffee
@@ -42,6 +42,7 @@
       e.preventDefault()
       $(this).ajaxSubmit
         success: (response) ->
+          options.html = response
           view = new FullPageFormView(options)
       return


:green_heart: Don't destroy markers twice

--- src/display-marker.coffee
+++ src/display-marker.coffee
@@ -11,8 +11,10 @@
     @bufferMarker.onDidDestroy(@destroy.bind(this))

   destroy: ->
+    return if @destroyed
+
+    @destroyed = true
     @bufferMarker.destroy()
-    @destroyed = true
     @emitter.emit('did-destroy')

   compare: (otherMarker) ->


Add `layer.options` to replace `layer.states.animationOptions`

--- framer/Layer.coffee
+++ framer/Layer.coffee
@@ -132,6 +132,7 @@
 			if options.hasOwnProperty(p)
 				@[p] = options[p]

+		@options = {}
 		@_stateMachine = new LayerStateMachine(@)
 		@_context.emit("layer:create", @)

@@ -905,7 +906,7 @@
 			stateName = properties
 			return @animateToState stateName, options

-		_.defaults(options,properties.options)
+		_.defaults(options,properties.options,@options)
 		delete properties.options

 		animatableProperties = Animation.filterAnimatableProperties(properties)
@@ -1298,7 +1299,7 @@

 		# Don't show any hints while we are animating
 		if @isAnimating
-			return false
+			return false

 		for parent in @ancestors()
 			return false if parent.isAnimating


Renaming user property to 'custom'.

Renaming user property to 'custom'.

--- framer/Layer.coffee
+++ framer/Layer.coffee
@@ -88,7 +88,8 @@
 	##############################################################
 	# Properties

-	@define "user", @simpleProperty("user", undefined)
+	# A placeholder for layer bound properties defined by the user:
+	@define "custom", @simpleProperty("custom", undefined)

 	# Css properties
 	@define "width",  layerProperty(@, "width",  "width", 100, _.isNumber)


Use domEventManager to add and remove listeners

--- framer/Layer.coffee
+++ framer/Layer.coffee
@@ -640,6 +640,8 @@
 	## IMAGE

 	cleanupImageLoader: ->
+		@imageEventManager?.removeAllListeners()
+		@imageEventManager = null
 		@imageLoader = null

@@ -664,11 +666,9 @@

 			# Set the property value
 			@_setPropertyValue("image", value)
-
 			if value in [null, ""]
 				if @imageLoader?
-					@imageLoader.onload = null
-					@imageLoader.onerror = null
+					@imageEventManager.removeAllListeners()
 					@imageLoader.src = null

 				@style["background-image"] = null
@@ -699,13 +699,13 @@
 				@imageLoader = new Image()
 				@imageLoader.name = imageUrl
 				@imageLoader.src = imageUrl
-
-				@imageLoader.onload = =>
+				@imageEventManager = @_context.domEventManager.wrap(@imageLoader)
+				@imageEventManager.addEventListener "load", =>
 					@style["background-image"] = "url('#{imageUrl}')"
 					@emit Events.ImageLoaded, @imageLoader
 					@cleanupImageLoader()

-				@imageLoader.onerror = =>
+				@imageEventManager.addEventListener "error", =>
 					@emit Events.ImageLoadError, @imageLoader
 					@cleanupImageLoader()

@@ -1214,7 +1214,7 @@
 			return false

 		if @_draggable
-
+
 			if @_draggable.isDragging is false and @_draggable.isMoving is false
 				return false


Decreased fontFamily check timeout

Decreased fontFamily check timeout

--- framer/Utils.coffee
+++ framer/Utils.coffee
@@ -495,7 +495,7 @@
 # Load fonts from Google Web Fonts
 _isFontLoadedResults = {}

-Utils.isFontFamilyLoaded = (fonts, timeout = 10000) ->
+Utils.isFontFamilyLoaded = (fonts, timeout = 1000) ->
 	if not _.isArray(fonts)
 		fonts = [fonts]
 	return Utils.loadWebFontConfig


removing Prem from team

removing Prem from team

--- scripts/offshore.coffee
+++ scripts/offshore.coffee
@@ -78,19 +78,19 @@
     msg.send "Praise to the Lord! Hail to you o mighty Prem."

   robot.respond /who will be victorious/i, (msg)->
-    msg.send "The Offshore team! :ahsan: :sachin: :prem: :kostas: :chris: :richard: :drmatt:"
+    msg.send "The Offshore team! :ahsan: :sachin: :kostas: :chris: :richard: :drmatt:"

   robot.respond /who will be triumphant/i, (msg)->
-    msg.send "The Offshore team! :ahsan: :sachin: :prem: :kostas: :chris: :richard: :drmatt:"
+    msg.send "The Offshore team! :ahsan: :sachin: :kostas: :chris: :richard: :drmatt:"

   robot.respond /who will prevail/i, (msg)->
-    msg.send "The Offshore team! :ahsan: :sachin: :prem: :kostas: :chris: :richard: :drmatt:"
+    msg.send "The Offshore team! :ahsan: :sachin: :kostas: :chris: :richard: :drmatt:"

   robot.respond /who will unleash righteous vengeance upon evil/i, (msg)->
-    msg.send "The heroes of offshore team! :ahsan: :sachin: :prem: :kostas: :chris: :richard: :drmatt:"
+    msg.send "The heroes of offshore team! :ahsan: :sachin: :kostas: :chris: :richard: :drmatt:"

   robot.respond /when all hope is lost and darkness is upon us/i, (msg)->
-    msg.send "The offshore team shall know no fear! :ahsan: :sachin: :prem: :kostas: :chris: :richard: :drmatt:"
+    msg.send "The offshore team shall know no fear! :ahsan: :sachin: :kostas: :chris: :richard: :drmatt:"

   robot.respond /when Prem is gone and we are lost in darkness/i, (msg)->
     msg.send "The offshore team shall know no fear! The offshore team will be triumphant! :ahsan: :sachin: :prem: :kostas: :chris: :drmatt:"


Get rid of the "clear cookies" part of the error message for sign in; this will no longer help much

--- salvus/page/account.coffee
+++ salvus/page/account.coffee
@@ -239,7 +239,7 @@
         cb            : (error, mesg) ->
             $("#sign_in-button").icon_spin()
             if error
-                alert_message(type:"error", message: "There was an error signing you in (#{error}).  Possibly clear your cloud.sagemath.com cookies, refresh your browser and try again; if that doesn't work, email help@sagemath.com.")
+                alert_message(type:"error", message: "There was an error signing you in (#{error}).  Please try again; if that doesn't work after a few minutes, email help@sagemath.com.")
                 return
             switch mesg.event
                 when 'sign_in_failed'


Fix conflicts

Fix conflicts

--- salvus/page/project.coffee
+++ salvus/page/project.coffee
@@ -2003,6 +2003,7 @@
             text = $(button).text()
             if localStorage["project-activity-button-#{text}"]
                 $(button).removeClass('btn-warning').addClass('btn-default')
+
         @container.find(".salvus-project-activity-search").keyup (e) =>
             @_project_activity_log_page = 0
             @render_project_activity_log()


disable crash check

disable crash check

--- index.cjsx
+++ index.cjsx
@@ -25,7 +25,7 @@

 if config.get('plugin.Akashic.enable', true)
   initialAkashicRecordsWindow()
-  setInterval checkAkashicRecordsCrashed, 2000
+  # setInterval checkAkashicRecordsCrashed, 2000

 module.exports =
   name: 'Akashic'
@@ -36,5 +36,5 @@
   link: 'https://github.com/JenningsWu'
   version: '1.0.0'
   handleClick: ->
-    checkAkashicRecordsCrashed()
+    # checkAkashicRecordsCrashed()
     window.akashicRecordsWindow.show()


add whitelist, move code into functions

add whitelist, move code into functions

--- src/uapanfw.coffee
+++ src/uapanfw.coffee
@@ -8,7 +8,7 @@
 #   xxxxxxxxxxx [required] - API KEY
 #
 # Commands:
-#   hubot fw - fw commands
+#   hubot firewall - firewall commands
 #
 # Notes:
 #   requires hubot-auth be present
@@ -23,12 +23,14 @@
 modulename = 'fw'
 blacklistfile = modulename + ".json"
 displayfmt = '%-4s %-50s %-15s %s'
+timefmt = 'YYYY-MM-DD HH:mm:ss ZZ'
 svcQueueIntervalMs = 300 * 1000

 robotRef = false
 fwdata =
   notify: []
   blacklist: []
+  whitelist: []

 isAuthorized = (msg) ->
   console.error 'bad robotRef' unless robotRef
@@ -44,13 +46,20 @@
   msg.reply "2fa required.  Use `auth 2fa` to validate identity."
   return false

+
 expirationWorker = ->
-  bl = fwdata.blacklist
+  expireEntriesFromList 'blacklist'
+  expireEntriesFromList 'whitelist'
+  setTimeout expirationWorker, svcQueueIntervalMs
+
+
+expireEntriesFromList = (list_name) ->
+  list = fwdata[list_name]
   removequeue = []
   deleted = [
     sprintf displayfmt, 'Type', 'Value', 'Expiration', 'Creator'
   ]
-  for obj in fwdata.blacklist when moment(obj.expires).valueOf() < Date.now()
+  for obj in fwdata[list_name] when moment(obj.expires).valueOf() < Date.now()
     deleted.push sprintf displayfmt, obj.type, obj.val,
       moment(obj.expires).fromNow(), obj.creator
     removequeue.push obj
@@ -58,20 +67,244 @@
   if removequeue.length > 0
     while removequeue.length > 0
       obj = removequeue.shift()
-      bl.splice(bl.indexOf(obj), 1)
-    msg = "fw: blacklist entries expired and have been removed. " +
+      list.splice(list.indexOf(obj), 1)
+    msg = "fw: #{list_name} entries expired and have been removed. " +
       "Change will be applied in < 5 minutes. Removed: " +
       "```"+ deleted.join("\n") + "```"
     notifySubscribers msg
     fs.writeFileSync blacklistfile, JSON.stringify(fwdata), 'utf-8'

-  setTimeout expirationWorker, svcQueueIntervalMs
-
 notifySubscribers = (msg, current_un = false) ->
   console.error 'bad robotRef' unless robotRef
   for un in fwdata.notify
     robotRef.send { room: un }, msg unless current_un && un == current_un

+addListEntry = (robot, msg) ->
+  who = msg.envelope.user.name
+
+  list_name = String(msg.match[1])
+  list_name = 'whitelist' if list_name.indexOf('w') == 0
+  list_name = 'blacklist' if list_name.indexOf('b') == 0
+
+  entry =
+    creator: who
+    created: moment().format()
+    expires: moment().add(1, 'months').format()
+    type: String(msg.match[2])
+    val: String(msg.match[3])
+
+  if entry.val.toLowerCase().indexOf('https://') == 0
+    usermsg = "#{list_name}ing of https links not supported."
+    return msg.reply usermsg
+
+  if entry.val.toLowerCase().indexOf('http') == 0
+    entry.val = entry.val.replace(/http:\/\//i,'')
+
+  if msg.match[4]?
+    expires = String(msg.match[4])
+    extra = expires.match /\+(\d)([hdwMQy])/
+    if extra?
+      n = extra[1]
+      unit = extra[2]
+      entry.expires = moment().add(n,unit).format()
+    else if moment(expires).isValid()
+      entry.expires = moment(expires).format()
+    else
+      usermsg = "invalid expiration date: #{expires}"
+      return msg.reply usermsg
+
+  logmsg = "#{modulename}: #{who} requested: " +
+    "#{list_name} #{entry.type} #{entry.val} expires #{moment(entry.expires).format(timefmt)}"
+  robot.logger.info logmsg
+
+  fwdata[list_name].push entry
+  fs.writeFileSync blacklistfile, JSON.stringify(fwdata), 'utf-8'
+
+  usermsg = "#{who} added `#{entry.val}` to firewall #{list_name}. " +
+    "Expires `#{entry.expires}`.  Change will be applied in < 5 minutes."
+  msg.reply usermsg
+
+  logmsg = "#{modulename}: robot responded to #{who}: " +
+    "added entry to #{list_name}"
+  robot.logger.info logmsg
+
+  #usermsg = usermsg.replace(/  Change will be applied in \< 5 minutes\./, '')
+  notifySubscribers usermsg, who
+
+
+deleteListEntry = (robot, msg) ->
+  who = msg.envelope.user.name
+
+  list_name = String(msg.match[1])
+  list_name = 'whitelist' if list_name.indexOf('w') == 0
+  list_name = 'blacklist' if list_name.indexOf('b') == 0
+
+  l_type = String(msg.match[2])
+  l_search = String(msg.match[3])
+
+  if l_search.toLowerCase().indexOf('https://') == 0
+    usermsg = "#{list_name}ing of https links not supported."
+    return msg.reply usermsg
+
+  if l_search.toLowerCase().indexOf('http') == 0
+    l_search = l_search.replace(/http:\/\//i,'')
+
+  logmsg = "#{modulename}: #{who} requested: " +
+    "#{list_name} delete #{l_type} #{l_search}"
+  robot.logger.info logmsg
+
+  deleted = []
+  new_entry = []
+  deleted.push sprintf displayfmt, 'Type', 'Value', 'Expiration', 'Creator'
+  for obj in fwdata[list_name]
+    expires = moment(obj.expires)
+    if l_type and l_type != obj.type
+      errfmt = 'type: [%s] != [%s]'
+      #console.log sprintf errfmt, l_type, obj.type
+      new_entry.push obj
+      continue
+    if l_search and obj.val.indexOf(l_search) == -1
+      errfmt = 'search: indexOf[%s] not found in [%s]'
+      #console.log sprintf errfmt, l_search, obj.val
+      new_entry.push obj
+      continue
+    if expires.isBefore() # now
+      #console.log 'expires: ['+ expires +'] is before now'
+      new_entry.push obj
+      continue
+    deleted.push sprintf displayfmt, obj.type, obj.val,
+      expires.fromNow(), obj.creator
+
+  deltaN = fwdata[list_name].length - new_entry.length
+  if deltaN > 0
+    usermsg = "#{who} removed `#{deltaN}` fw #{list_name} entries.  " +
+      "Change will be applied in < 5 minutes. Removed: " +
+      "```"+ deleted.join("\n") + "```"
+    fwdata[list_name] = new_entry
+    fs.writeFileSync blacklistfile, JSON.stringify(fwdata), 'utf-8'
+  else
+    usermsg = "#{list_name} delete request did not match any records."
+  msg.reply usermsg
+
+  logmsg = "#{modulename}: robot responded to #{msg.envelope.user.name}: " +
+    "removed #{deltaN} entries from #{list_name}"
+  robot.logger.info logmsg
+
+  if deltaN > 0
+    #usermsg = usermsg.replace(/  Change will be applied in \< 5 minutes\./, '')
+    notifySubscribers usermsg, who
+
+
+showList = (robot, msg) ->
+  list_name = String(msg.match[1])
+  list_name = 'whitelist' if list_name.indexOf('w') == 0
+  list_name = 'blacklist' if list_name.indexOf('b') == 0
+
+  l_type = false
+  l_type = msg.match[2] if msg.match[2]
+
+  l_search = false
+  l_search = msg.match[3] if msg.match[3]
+
+  logmsg = "#{modulename}: #{msg.envelope.user.name} requested: show #{list_name}"
+  robot.logger.info logmsg
+
+  arr = []
+  arr.push sprintf displayfmt, 'Type', 'Value', 'Expiration', 'Creator'
+  for obj in fwdata[list_name]
+    expires = moment(obj.expires)
+    if l_type and l_type != obj.type
+      #console.log 'skipping type: '+ obj.type
+      continue
+    if l_search and obj.val.indexOf(l_search) == -1
+      #console.log 'skipping search: '+ obj.val
+      continue
+    if expires.isBefore() # now
+      #console.log 'skipping expires: '+ obj.expires
+      continue
+    #console.log 'adding to array: '
+    #console.log obj
+    arr.push sprintf displayfmt, obj.type, obj.val, expires.fromNow(), obj.creator
+
+  msg.reply "#{list_name} items and expirations\n```\n"+ arr.join("\n") + "\n```"
+
+  logmsg = "#{modulename}: robot responded to #{msg.envelope.user.name}: " +
+    "displayed #{list_name} items and expirations"
+  robot.logger.info logmsg
+
+
+subscribe = (robot, msg) ->
+  user = msg.envelope.user
+  who = user.name
+  who = msg.match[1] if msg.match[1]
+
+  logmsg = "#{modulename}: #{user.name} requested: notify #{who}"
+  robot.logger.info logmsg
+
+  fwdata.notify.push who unless who in fwdata.notify
+
+  usermsg = "Added `#{who}` to firewall change notifications."
+  msg.reply usermsg
+
+  logmsg = "#{modulename}: robot responded to #{user.name}: " +
+    "added #{who} to firewall change notifications"
+  robot.logger.info logmsg
+
+  fs.writeFileSync blacklistfile, JSON.stringify(fwdata), 'utf-8'
+
+
+unsubscribe = (robot, msg) ->
+  user = msg.envelope.user
+  who = msg.envelope.user.name
+  who = msg.match[1] if msg.match[1]
+
+  logmsg = "#{modulename}: #{user.name} requested: unnotify #{who}"
+  robot.logger.info logmsg
+
+  n = fwdata.notify.indexOf(who)
+  fwdata.notify.splice(n, 1) if n > -1
+
+  usermsg = "Removed `#{who}` from firewall change notifications."
+  msg.reply usermsg
+
+  logmsg = "#{modulename}: robot responded to #{user.name}: " +
+    "removed #{who} from firewall change notifications"
+  robot.logger.info logmsg
+
+  fs.writeFileSync blacklistfile, JSON.stringify(fwdata), 'utf-8'
+
+
+showSubscribers = (robot, msg) ->
+  who = msg.envelope.user.name
+
+  logmsg = "#{modulename}: #{who} requested: subscribers"
+  robot.logger.info logmsg
+
+  usermsg = "Subscribers: `"+ fwdata.notify.join('`, `') + "`"
+  msg.reply usermsg
+
+  logmsg = "#{modulename}: robot responded to #{who}: " +
+    fwdata.notify.join(', ')
+  robot.logger.info logmsg
+
+httpGetList = (robot, req, res, list_name, l_type) ->
+  clientip = req.connection.remoteAddress || req.socket.remoteAddress ||
+    req.connection.socket.remoteAddress
+  logmsg = "#{modulename}: web request from #{clientip}: get #{l_type} #{list_name}"
+  robot.logger.info logmsg
+
+  arr = []
+  for obj in fwdata[list_name]
+    if obj.type == l_type and moment(obj.expires).isAfter()
+      arr.push obj.val
+  content = '# nothing here yet! #'
+  content = arr.join "\n" if arr.length
+  res.setHeader 'content-type', 'text/plain'
+  res.end content
+
+  logmsg = "#{modulename}: robot responded to web request: sent #{l_type} #{list_name}"
+  robot.logger.info logmsg
+

 module.exports = (robot) ->

@@ -84,54 +317,26 @@
     console.log('Unable to read file', error) unless error.code is 'ENOENT'

   robot.router.get "/#{robot.name}/#{modulename}/blacklist/url", (req, res) ->
-    clientip = req.connection.remoteAddress || req.socket.remoteAddress ||
-      req.connection.socket.remoteAddress
-    logmsg = "#{modulename}: web request from #{clientip}: get url blacklist"
-    robot.logger.info logmsg
-
-    arr = []
-    for obj in fwdata.blacklist
-      if obj.type == 'url' and moment(obj.expires).isAfter()
-        arr.push obj.val
-    content = '# nothing here yet! #'
-    content = arr.join "\n" if arr.length
-    res.setHeader 'content-type', 'text/plain'
-    res.end content
-
-    logmsg = "#{modulename}: robot responded to web request: sent url blacklist"
-    robot.logger.info logmsg
-
-    return
+    return httpGetList robot, req, res, 'blacklist', 'url'

   robot.router.get "/#{robot.name}/#{modulename}/blacklist/cidr", (req, res) ->
-    clientip = req.connection.remoteAddress || req.socket.remoteAddress ||
-      req.connection.socket.remoteAddress
-    logmsg = "#{modulename}: web request from #{clientip}: get cidr blacklist"
-    robot.logger.info logmsg
-
-    arr = []
-    for obj in fwdata.blacklist
-      if obj.type == 'cidr' and moment(obj.expires).isAfter()
-        arr.push obj.val
-    content = '# nothing here yet! #'
-    content = arr.join "\n" if arr.length
-    res.setHeader 'content-type', 'text/plain'
-    res.end content
-
-    logmsg = "#{modulename}: robot responded to web request: " +
-      "sent cidr blacklist"
-    robot.logger.info logmsg
-
-    return
-
-  robot.respond /fw(?: help| h|)$/, (msg) ->
+    return httpGetList robot, req, res, 'blacklist', 'cidr'
+
+  robot.router.get "/#{robot.name}/#{modulename}/whitelist/url", (req, res) ->
+    return httpGetList robot, req, res, 'whitelist', 'url'
+
+  robot.router.get "/#{robot.name}/#{modulename}/whitelist/cidr", (req, res) ->
+    return httpGetList robot, req, res, 'whitelist', 'cidr'
+
+  robot.respond /(?:firewall|fw)(?: help| h|)$/, (msg) ->
     cmds = ['```']
     arr = [
-      modulename + " blacklist [(cidr|url) [searchterm]] - show blacklist"
-      modulename + " blacklist add (url|cidr) <weburl.tld/etc|x.x.x.x> - add to blacklist"
-      modulename + " blacklist del (url|cidr) <weburl.tld/etc|x.x.x.x> - del from blacklist"
-      modulename + " blacklist notify [username] - notify when changes happen"
-      modulename + " blacklist subscribers - list notify subscribers"
+      modulename + " show (white|black)list [(cidr|url) [searchterm]]"
+      modulename + " add (white|black)list (url|cidr) <weburl.tld/etc|x.x.x.x>"
+      modulename + " del (white|black)list (url|cidr) <weburl.tld/etc|x.x.x.x>"
+      modulename + " subscribe [username] - subscribe to change notifications"
+      modulename + " unsubscribe [username]"
+      modulename + " show subscribers"
     ]

     for str in arr
@@ -148,211 +353,27 @@

     return

-  robot.respond /fw (?:blacklist|b)(?: (cidr|url)(?: ([^ ]+)|)|)$/i, (msg) ->
-    logmsg = "#{modulename}: #{msg.envelope.user.name} requested: show blacklist"
-    robot.logger.info logmsg
-
-    bl_type = false
-    bl_type = msg.match[1] if msg.match[1]
-
-    bl_search = false
-    bl_search = msg.match[2] if msg.match[2]
-
-    arr = []
-    arr.push sprintf displayfmt, 'Type', 'Value', 'Expiration', 'Creator'
-    for obj in fwdata.blacklist
-      expires = moment(obj.expires)
-      if bl_type and bl_type != obj.type
-        #console.log 'skipping type: '+ obj.type
-        continue
-      if bl_search and obj.val.indexOf(bl_search) == -1
-        #console.log 'skipping search: '+ obj.val
-        continue
-      if expires.isBefore() # now
-        #console.log 'skipping expires: '+ obj.expires
-        continue
-      #console.log 'adding to array: '
-      #console.log obj
-      arr.push sprintf displayfmt, obj.type, obj.val, expires.fromNow(), obj.creator
-
-    msg.reply "Blacklist items and expirations\n```\n"+ arr.join("\n") + "\n```"
-
-    logmsg = "#{modulename}: robot responded to #{msg.envelope.user.name}: " +
-      "displayed blacklist items and expirations"
-    robot.logger.info logmsg
-
-    return
-
-  robot.respond /fw (?:blacklist|b) add (url|cidr) ([^ ]+)(?: ([^ ]+)|)$/i, (msg) ->
+  robot.respond /(?:firewall|fw) show (whitelist|wl|w|blacklist|bl|b)(?: (cidr|url)(?: ([^ ]+)|)|)$/i, (msg) ->
+    return showList robot, msg
+
+  robot.respond /(?:firewall|fw) add (whitelist|wl|w|blacklist|bl|b) (url|cidr) ([^ ]+)(?: ([^ ]+)|)$/i, (msg) ->
     return unless isAuthorized msg
     return unless is2fa msg

-    who = msg.envelope.user.name
-
-    bl =
-      created: moment().format()
-      expires: moment().add(1, 'months').format()
-      type: String(msg.match[1])
-      val: String(msg.match[2])
-      creator: who
-
-    if bl.val.toLowerCase().indexOf('https://') == 0
-      usermsg = "Blacklisting of https links not supported."
-      return msg.reply usermsg
-
-    if bl.val.toLowerCase().indexOf('http') == 0
-      bl.val = bl.val.replace(/http:\/\//i,'')
-
-    if msg.match[3]?
-      expires = msg.match[3]
-      extra = expires.match /\+(\d)([hdwMQy])/
-      if extra?
-        n = extra[1]
-        unit = extra[2]
-        bl.expires = moment().add(n,unit).format()
-      else if moment(expires).isValid()
-        bl.expires = moment(expires).format()
-      else
-        usermsg = "invalid expiration date: #{expires}"
-        return msg.reply usermsg
-
-    logmsg = "#{modulename}: #{who} requested: " +
-      "blacklist #{bl.type} #{bl.val} expires #{bl.expires}"
-    robot.logger.info logmsg
-
-    fwdata.blacklist.push bl
-    fs.writeFileSync blacklistfile, JSON.stringify(fwdata), 'utf-8'
-
-    usermsg = "#{who} added `#{bl.val}` to firewall blacklist. " +
-      "Expires `#{bl.expires}`.  Change will be applied in < 5 minutes."
-    msg.reply usermsg
-
-    logmsg = "#{modulename}: robot responded to #{who}: " +
-      "added entry to blacklist"
-    robot.logger.info logmsg
-
-    #usermsg = usermsg.replace(/  Change will be applied in \< 5 minutes\./, '')
-    notifySubscribers usermsg, who
-
-    return
-
-  robot.respond /fw (?:blacklist|b) (?:delete|del|d) (url|cidr) ([^ ]+)$/i, (msg) ->
+    return addListEntry robot, msg
+
+  robot.respond /(?:firewall|fw) (?:delete|del|d) (whitelist|wl|w|blacklist|bl|b) (url|cidr) ([^ ]+)$/i, (msg) ->
     return unless isAuthorized msg
     return unless is2fa msg

-    who = msg.envelope.user.name
-    bl_type = String(msg.match[1])
-    bl_search = String(msg.match[2])
-
-    if bl_search.toLowerCase().indexOf('https://') == 0
-      usermsg = "Blacklisting of https links not supported."
-      return msg.reply usermsg
-
-    if bl_search.toLowerCase().indexOf('http') == 0
-      bl_search = bl_search.replace(/http:\/\//i,'')
-
-    logmsg = "#{modulename}: #{who} requested: " +
-      "blacklist delete #{bl_type} #{bl_search}"
-    robot.logger.info logmsg
-
-    deleted = []
-    new_bl = []
-    deleted.push sprintf displayfmt, 'Type', 'Value', 'Expiration', 'Creator'
-    for obj in fwdata.blacklist
-      expires = moment(obj.expires)
-      if bl_type and bl_type != obj.type
-        errfmt = 'type: [%s] != [%s]'
-        #console.log sprintf errfmt, bl_type, obj.type
-        new_bl.push obj
-        continue
-      if bl_search and obj.val.indexOf(bl_search) == -1
-        errfmt = 'search: indexOf[%s] not found in [%s]'
-        #console.log sprintf errfmt, bl_search, obj.val
-        new_bl.push obj
-        continue
-      if expires.isBefore() # now
-        #console.log 'expires: ['+ expires +'] is before now'
-        new_bl.push obj
-        continue
-      deleted.push sprintf displayfmt, obj.type, obj.val,
-        expires.fromNow(), obj.creator
-
-    deltaN = fwdata.blacklist.length - new_bl.length
-    if deltaN > 0
-      usermsg = "#{who} removed `#{deltaN}` fw blacklist entries.  " +
-        "Change will be applied in < 5 minutes. Removed: " +
-        "```"+ deleted.join("\n") + "```"
-      fwdata.blacklist = new_bl
-      fs.writeFileSync blacklistfile, JSON.stringify(fwdata), 'utf-8'
-    else
-      usermsg = "Blacklist delete request did not match any records."
-    msg.reply usermsg
-
-    logmsg = "#{modulename}: robot responded to #{msg.envelope.user.name}: " +
-      "removed #{deltaN} entries from blacklist"
-    robot.logger.info logmsg
-
-    if deltaN > 0
-      #usermsg = usermsg.replace(/  Change will be applied in \< 5 minutes\./, '')
-      notifySubscribers usermsg, who
-
-    return
-
-  robot.respond /fw (?:blacklist|b) (?:notify|n)(?: ([^ ]+))$/i, (msg) ->
-    user = msg.envelope.user
-    who = user.name
-    who = msg.match[1] if msg.match[1]
-
-    logmsg = "#{modulename}: #{user.name} requested: notify #{who}"
-    robot.logger.info logmsg
-
-    fwdata.notify.push who unless who in fwdata.notify
-
-    usermsg = "Added `#{who}` to firewall blacklist notifications."
-    msg.reply usermsg
-
-    logmsg = "#{modulename}: robot responded to #{user.name}: " +
-      "added #{who} to blacklist notifications"
-    robot.logger.info logmsg
-
-    fs.writeFileSync blacklistfile, JSON.stringify(fwdata), 'utf-8'
-
-    return
-
-  robot.respond /fw (?:blacklist|b) (?:subscribers|s)$/i, (msg) ->
-    who = msg.envelope.user.name
-
-    logmsg = "#{modulename}: #{who} requested: subscribers"
-    robot.logger.info logmsg
-
-    usermsg = "Subscriber list: `"+ fwdata.notify.join('`, `') + "`"
-    msg.reply usermsg
-
-    logmsg = "#{modulename}: robot responded to #{who}: " +
-      fwdata.notify.join(', ')
-    robot.logger.info logmsg
-
-    return
-
-  robot.respond /fw (?:blacklist|b) (?:unnotify|un)(?: ([^ ]+))$/i, (msg) ->
-    user = msg.envelope.user
-    who = msg.envelope.user.name
-    who = msg.match[1] if msg.match[1]
-
-    logmsg = "#{modulename}: #{user.name} requested: unnotify #{who}"
-    robot.logger.info logmsg
-
-    n = fwdata.notify.indexOf(who)
-    fwdata.notify.splice(n, 1) if n > -1
-
-    usermsg = "Removed `#{who}` from firewall blacklist notifications."
-    msg.reply usermsg
-
-    logmsg = "#{modulename}: robot responded to #{user.name}: " +
-      "removed #{who} from blacklist notifications"
-    robot.logger.info logmsg
-
-    fs.writeFileSync blacklistfile, JSON.stringify(fwdata), 'utf-8'
-
-    return
-
+    return deleteListEntry robot, msg
+
+  robot.respond /(?:firewall|fw) (?:subscribe|sub)(?: ([^ ]+))$/i, (msg) ->
+    return subscribe robot, msg
+
+  robot.respond /(?:firewall|fw) show (?:subscribers|s)$/i, (msg) ->
+    return showSubscribers robot, msg
+
+  robot.respond /(?:firewall|fw) (?:unsubscribe|unsub)(?: ([^ ]+))$/i, (msg) ->
+    return unsubscribe robot, msg
+


closes #993 and #875

closes #993 and #875

--- src/ui/util.coffee
+++ src/ui/util.coffee
@@ -144,6 +144,7 @@
 isImg = (url) -> url?.match /\.(png|jpe?g|gif|svg)$/i

 getImageUrl = (url="") ->
+    return false if !url? | url == ""
     return url if isImg url
     parsed = URL.parse url, true
     url = parsed.query.q


A small but nice simplification

A small but nice simplification

--- src/wordnet.coffee
+++ src/wordnet.coffee
@@ -96,8 +96,7 @@
     query = undefined
     if @cache
       query = "get:#{synsetOffset}:#{pos}"
-      hit = wordnet.cache.get query
-      return callback(hit) if hit?
+      return callback(hit) if hit = wordnet.cache.get query

     dataFile = wordnet.getDataFile(pos)
     dataFile.get synsetOffset, (result) ->


Create queues in ctor

Create queues in ctor

--- lib/ghc-modi-process.coffee
+++ lib/ghc-modi-process.coffee
@@ -12,17 +12,12 @@
 module.exports =
 class GhcModiProcess
   backend: null
-  commandQueues:
-    checklint: new Queue(1)
-    browse: new Queue(4)
-    typeinfo: new Queue(1)
-    find: new Queue(1)
-    init: new Queue(4)
-    list: new Queue(1)

   constructor: ->
     @disposables = new CompositeDisposable
     @disposables.add @emitter = new Emitter
+
+    @createQueues()

     opts = Util.getProcessOptions()
     opts.timeout = atom.config.get('haskell-ghc-mod.syncTimeout')
@@ -39,6 +34,15 @@
       @backend = new GhcModiProcessTemp
     else
       @backend = new GhcModiProcessRedirect
+
+  createQueues: =>
+    @commandQueues =
+      checklint: new Queue(1)
+      browse: new Queue(4)
+      typeinfo: new Queue(1)
+      find: new Queue(1)
+      init: new Queue(4)
+      list: new Queue(1)

   killProcess: =>
     @backend.killProcess()


Common code, DRY

Common code, DRY

--- lib/ide-haskell-repl.coffee
+++ lib/ide-haskell-repl.coffee
@@ -76,6 +76,11 @@
       'ide-haskell-repl:ghci-reload': commandFunction 'ghciReload'
       'ide-haskell-repl:ghci-interrupt': commandFunction 'interrupt'

+    externalCommandFunction = (func) => ({currentTarget}) =>
+      @open(currentTarget.getModel(), false)
+      .then (model) ->
+        model[func]()
+
     @disposables.add atom.commands.add 'atom-text-editor:not(.ide-haskell-repl)',
       'ide-haskell-repl:copy-selection-to-repl-input':  ({currentTarget}) =>
         ed = currentTarget.getModel()
@@ -89,18 +94,9 @@
         @open(ed, false)
         .then (model) ->
           model.runCommand(cmd)
-      'ide-haskell-repl:ghci-reload': ({currentTarget}) =>
-        @open(currentTarget.getModel(), false)
-        .then (model) ->
-          model.ghciReload()
-      'ide-haskell-repl:reload-repeat':  ({currentTarget}) =>
-        @open(currentTarget.getModel(), false)
-        .then (model) ->
-          model.ghciReloadRepeat()
-      'ide-haskell-repl:toggle-auto-reload-repeat':  ({currentTarget}) =>
-        @open(currentTarget.getModel(), false)
-        .then (model) ->
-          model.toggleAutoReloadRepeat()
+      'ide-haskell-repl:ghci-reload': externalCommandFunction 'ghciReload'
+      'ide-haskell-repl:reload-repeat': externalCommandFunction 'ghciReloadRepeat'
+      'ide-haskell-repl:toggle-auto-reload-repeat': externalCommandFunction 'toggleAutoReloadRepeat'

     @disposables.add atom.menu.add [
       'label': 'Haskell IDE'


cache posts locally

cache posts locally

--- lib/insert-link-view.coffee
+++ lib/insert-link-view.coffee
@@ -4,10 +4,11 @@
 path = require "path"
 fs = require "fs-plus"

+posts = null # to cache it
+
 module.exports =
 class AddLinkView extends View
   editor: null
-  posts: null
   links: null
   referenceId: false
   previouslyFocusedElement: null
@@ -38,7 +39,7 @@
     @on "core:cancel", => @detach()

   handleEvents: ->
-    @searchEditor.hiddenInput.on "keyup", => @updateSearch()
+    @searchEditor.hiddenInput.on "keyup", => @updateSearch() if posts
     @searchResult.on "click", "li", (e) => @useSearchResult(e)

   onConfirm: ->
@@ -87,9 +88,8 @@
       @setLink(selection, "", "")

   updateSearch: ->
-    return unless @posts
     query = @searchEditor.getText().trim().toLowerCase()
-    results = @posts.filter (post) ->
+    results = posts.filter (post) ->
       query and post.title.toLowerCase().contains(query)
     results = results.map (post) ->
       "
#{post.title}
"
@@ -193,9 +193,13 @@
     atom.project.resolve(atom.config.get("markdown-writer.siteLinkPath"))

   fetchPosts: ->
-    uri = atom.config.get("markdown-writer.urlForPosts")
-    succeed = (body) =>
-      @searchBox.show()
-      @posts = body.posts
-    error = (err) => @searchBox.hide()
-    utils.getJSON(uri, succeed, error)
+    if posts
+      @searchBox.show() if posts.length > 0
+    else
+      uri = atom.config.get("markdown-writer.urlForPosts")
+      succeed = (body) =>
+        posts = body.posts
+        @searchBox.show() if posts.length > 0
+      error = (err) =>
+        @searchBox.hide()
+      utils.getJSON(uri, succeed, error)


Fixes #1961, Fixes #1974, compound assignments to an implicit object.

--- lexer.coffee
+++ lexer.coffee
@@ -515,7 +515,7 @@
   unfinished: ->
     LINE_CONTINUER.test(@chunk) or
     @tag() in ['\\', '.', '?.', 'UNARY', 'MATH', '+', '-', 'SHIFT', 'RELATION'
-               'COMPARE', 'LOGIC', 'COMPOUND_ASSIGN', 'THROW', 'EXTENDS']
+               'COMPARE', 'LOGIC', 'THROW', 'EXTENDS']

   # Converts newlines for string literals.
   escapeLines: (str, heredoc) ->


Bug fix

Bug fix

--- lib/model_extensions.coffee
+++ lib/model_extensions.coffee
@@ -100,8 +100,7 @@

   model_type.findOne = (query, callback) ->
     [query, callback] = [{}, query] if arguments.length is 1
-    query.$one = true
-    model_type::sync('cursor', query).toModels(callback)
+    model_type::sync('cursor', _.extend({$one: true}, query)).toModels(callback)

   model_type.findOrCreate = (data, callback) ->
     throw 'findOrCreate requires object data' if not _.isObject(data) or (data instanceof Backbone.Model) or (data instanceof Backbone.Collection)


Reduce verbosity of doc output

Reduce verbosity of doc output

--- grunt/quidoc.coffee
+++ grunt/quidoc.coffee
@@ -37,14 +37,22 @@
   constructor: ( @regexes ) ->

   extract: ( source ) ->
-    baseClassName: @baseClassName source
-    className: @controlClassName source
-    members: @memberComments source
-    requiredClasses: @requiredClasses source
+    className = @controlClassName source
+    baseClass = @baseClass source
+    members = @memberComments source
+    requiredClasses = @requiredClasses source
+    results = { className }
+    if baseClass? and baseClass != "Control"
+      results.baseClass = baseClass
+    if members?
+      results.members = members
+    if requiredClasses?
+      results.requiredClasses = requiredClasses
+    results

   # Return the name for the control class defined in the source text.
-  baseClassName: ( source ) ->
-    match = @regexes.baseClassName.exec source
+  baseClass: ( source ) ->
+    match = @regexes.baseClass.exec source
     match?[1]

   # Remove the comment indicator (e.g., "*") on interior block comment lines.
@@ -61,10 +69,11 @@
   # setters invoked on the base class.
   content: ( source ) ->
     match = @regexes.content.exec source
-    # Return the first non-empty form of content which matched
-    for i in [1..match.length]
-      if match?[i]
-        return match?[i]
+    if match?
+      # Return the first non-empty form of content which matched
+      for i in [1..match.length]
+        if match?[i]
+          return match?[i]

   # Return the name for the control class defined in the source text.
   controlClassName: ( source ) ->
@@ -106,6 +115,7 @@
     requiredClasses = implicit.concat explicit
     unique = @_unique requiredClasses
     unique.sort()
+    if unique.length > 0 then unique else null

   # Return the first capture group for each match of a regex against source text.
   _matches: ( regex, source ) ->
@@ -125,7 +135,7 @@

 # Extracts docs from CoffeeScript controls
 coffeeDocsExtractor = new DocsExtractor
-  baseClassName: ///
+  baseClass: ///
     \r?\n                     # Start of line (no indentation)
     class                     # "class" keyword
     \s+                       # Whitespace
@@ -199,7 +209,7 @@

 # Extracts docs from QuickUI markup controls
 markupDocsExtractor = new DocsExtractor
-  baseClassName: ///
+  baseClass: ///
     <prototype>               # Opening prototype tag
     \s*                       # Optional whitespace
     <                         # Open tag for base class instance
@@ -293,8 +303,11 @@
     if extractor?
       source = fs.readFileSync filePath, "utf8"
       controlDocs = extractor.extract source
-      if controlDocs.className?
-        docs[ controlDocs.className ] = controlDocs
+      className = controlDocs.className
+      if className?
+        # Don't need two copies of class name in JSON output
+        delete controlDocs.className
+        docs[ className ] = controlDocs
   docs


Error propagation.

Error propagation.

--- indexer.coffee
+++ indexer.coffee
@@ -19,7 +19,7 @@
 			if err?
 				return callback err, null
 			page = cheerio.load(body)
-			package_names = page.root().text().split('\n').slice(1, 1000)  # FIXME: testing on just 200 packages
+			package_names = page.root().text().split('\n').slice(1, 10000)  # FIXME: testing on just 200 packages
 			async.map(
 				package_names,
 				(package_name, cb) ->
@@ -27,9 +27,12 @@
 					opts =
 						uri: "https://pypi.python.org/pypi/#{package_name}/json"
 						json: true
+						timeout: 600 * 1000
 					request.get opts, (err, response, body) ->
 						if err?
-							return cb err, null
+							error = new Error("Error: could not get #{package_name} from pypi. status: #{response?.statusCode}, message: #{err.message}")
+							error.err = err
+							return cb error, null
 						info =
 							name: package_name,
 							details: if response.statusCode == 200 then body else null
@@ -61,6 +64,8 @@
 		console.log ">> got all conda packages"

 		Indexer.getPipPackages (err, pip_packages) ->
+			if err?
+				return callback err
 			console.log ">> got all pip packages"
 			pip_names = _.keys(pip_packages)
 			pip_and_conda = _.intersection(conda_names, pip_names)
@@ -99,6 +104,8 @@

 args = process.argv.slice(2)
 Indexer.build (err, result) ->
+	if err
+		throw err
 	if '--save' in args
 		result_json = JSON.stringify(result, null, 2)
 		fs.writeFileSync(__dirname + '/data/packageIndex.json', result_json)


Fix macro extended function bugs

Fix macro extended function bugs

--- grammars/stata.cson
+++ grammars/stata.cson
@@ -927,14 +927,11 @@
   'macro-extended-functions':
     'patterns': [
       {
-        'include': '#comments'
-      }
-      {
         'match': '\\b(properties)\\b'
         'name': 'keyword.macro.extendedfcn.stata'
       }
       {
-        'match': '\\b(t(ype|yp|y)?|f(ormat|orma|orm|or|o)?|val(ue|u)?\\s+l(able|abl|ab|a)?|var(iable|iabl|iab|ia|i)?\\s+l(able|abl|ab|a)?|data\\s+l(able|abl|ab|a)?|sort(edby|edb|ed|e)?|lab(el|e)?|maxlength|constraint|char)\\b'
+        'match': '\\b(t(ype|yp|y)?|f(ormat|orma|orm|or|o)?|val(ue|u)?\\s+l(able|abl|ab|a)?|var(iable|iabl|iab|ia|i)?\\s+l(abel|abe|ab|a)?|data\\s+l(able|abl|ab|a)?|sort(edby|edb|ed|e)?|lab(el|e)?|maxlength|constraint|char)\\b'
         'name': 'keyword.macro.extendedfcn.stata'
       }
       {
@@ -954,8 +951,15 @@
         'name': 'keyword.macro.extendedfcn.stata'
       }
       {
-        'match': '\\b(di(splay|spla|spl|sp|s)?)\\b'
-        'name': 'keyword.macro.extendedfcn.stata'
+        'begin': '\\b(display|displa|displ|disp|dis|di)\\b'
+        'end': '(?=//|\\n)'
+        'beginCaptures':
+          '0': 'keyword.macro.extendedfcn.stata'
+        'patterns': [
+          {
+            'include': '$self'
+          }
+        ]
       }
       {
         'match': '\\b(list)\\b'
@@ -982,10 +986,6 @@
               {
                 'include': '#macro-global'
               }
-              {
-                'match': '.'
-                'name': 'entity.name.type.class.stata'
-              }
             ]
       }
       {
@@ -1012,26 +1012,44 @@
             'name': 'keyword.macro.extendedfcn.stata'
       }
       {
-        'match': '\\b(subinstr\\s+(loc(al|a)?|gl(obal|oba|ob|o)?))\\s+([^\']+)'
-        'captures':
+        'begin': '\\b(subinstr\\s+(loc(al|a)?|gl(obal|oba|ob|o)?))\\s+([A-Za-z_][A-Za-z0-9_]{0,31})'
+        'end': '(?=//|\\n)'
+        'beginCaptures':
           '1':
             'name': 'keyword.macro.extendedfcn.stata'
           '5':
-            'patterns': [
-              {
-                'include': '#macro-local'
-              }
-              {
-                'include': '#macro-global'
-              }
-              {
-                'include': '#string-regular'
-              }
-              {
-                'match': '.'
+            'name': 'entity.name.type.class.stata'
+        'patterns': [
+          {
+            'include': '#macro-local'
+          }
+          {
+            'include': '#macro-global'
+          }
+          {
+            'include': '#string-compound'
+          }
+          {
+            'include': '#string-regular'
+          }
+          {
+            'match': '(count|coun|cou|co|c)(\\()(local|loca|loc|global|globa|glob|glo|gl)\\s+([A-Za-z_][A-Za-z0-9_]{0,31})(\\))'
+            'captures':
+              '1':
+                'name': 'support.function.builtin.stata'
+              '2':
+                'name': 'punctuation.definition.parameters.begin.stata'
+              '3':
+                'name': 'keyword.macro.extendedfcn.stata'
+              '4':
                 'name': 'entity.name.type.class.stata'
-              }
-            ]
+              '5':
+                'name': 'punctuation.definition.parameters.end.stata'
+          }
+        ]
+      }
+      {
+        'include': '#comments'
       }
       {
         'include': '#macro-local'
@@ -1078,7 +1096,7 @@
         'end': '\''
         'endCaptures':
           '0':
-            'name': 'punctuation.definition.blahblah.string.end.stata'
+            'name': 'punctuation.definition.string.end.stata'
         'patterns': [
           {
             'include': '$self'
@@ -1095,7 +1113,7 @@
         'end': '\''
         'endCaptures':
           '0':
-            'name': 'punctuation.definition.blahblah.string.end.stata'
+            'name': 'punctuation.definition.string.end.stata'
         'contentName': 'meta.macro-extended-function.stata'
         'patterns': [
           {
@@ -1112,6 +1130,35 @@
           }
           {
             'include': '#string-regular'
+          }
+        ]
+      }
+      {
+        'begin': '(`)(macval)(\\()'
+        'beginCaptures':
+          '1':
+            'name': 'punctuation.definition.string.begin.stata'
+          '2':
+            'name': 'support.function.builtin.stata'
+          '3':
+            'name': 'punctuation.definition.parameters.begin.stata'
+        'end': '(\\))(\')'
+        'endCaptures':
+          '1':
+            'name': 'punctuation.definition.parameters.begin.stata'
+          '2':
+            'name': 'punctuation.definition.string.end.stata'
+        'contentName': 'meta.macro-extended-function.stata'
+        'patterns': [
+          {
+            'include': '#macro-local'
+          }
+          {
+            'include': '#macro-global'
+          }
+          {
+            'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'
+            'name': 'entity.name.type.class.stata'
           }
         ]
       }
@@ -1394,6 +1441,9 @@
           }
           {
             'include': '#constants'
+          }
+          {
+            'include': '#functions'
           }
         ]
       }


Remove old history view

Remove old history view

--- lib/old-history-view.coffee
+++ lib/old-history-view.coffee
@@ -1,156 +0,0 @@
-TemplateHelper = require './template-helper'
-GitHistory = require './git-history'
-PatchView = require './patch-view'
-CommitHeaderView = require './commit-header-view'
-timeago = require 'timeago'
-_ = require 'underscore-contrib'
-$ = require 'jquery'
-
-BaseTemplate = """
-

-

-
-
-

-

-

-

-"""
-
-CommitSummaryTemplateString = """
-

-

-
-

-

-

-

-         by
-
-

-

-

-

-"""
-
-class HistoryView extends HTMLElement
-  initialize: ({@uri}) ->
-
-  createdCallback: ->
-    atom.commands.add 'git-experiment-history-view .data',
-      'core:move-down': => @moveSelectionDown()
-      'core:move-up': => @moveSelectionUp()
-
-    window.diffsView = this
-
-    @history = new GitHistory()
-
-    @innerHTML = BaseTemplate
-    @historyNode = @querySelector('.scroller')
-    @diffsNode = @querySelector('.diffs')
-
-    @historyNode.addEventListener 'click', (e) =>
-      commitNode = e.target
-      while commitNode? && !commitNode.classList?.contains('commit')
-        commitNode = commitNode.parentNode
-      if commitNode?
-        sha = commitNode.querySelector('.sha').dataset['sha']
-        @renderCommitDetail(sha)
-
-    @commitTemplate = TemplateHelper.addTemplate(this, CommitSummaryTemplateString)
-
-    $(window).on 'focus', =>
-      @renderHistory()
-
-    @history.getBranch().then (branch) =>
-      name = branch.name().replace('refs/heads/', '')
-      @querySelector('.branch-name').textContent = name
-
-    process.nextTick =>
-      # HACK: atom.workspace is weirdly not available when this is deserialized
-      atom.workspace.onDidChangeActivePaneItem (pane) =>
-        @renderHistory() if pane is this
-
-  attatchedCallback: ->
-    @renderHistory()
-
-  getTitle: ->
-    'Git History View'
-
-  getURI: -> @uri
-
-  serialize: ->
-    deserializer: 'GitHistoryView'
-    uri: @getURI()
-
-  renderHistory: ->
-    new Promise (resolve, reject) =>
-      @historyNode.innerHTML = ''
-      @history.walkHistory (commit) =>
-        promise = @renderCommitSummary(commit)
-        resolve(commit)
-        promise
-    .then (firstCommit) =>
-      @historyNode.focus()
-      @renderCommitDetail(firstCommit.sha())
-
-  renderCommitSummary: (commit) ->
-    # We still dont have a good view framework yet
-    commitNode = TemplateHelper.renderTemplate(@commitTemplate)
-    commitNode.querySelector('.sha').textContent = commit.sha().slice(0,8)
-    commitNode.querySelector('.sha').dataset['sha'] = commit.sha()
-    commitNode.querySelector('.author').textContent = commit.author().name()
-    commitNode.querySelector('.time').textContent = timeago(commit.date())
-    commitNode.querySelector('.message').textContent = commit.message().split('\n')[0]
-    commitNode.querySelector('.avatar img').src = GitHistory.authorAvatar(commit.author().email())
-    commitNode.firstElementChild.id = "sha-#{commit.sha()}"
-    @historyNode.appendChild(commitNode)
-
-  renderCommitDetail: (sha) ->
-    diffsNode = @diffsNode
-    headerView = new CommitHeaderView
-    headerView.setData(@history.getCommit(sha))
-
-    diffsNode.innerHTML = ''
-    diffsNode.appendChild(headerView)
-
-    @selectCommit(@querySelector("#sha-#{sha}"))
-
-    setImmediate =>
-      @history.getDiff(sha).then (diffList) =>
-        window.actionTimestamp = actionTimestamp = Date.now()
-
-        chunkSize = 5
-        promise = Promise.resolve()
-        diffList.forEach (diff) =>
-          _.chunkAll(diff.patches(), chunkSize).forEach (patches) =>
-            promise = promise.then -> new Promise (resolve) =>
-              return unless actionTimestamp == window.actionTimestamp
-              setImmediate ->
-                patches.forEach (patch) =>
-                    patchView = new PatchView
-                    patchView.setPatch(patch)
-                    diffsNode.appendChild(patchView, @)
-                    diffsNode.style.webkitTransform = 'scale(1)' # fixes redraw issues
-                resolve()
-
-  selectCommit: (el) ->
-    return unless el
-
-    for commitNode in @querySelectorAll('.commit')
-      commitNode.classList.remove('selected')
-
-    el.classList.add('selected')
-
-    if @historyNode.offsetHeight + @historyNode.scrollTop - el.offsetTop - el.offsetHeight < 0
-      el.scrollIntoView(false) # off the bottom of the scroll
-    else if el.offsetTop < @historyNode.scrollTop
-      el.scrollIntoView()
-
-  moveSelectionUp: ->
-    @querySelector(".commit.selected").previousElementSibling?.click()
-
-  moveSelectionDown: ->
-    @querySelector(".commit.selected").nextElementSibling?.click()
-
-module.exports = document.registerElement 'git-experiment-history-view', prototype: HistoryView.prototype


Added debugging statements

Added debugging statements

--- lib/service/dataLink.coffee
+++ lib/service/dataLink.coffee
@@ -99,6 +99,7 @@
       return errorResult

     completionHandler = (entity) ->
+      console.log "In completion handler"
       entityParser = entityHelper environmentId

       responseCallback = callback
@@ -159,9 +160,10 @@
         responseCallback null, task

       next = ->
+        console.log "In next handler"
         unless result.statusCode?
           result.statusCode = 200
-
+        console.log "About to parse entity(s)"
         if result.statusCode < 400 and entityParser.isKinveyEntity(entity) is false
           if entity.constructor isnt Array
             entity = entityParser.entity entity
@@ -169,9 +171,11 @@
         result.continue = true

         if result.statusCode >= 400
+          console.log "About to parse error"
           convertToError result.body

         task.request = result
+        console.log "About to process callback"
         responseCallback null, task

       methods =
@@ -193,24 +197,33 @@
     return completionHandler

   process = (task, modules, callback) ->
+    console.log "Processing entry"
+    console.log task
     unless task.collectionName?
       return callback new Error "CollectionName not found"

     collectionToProcess = collection task.collectionName
     dataOp = ''
-
+    console.log "Generating completion handler"
     completionHandler = initCompletionHandler task

+    console.log "Checking dataop"
     if task.method is 'POST'
+      console.log "DataOp is insert"
       dataOp = 'onInsert'
     else if task.method is 'PUT'
+      console.log "DataOp is update"
       dataOp = 'onUpdate'
     else if task.method is 'GET' and task.endpoint isnt '_count'
+      console.log "DataOp is GET"
       if task.entityId?
+        console.log "GetById"
         dataOp = 'onGetById'
       else if task.query?
+        console.log "getByQuery"
         dataOp = 'onGetByQuery'
       else
+        console.log "GetAll"
         dataOp = 'onGetAll'
     else if task.method is 'GET' and task.endpoint is '_count'
       if task.query?
@@ -218,6 +231,7 @@
       else
         dataOp = 'onGetCount'
     else if task.method is 'DELETE'
+      console.log "DataOp is Delete"
       if task.entityId?
         dataOp = 'onDeleteById'
       else if task.query?
@@ -225,14 +239,18 @@
       else
         dataOp = 'onDeleteAll'
     else
+      console.log "Problem - no dataOp"
       return callback new Error "Cannot determine data operation"

+    console.log "Getting handler function"
     operationHandler = collectionToProcess.resolve dataOp
+    console.log "Handler is #{typeof operationHandler}"

     if operationHandler instanceof Error
       return callback(convertToError operationHandler)

     # TODO Need to handle runtime errors/unhandled exceptions - or do we?
+    console.log "Binding domain for error handling"
     taskDomain = domain.create()

     taskDomain.on 'error', (err) =>
@@ -244,7 +262,7 @@

     domainBoundOperationHandler = taskDomain.bind operationHandler

-
+    console.log "about to execute function"
     domainBoundOperationHandler task.request, completionHandler

   obj =


router: optimized hashbang rendering

router: optimized hashbang rendering

--- lib/web/views/router.coffee
+++ lib/web/views/router.coffee
@@ -25,6 +25,7 @@
 # - copy build configs from bacon.js
 # - split to own repository
 # - more complete sinatra path parsing, JavascriptRouteParser
+# - test suite
 # Known issues:
 # - hashbang urls don't work in a href tags -> won't fix, use /plain/urls
 # - does not resolve situation hashbang url needs to be converted and both window.location.pathname and window.location.hash are defined
@@ -62,10 +63,10 @@
   pushStateSupport: history && history.pushState
   hashchangeSupport: "onhashchange" of window
   hashBaseUrl: false
+  hashPreviousView: false
   disableUrlUpdate: false
   fallbackRoute: false
   initPushState: () =>
-    # check if current url needs to be changed pushState <-> hashbang
     @attachClickListener()
     @attachLocationChangeListener()
     @renderInitialView()
@@ -81,9 +82,10 @@
             event.preventDefault();
             @updateUrl(href)
         else
-          if @find(href)
+          if @exec(href)
             @log("Updating hash with", href)
             event.preventDefault();
+            @hashPreviousView = href
             @updateUrl(href)

   attachLocationChangeListener: =>
@@ -95,9 +97,11 @@
     else
       if @hashchangeSupport
         window.onhashchange = (event) =>
-          href = window.location.hash.substring(2)
-          @log("Rendering onhashchange", href)
-          @renderUrl(href)
+          if window.location.hash.substring(0, 2) == "#!"
+            href = window.location.hash.substring(2)
+            if href != @hashPreviousView
+              @log("Rendering onhashchange", href)
+              @renderUrl(href)

   renderInitialView: =>
     @log("Rendering initial page")


Fixed location marker

Fixed location marker

Marker was creating infinite loop, fixed now with subscribe function

--- client.coffee
+++ client.coffee
@@ -27,14 +27,18 @@
         scoresContent()
     else if page == 'log'
         logContent()
-	Geoloc.auth !->
+	if !Geoloc.isSubscribed()
+		Geoloc.subscribe()
+	if Geoloc.isSubscribed()
 		state = Geoloc.track()
 		location = state.get('latlong');
-		location = location.split(',')
-		marker = L.marker(L.latLng(location[0], location[1]))
-		marker.bindPopup("Hier ben ik nu!" + "
 accuracy: " + state.get('accuracy'))
-		marker.addTo(window.map)
-
+		if location?
+			location = location.split(',')
+			marker = L.marker(L.latLng(location[0], location[1]))
+			marker.bindPopup("Hier ben ik nu!" + "
 accuracy: " + state.get('accuracy'))
+			marker.addTo(window.map)
+		else
+			log 'location could not be found'

 # Load the javascript necessary for the map


fixed score list, though still buggy with localplayer's score

--- client.coffee
+++ client.coffee
@@ -38,24 +38,7 @@
 			Dom.text "Click"

 		Dom.onTap !->
-			###
-			counter = Db.shared.ref 'counters', App.userId()
-			if counter?
-				Server.sync 'incr', !->
-					counter.incr()
-			else
-				Server.call 'incr'
-			###
-			###
-			if not buffer.peek() # buffer still 0? then this is the first click
-				Obs.onTime 1000, !-> flushBuffer buffer
-				log 'buffered', buffer
 			buffer.incr()
-			log 'buffer' #, buffer.get()
-			###
-
-			buffer.incr()
-
 			if buffer.get() is 1
 				Obs.onTime 1000, !-> flushBuffer buffer

@@ -87,7 +70,7 @@
 renderScores = (buffer) !->
 	Ui.list !->
 		Dom.style margin: '0 15px'
-		Db.shared.iterate 'counters', renderScore, (counter) -> -(counter.get() + if (+counter.key() is App.userId()) then buffer.get() else 0)
+		Db.shared.iterate 'counters', renderScore, (counter) -> -(counter.get() + if (counter.key() is App.userId()) then buffer.get() else 0)

 renderScore = (counter) !->
 	Ui.item !->


Support chaining of templatized resources

--- client.coffee
+++ client.coffee
@@ -90,8 +90,8 @@
   create_resource: (name, template) ->
     (options) =>
       constructor = @resource_constructors[name]
-      url = @generate_url(template, options)
-      return new constructor(url: url)
+      options.url = @generate_url(template, options)
+      return new constructor(options)

   generate_url: (template, options) ->
     parts = template.split("/")