create fake previous mark generator (while we work on getting real data)

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -102,14 +102,42 @@
       sum = sum + value
     return sum / data.length

+  showFakePrevMarks: () ->
+    howMany = Math.floor( Math.random() * (10-1) + 1 )
+    console.log 'generating ', howMany, ' (fake) marks'
+    @generateFakePrevMarks( howMany )
+    for entry in [@prevMarks...]
+      console.log '[',entry.xL,',',entry.xR,']'
+      @highlightCurve(entry.xL,entry.xR)
+
+  generateFakePrevMarks: (n) ->
+    minWid = 0.5 # [days]
+    maxWid = 3.0
+    console.log 'generateFakePrevMarks()'
+    xPos = []
+    wid = []
+    @prevMarks = []
+    for i in [0...n]
+      wid[i] = Math.random() * ( maxWid - minWid ) + minWid
+      xPos[i] = Math.random() * (36-1) + 1
+      # console.log wid[i], xPos[i]
+      # console.log '[',xPos[i]-wid[i]/2,',',xPos[i]+wid[i]/2,']'
+      @prevMarks[i] = { xL: xPos[i]-wid[i]/2, xR: xPos[i]+wid[i]/2 }
+
+  showPrevMarks: ->
+    for entry in [@prevMarks...]
+      console.log '[',entry.xL,',',entry.xR,']'
+      @highlightCurve(entry.xL,entry.xR)
+
   highlightCurve: (xLeft,xRight) ->
     for i in [0...@dataLength]
       if @data.x[i] >= xLeft and @data.x[i] <= xRight
         x = ((+@data.x[i]-@xMin)/(@xMax-@xMin)) * @canvas.width
         y = ((+@data.y[i]-@yMin)/(@yMax-@yMin)) * @canvas.height
         y = -y + @canvas.height # flip y-values
-        @ctx.fillStyle = "#fc4541"
+        @ctx.fillStyle = "rgba(252, 69, 65, 0.65)" #"#fc4541"
         @ctx.fillRect(x,y,2,2)
+    return

   plotPoints: (xMin = @smallestX, xMax = @largestX, yMin = @smallestY, yMax = @largestY) ->
     @xMin = xMin
@@ -129,7 +157,6 @@
       y = -y + @canvas.height # flip y-values
       @ctx.fillStyle = "#fff" #fc4541"
       @ctx.fillRect(x,y,2,2)
-
     if @marks
       for mark in @marks.all
         scaledMin = ((mark.dataXMinRel - xMin) / (xMax - xMin)) * @canvas.width
@@ -137,11 +164,9 @@
         mark.element.style.width = (scaledMax-scaledMin) + "px"
         mark.element.style.left = (scaledMin) + "px"
         mark.save(scaledMin, scaledMax)
-
     # @highlightCurve(10,14) # test
-
-
     @scale = (@largestX - @smallestX) / (@xMax - @xMin)
+    return

   drawXTickMarks: (xMin, xMax) ->
     tickMinorInterval = 1


update updateCursor

update updateCursor

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -80,12 +80,13 @@
     @element.className = "mark"

     @element.innerHTML = """
-

-
-

+

+
+

+

-

+

     """
@@ -102,9 +103,7 @@
     @dragging = false

     @element.addEventListener 'mousemove', @updateCursor
-
     @element.addEventListener 'mousedown', @onMouseDown
-
     @element.addEventListener 'click', (e) => @canvasGraph.marks.remove(@) if @pointerYInElement(e) < 15

   draw: (e) ->
@@ -133,24 +132,23 @@
     @dataXMax = @canvasGraph.toDataXCoord(@canvasXMax)

   updateCursor: (e) =>
-    if @pointerYInElement(e) < 15
-      @element.style.cursor = "pointer"
-    else if ((Math.abs @pointerXInElement(e) - (@canvasXMax-@canvasXMin)) < 10) || @pointerXInElement(e) < 10
-      @element.style.cursor = "ew-resize"
-    else
-      @element.style.cursor = "move"
+    switch e.target.className
+      when "mark" then @element.style.cursor = "move"
+      when "left-border" then @element.style.cursor = "ew-resize"
+      when "right-border" then @element.style.cursor = "ew-resize"
+      when "top-bar" then @element.style.cursor = "pointer"

   onMouseMove: (e) =>
     e.preventDefault()
     @draw(e) if @dragging
     @move(e) if @moving
-    @updateCursor(e) if @hovering

   onMouseDown: (e) =>
     e.preventDefault()
     window.addEventListener 'mousemove', @onMouseMove
     window.addEventListener 'mouseup', @onMouseUp

+    console.log e.target.className
     @element.parentNode.appendChild(@element) #move to end of container for z index
     if (Math.abs @pointerXInElement(e) - (@canvasXMax-@canvasXMin)) < 10
       @startingPoint = @canvasXMin


switch to minor-button style

switch to minor-button style

--- app/pages/lab/workflow.cjsx
+++ app/pages/lab/workflow.cjsx
@@ -355,7 +355,7 @@
               <br />
               <AutoSave resource={@props.workflow}>
                 <small>
-                  <button type="button" className="standard-button" onClick={@handleTaskDelete.bind this, @state.selectedTaskKey}>Delete this task</button>
+                  <button type="button" className="minor-button" onClick={@handleTaskDelete.bind this, @state.selectedTaskKey}>Delete this task</button>
                 </small>
               </AutoSave>
             </div>


Updated twitter messaging

Updated twitter messaging

--- framer/Components/ShareComponent.coffee
+++ framer/Components/ShareComponent.coffee
@@ -417,19 +417,18 @@
 				backgroundImage: "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAcCAYAAABRVo5BAAAABGdBTUEAALGPC/xhBQAAAO1JREFUOBFjZMABWltbJX///l0GlDYAYg1GRkZxIP3n////P4E0EyOQwABNTU05QAVtQMyLIQkVwNDY3Nzs9vfv3524NMDEmWAMED1z5kyuf//+zUEWw8VG0fjq1SsnoPNkcSlGFmdB5gA16SHzQWxgoPxnZma2UVNTOxkWFvYXJo+uUQwmAaOBhn2tqak5BuPDaBSnAhXJwySQaB4kNpyJohEoqgCXIcAgWyNjQ0PDfwKGY0gDA+w5uo0YirAJAMPiLFkagTaeIUsjExMTeTaCNGIkcmyBBRTDUEeWU0EBNqoRW7KBio0GDi0CBwAHJ0YrwGtXbwAAAABJRU5ErkJggg==')"

 		# Generate tweet
-
 		tweet = ""
 		if @shareInfo.twitter or @shareInfo.author
-			tweet += "A prototype by "
+

 			if @shareInfo.twitter
-				tweet += "@" + @shareInfo.twitter
+				tweet += "A prototype by @#{@shareInfo.twitter}. Design without limitations in @framerjs — "
 			else if @shareInfo.author
-				tweet += @shareInfo.author
-
-			tweet += ". "
-
-		tweet += "Design without limitations in @framerjs — #{window.location.href}"
+				tweet += "A prototype by @#{@shareInfo.author}. Design without limitations in @framerjs — "
+			else
+				tweet += "A @framerjs prototype by @#{@shareInfo.author}. Design without limitations — "
+
+		tweet += window.location.href
 		tweet = encodeURIComponent(tweet)

 		@buttonTwitter = new Button


increase clock skew warning message thresh from 10s to 30s

--- src/smc-webapp/alerts.coffee
+++ src/smc-webapp/alerts.coffee
@@ -98,7 +98,7 @@
 check_for_clock_skew = () ->
     local_time = new Date()
     s = Math.ceil(Math.abs(salvus_client.server_time() - local_time)/1000)
-    if s > 10
+    if s > 30
         exports.alert_message(type:'error', timeout:9999,  message:"Your computer's clock is off by about #{s} seconds!  You MUST set it correctly to use SageMathCloud.  Expect very serious problems until you do.")

 # Wait until after the page is loaded and clock sync'd before checking for skew.


improved visibility listener. renamed to focus listener.

--- core/KDWindowController.coffee
+++ core/KDWindowController.coffee
@@ -13,6 +13,18 @@
   superKey        = if navigator.userAgent.indexOf("Mac OS X") is -1 then "ctrl" else "command"
   addListener     = (eventName, listener, capturePhase=yes)->
     document.body.addEventListener eventName, listener, capturePhase
+
+  # Finding vendor prefixes for visibility
+  getVisibilityProperty = ->
+    prefixes = ["webkit", "moz", "o"]
+    return "hidden" if `"hidden" in document`
+    return "#{prefix}Hidden" for prefix in prefixes when `prefix + "Hidden" in document`
+    return null
+
+  isFocused = -> Boolean document[getVisibilityProperty()]
+
+  getVisibilityEventName = ->
+    return "#{getVisibilityProperty().replace(/[Hh]idden/, '')}visibilitychange"

   constructor:(options,data)->

@@ -24,7 +36,7 @@
     @scrollingEnabled      = yes
     @layers                = []
     @unloadListeners       = []
-    @visibilityListeners   = []
+    @focusListeners        = []

     @bindEvents()
     @setWindowProperties()
@@ -112,29 +124,17 @@
           window.location.replace '/'
         cookie = $.cookie 'clientId'

-    # Finding vendor prefixes
-    getVisibilityProperty = ->
-      prefixes = ["webkit", "moz", "o"]
-      return "hidden" if `"hidden" in document`
-      return "#{prefix}Hidden" for prefix in prefixes when `prefix + "Hidden" in document`
-      return null
-
-    isHidden = -> Boolean document[getVisibilityProperty()]
-
-    getVisibilityEventName = ->
-      return "#{getVisibilityProperty().replace(/[Hh]idden/, '')}visibilitychange"
-
     document.addEventListener getVisibilityEventName(), (event)=>
-      @visibilityChange event, isHidden()
+      @focusChange event, isFocused()

   addUnloadListener:(listener)-> @unloadListeners.push listener

-  addVisibilityListener: (listener)-> @visibilityListeners.push listener
-
-  visibilityChange: (event, state)->
+  addFocusListener: (listener)-> @focusListeners.push listener
+
+  focusChange: (event, state)->

     return unless event
-    listener state, event for listener in @visibilityListeners
+    listener state, event for listener in @focusListeners

   beforeUnload:(event)->


fix: typo

fix: typo

--- test/basic.coffee
+++ test/basic.coffee
@@ -87,7 +87,7 @@
 			, 10

 randomPromise = (i) ->
-	new Promise (r) ->
+	new Yaku (r) ->
 		setTimeout ->
 			r(i)
 		, Math.random() * 100


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/color.coffee
+++ test/color.coffee
@@ -177,6 +177,11 @@
 test "should parse 549E00", ->
   color = Color(549E00)

+  equals color.r(), 84
+  equals color.g(), 158
+  equals color.b(), 0
+  equals color.a(), 1
+
 test "should be able to lighten", ->
   color = Color('#f00').lighten(0.1)


method to add a blank distractor

method to add a blank distractor

--- src/stores/vocabulary.coffee
+++ src/stores/vocabulary.coffee
@@ -26,7 +26,7 @@
     @loaded(template, id)

   updateDistractor: (id, oldValue, newValue) ->
-    distractor_literals = @_get(id).distractor_literals or []
+    distractor_literals = _.clone(@_get(id).distractor_literals or [])
     index = _.indexOf distractor_literals, oldValue
     if -1 is index
       distractor_literals.push(newValue)
@@ -34,6 +34,11 @@
       distractor_literals.splice(index, 1)
     else
       distractor_literals[index] = newValue
+    @_change(id, {distractor_literals})
+
+  addBlankDistractor: (id, index) ->
+    distractor_literals = _.clone(@_get(id).distractor_literals or [])
+    distractor_literals.splice(index, 0, '')
     @_change(id, {distractor_literals})

   change: (id, attrs) ->
@@ -57,7 +62,7 @@
     getTemplate: (id) ->
       term: ''
       definition: ''
-      distractor_literals: []
+      distractor_literals: ['']
       tags: ['dok:1', 'blooms:1', 'time:short']

     hasExercise: (id) -> @_get(id)?.exercise_uids?.length


:art: Fix spec title

:art: Fix spec title

--- spec/config-spec.coffee
+++ spec/config-spec.coffee
@@ -1139,7 +1139,7 @@
                 type: 'string'
                 default: 'def'

-        it "the values set respect the new schema", ->
+        it "respects the new schema when values are set", ->
           expect(atom.config.set('foo.bar.str', 'global')).toBe true
           expect(atom.config.set('foo.bar.str', 'scoped', scopeSelector: '.source.js')).toBe true
           expect(atom.config.get('foo.bar.str')).toBe 'global'


:lipstick: Require Editor and Range from atom

--- spec/editor-spec.coffee
+++ spec/editor-spec.coffee
@@ -1,6 +1,4 @@
-{_, $, $$, fs, RootView} = require 'atom'
-Editor = require '../src/editor'
-{Range} = require 'telepath'
+{_, $, $$, fs, Editor, Range, RootView} = require 'atom'
 path = require 'path'

 describe "Editor", ->


Fix bad comment

Fix bad comment

Signed-off-by: Ian Macalinao <08370e385f4778cad8fe504ec5445edd3d45bd9a@gmail.com>

--- test/items.coffee
+++ test/items.coffee
@@ -41,7 +41,7 @@
           list.should.include items.list["Hextech Gunblade"] # ap and ad
           list.should.include items.list["Hextech Revolver"] # ap
           list.should.include items.list["Bilgewater Cutlass"] # ad
-          list.should.not.include items.list["Warmog's Armor"] # ad
+          list.should.not.include items.list["Warmog's Armor"] # none

       describe "not", ->


Change python style to 100 line limit.

Change python style to 100 line limit.

--- env/atom.config.cson
+++ env/atom.config.cson
@@ -2,7 +2,7 @@
 	"autocomplete-plus":
 		confirmCompletion: "tab and enter"
 	"autoupdate-packages":
-		lastUpdateTimestamp: 1465049325365
+		lastUpdateTimestamp: 1465833100274
 	"bracket-matcher":
 		autocompleteSmartQuotes: false
 	core:
@@ -150,7 +150,7 @@
 	"python-yapf":
 		checkOnSave: false
 		yapfPath: "~/bin/yapf"
-		yapfStyle: "{based_on_style: pep8, column_limit: 120}"
+		yapfStyle: "{based_on_style: pep8, column_limit: 100}"
 	"rst-preview-pandoc": {}
 	"spell-check":
 		grammars: [


Fix Tests

Fix Tests

--- spec/gocode-spec.coffee
+++ spec/gocode-spec.coffee
@@ -90,7 +90,7 @@

   describe 'when the gocode autocomplete-plus provider is enabled', ->

-    fit 'displays suggestions from gocode', ->
+    it 'displays suggestions from gocode', ->
       runs ->
         expect(provider).toBeDefined()
         expect(provider.getSuggestions).not.toHaveBeenCalled()
@@ -114,8 +114,8 @@
         expect(provider.getSuggestions).toHaveBeenCalled()
         expect(provider.getSuggestions.calls.length).toBe(1)
         expect(editorView.querySelector('.autocomplete-plus')).toExist()
-        expect(editorView.querySelector('.autocomplete-plus span.word')).toHaveText('Print(')
-        expect(editorView.querySelector('.autocomplete-plus span.completion-label')).toHaveText('func(a ...interface{}) (n int, err error)')
+        expect(editorView.querySelector('.autocomplete-plus span.word').innerHTML).toBe('Print(a ...interface)')
+        expect(editorView.querySelector('.autocomplete-plus span.completion-label').innerHTML).toBe('func')
         editor.backspace()

     xit 'does not display suggestions when no gocode suggestions exist', ->
@@ -154,14 +154,14 @@
         advanceClock(completionDelay)

       waitsFor ->
-        autocompleteManager.displaySuggestions.calls.length is 1
+        autocompleteManager.displaySuggestions.calls.length is 2

       runs ->
         expect(editorView.querySelector('.autocomplete-plus')).not.toExist()
         editor.insertText(';')

       waitsFor ->
-        autocompleteManager.displaySuggestions.calls.length is 2
+        autocompleteManager.displaySuggestions.calls.length is 3
         advanceClock(completionDelay)

       runs ->


:new: Add a test for ::parse

:new: Add a test for ::parse

--- spec/helper-spec.coffee
+++ spec/helper-spec.coffee
@@ -39,3 +39,10 @@
       waitsForPromise ->
         helpers.execFilePath('cat', [], testFile).then (text) ->
           expect(text).toBe(testContents)
+  describe '::parse', ->
+    it 'cries when no argument is passed', ->
+      gotError = false
+      try
+        helpers.parse()
+      catch erro then gotError = true
+      expect(gotError).toBe(true)


:new: Add tests for execFilePath

:new: Add tests for execFilePath

--- spec/helper-spec.coffee
+++ spec/helper-spec.coffee
@@ -21,4 +21,21 @@
     it 'accepts stdin', ->
       waitsForPromise ->
         helpers.exec('cat', [], stream: 'stdout', stdin: testContents).then (text) ->
-          expect(text).toBe(testContents)+          expect(text).toBe(testContents)
+  describe '::execFilePath', ->
+    it 'cries when no argument is passed', ->
+      gotError = false
+      try
+        helpers.execFilePath()
+      catch erro then gotError = true
+      expect(gotError).toBe(true)
+    it 'cries when no filepath is passed', ->
+      gotError = false
+      try
+        helpers.execFilePath('cat', [])
+      catch erro then gotError = true
+      expect(gotError).toBe(true)
+    it 'works', ->
+      waitsForPromise ->
+        helpers.execFilePath('cat', [], testFile).then (text) ->
+          expect(text).toBe(testContents)


Don't clobber diff module with local variable

--- spec/parser_spec.coffee
+++ spec/parser_spec.coffee
@@ -70,14 +70,12 @@
             # [0], because we don't want the parsed files in the resulting JSON
             generated = JSON.stringify([parser.toJSON()[0]], null, 2)

-          # if the tests complain about "Object #<Object> has no method 'diffLines'"
-          # you can safely ignore it; an earlier test error is causing it
           delta = diff.diffLines(expected, generated)
           if (delta.length > 1)
             console.error "\nFor #{filename}:"
-            for diff in delta
-              if diff.added
+            for hunk in delta
+              if hunk.added
                 console.error "Added: \n#{_.str.strip(diff.value)}"
-              if diff.removed
+              if hunk.removed
                 console.error "Removed: \n#{_.str.strip(diff.value)}"
             console.error delta


call changeEmail before the callback in setDefaultEmailAddress

--- services/web/app/coffee/Features/User/UserUpdater.coffee
+++ services/web/app/coffee/Features/User/UserUpdater.coffee
@@ -112,8 +112,8 @@
 				else if res.n == 0 # TODO: Check n or nMatched?
 					return callback(new Error('Default email does not belong to user'))
 				else
+					NewsletterManager.changeEmail oldEmail, email, ->
 					callback()
-					NewsletterManager.changeEmail oldEmail, email, ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/string_extensions.coffee
+++ src/string_extensions.coffee
@@ -82,7 +82,7 @@
 ###
 String::extension = ->
   if extension = this.match(/\.([^\.]*)$/, '')?.last()
-    extension
+    ex
   else
     ''


string_extensions

--- src/string_extensions.coffee
+++ src/string_extensions.coffee
@@ -1,5 +1,16 @@
 ###*
 Returns true if this string only contains whitespace characters.
+
+

+"".blank()
+# => true
+
+"hello".blank()
+# => false
+
+"   ".blank()
+# => true
+

 @name blank
 @methodOf String#


query by tier 1

query by tier 1

--- api/apps/articles/model.coffee
+++ api/apps/articles/model.coffee
@@ -77,6 +77,7 @@
   fair_ids: @array()
   partner_id: @objectId()
   sort: @string()
+  tier: @number()
 ).call Joi

 #


reworking the demo

reworking the demo


Fix improper indentation

Fix improper indentation

--- test/solid.coffee
+++ test/solid.coffee
@@ -75,7 +75,7 @@
       server = solid {port: port}, ->
                   "/" : -> "home"
       get "/", port: port, (res, data) ->
-          res.statusCode.should.equal 200
-          data.should.equal "home"
-          server.close()
-          done()+        res.statusCode.should.equal 200
+        data.should.equal "home"
+        server.close()
+        done()


grid: remove repeated function and reset model on destroy

--- app/_src/components/grid.coffee
+++ app/_src/components/grid.coffee
@@ -90,6 +90,7 @@
 	destroy: =>
 		@removeWatcherListeners()
 		@removeEventListeners()
+		@resetDOMModel()
 		hasBeenShown = false
 		if spinnerTimerId isnt null then clearTimeout spinnerTimerId

@@ -129,9 +130,6 @@
 			watcher.enterViewport @onEnterViewport
 			watcher.exitViewport @onExitViewport

-	removeWatcherListeners: =>
-		watcher.destroy()
-		watcher = null

 	###
 		Add event callbacks
@@ -149,6 +147,13 @@

 	###
+		Removes watcher events callbacks
+	###
+	removeWatcherListeners: () =>
+		if watcher isnt null
+			watcher.destroy()
+
+	###
 		This function detects which breakpoint are we working on now
 		See the MQ_BREAKPOINT_X constants defined on the top.

@@ -173,18 +178,6 @@

 				@

-	###
-		Removes watcher events callbacks
-	###
-	removeWatcherListeners: () ->
-		if watcher isnt null
-			watcher.destroy()
-
-	###
-		Removes event callbacks
-	###
-	removeEventListeners: () ->
-		$(window).off( 'resize', debouncedResizeFn )

 	###


added new examples

added new examples

--- examples/test.coffee
+++ examples/test.coffee
@@ -2,13 +2,31 @@

 api = new SysAPI({})

-respond = (req, res, next) ->
-    res.send('hello ' + req.params.name)
-    next()
-
-api.get('/hello/:name', respond)
+###->>>>>   NET   <<<<<-###
+api.get('/net/isip/:ip', (req, res, next) ->
+    api.respond(req, res, next,
+        api.net.isIP(req.params.ip)
+    )
+)

+api.get('/net/isv4/:ip', (req, res, next) ->
+    api.respond(req, res, next,
+        api.net.isIPv4(req.params.ip)
+    )
+)

-api.users.getAll()
+api.get('/net/isv6/:ip', (req, res, next) ->
+    api.respond(req, res, next,
+        api.net.isIPv6(req.params.ip)
+    )
+)
+
+###->>>>>   SYS   <<<<<-###
+api.get('/sys/users/all', (req, res, next) ->
+    api.respond(req, res, next,
+        api.sys.users.all()
+    )
+)

 api.connect(8080)
+


Use params also when logging out.

Use params also when logging out.

--- src/scripts/actions/user-actions.coffee
+++ src/scripts/actions/user-actions.coffee
@@ -23,7 +23,9 @@
 		payload:
 			promise: API.logout(data.id).then ->
 				(action, dispatch) ->
-					dispatch ReduxRouter.pushPath data.from
+					params = JSON.parse data.from
+					delete params.userId
+					dispatch ReduxRouter.pushPath URL.getPath(params), params
 					dispatch action
 	setUser: (obj, timestamp) ->
 		type: 'SET_LOGGEDIN_USER'


Rename homePage to home.

Rename homePage to home.

--- app/client/js/react/app.coffee
+++ app/client/js/react/app.coffee
@@ -6,13 +6,16 @@
     @param {app.Routes} routes
     @param {app.react.Header} header
     @param {app.react.Footer} footer
-    @param {app.react.pages.Home} homePage
+    @param {app.react.pages.Home} home
     @param {app.react.pages.EditSong} editSong
     @param {app.react.pages.Song} song
+    @param {app.react.pages.NotFound} notFound
     @constructor
   ###
-  constructor: (routes, header, footer,
-      homePage, editSong, song) ->
+  constructor: (routes,
+      header, footer,
+      home, editSong, song, notFound) ->
+
     {div} = React.DOM

     @create = React.createClass
@@ -21,7 +24,8 @@
         div className: 'app',
           header.create null
           switch routes.active
-            when routes.home then homePage.create null
+            when routes.home then home.create null
             when routes.newSong then editSong.create null
             when routes.song then song.create null
+            when routes.notFound then notFound.create null
           footer.create null


Clean object serialization

Clean object serialization

--- expresscoffee.coffee
+++ expresscoffee.coffee
@@ -25,9 +25,7 @@
     return this._array obj if Array.isArray obj

     code = "{"
-    for k, v of obj
-      code +=  "\"#{ k }\": #{ codeFrom v },"
-
+    code +=  "\"#{ k }\": #{ codeFrom v }," for k, v of obj
     removeTrailingComma(code) + "}"

   _array: (array) ->


also serve app dist dir

also serve app dist dir

--- src/steroids/servers/BuildServer.coffee
+++ src/steroids/servers/BuildServer.coffee
@@ -75,6 +75,7 @@
     super(@options)

     @app.use express.static(Paths.connectStaticFiles)
+    @app.use express.static(Paths.application.distDir)
     @app.use bodyParser.json()

   setRoutes: =>


add comment about hack to codemirror python mode

--- salvus/diffsync.coffee
+++ salvus/diffsync.coffee
@@ -562,6 +562,9 @@
 # Support for using synchronized docs to represent Sage Worksheets (i.e., live compute documents)
 #---------------------------------------------------------------------------------------------------------

+# WARNING: in Codemirror, to avoid issues with parsing I also set the output marker to be a comment character
+# by modifying the python mode as follows:     if (ch == "#"  || ch == "\uFE21") {
+
 exports.MARKERS =
     cell   : "\uFE20"
     output : "\uFE21"


Make a HACK to prevent circular dependencies with [$tgConfirm, $translate] :see_no_evil: :scream_cat:

--- app/coffee/app.coffee
+++ app/coffee/app.coffee
@@ -199,23 +199,23 @@

     $httpProvider.interceptors.push('authHttpIntercept')

-    # If there is an error in the version throw a notify error
-    versionCheckHttpIntercept = ($q, $confirm, $translate) ->
-        versionErrorMsg = $translate.instant("ERROR.VERSION_ERROR")
-
+    # If there is an error in the version throw a notify error.
+    # IMPROVEiMENT: Move this version error handler to USs, issues and tasks repository
+    versionCheckHttpIntercept = ($q) ->
         httpResponseError = (response) ->
             if response.status == 400 && response.data.version
-                $confirm.notify("error", versionErrorMsg, null, 10000)
-
-                return $q.reject(response)
+                # HACK: to prevent circular dependencies with [$tgConfirm, $translate]
+                $injector = angular.element("body").injector()
+                $injector.invoke(["$tgConfirm", "$translate", ($confirm, $translate) =>
+                    versionErrorMsg = $translate.instant("ERROR.VERSION_ERROR")
+                    $confirm.notify("error", versionErrorMsg, null, 10000)
+                ])

             return $q.reject(response)

-        return {
-            responseError: httpResponseError
-        }
-
-    $provide.factory("versionCheckHttpIntercept", ["$q", "$tgConfirm", "$translate", versionCheckHttpIntercept])
+        return {responseError: httpResponseError}
+
+    $provide.factory("versionCheckHttpIntercept", ["$q", versionCheckHttpIntercept])

     $httpProvider.interceptors.push('versionCheckHttpIntercept')

@@ -231,12 +231,16 @@

     $compileProvider.debugInfoEnabled(window.taigaConfig.debugInfo || false)

-    $translateProvider.useStaticFilesLoader({
-        prefix: '/locales/locale-',
-        suffix: '.json'
-    })
-
-    $translateProvider.preferredLanguage(window.taigaConfig.defaultLanguage || 'en')
+    $translateProvider
+        .useStaticFilesLoader({
+            prefix: '/locales/locale-',
+            suffix: '.json'
+        })
+        .preferredLanguage(window.taigaConfig.defaultLanguage || 'en')
+
+    if not window.taigaConfig.debugInfo
+        $translateProvider.fallbackLanguage([window.taigaConfig.defaultLanguage || 'en'])
+

 init = ($log, $config, $rootscope, $auth, $events, $analytics, $translate) ->
     $log.debug("Initialize application")


Remove X-Requested-With HTTP header from defaults

This header prevents Javascript requests to other domains (e.g. https://www.googleapis.com)
Fixes #1

--- app/coffee/app.coffee
+++ app/coffee/app.coffee
@@ -29,3 +29,7 @@
 handleAuthorized = (token) ->
   TM.value 'ACCESS_TOKEN', token
   angular.bootstrap document, ['TM']
+
+TM.config(['$httpProvider', ($httpProvider) ->
+    delete $httpProvider.defaults.headers.common["X-Requested-With"]
+])


Remove randomness from browser crash logging (#168)

No logs for this event have been captured yet (in the last 1 week), removing the randomness to capture 100% crashes since this event does not seem frequent enough.

--- app/core/initialize.ozar.coffee
+++ app/core/initialize.ozar.coffee
@@ -229,7 +229,7 @@
 window.onload = () ->
   if window.sessionStorage
     # Check if the browser crashed before the current loading in order to log it on datadog
-    if window.sessionStorage.getItem('oz_exit') and window.sessionStorage.getItem('oz_exit') != 'true' and Math.random() < 0.1
+    if window.sessionStorage.getItem('oz_exit') and window.sessionStorage.getItem('oz_exit') != 'true'
       window.sessionStorage.setItem('oz_crashed', 'true');
     window.sessionStorage.setItem('oz_exit', 'pending');


Add doc static tag.

Add doc static tag.

--- src/common/rest.resource.coffee
+++ src/common/rest.resource.coffee
@@ -2,6 +2,7 @@

 ###*
  * @class Rest
+ * @static
 ###
 Rest =
   ###*


Fix regarding libjass's handling of video size

--- scripts/players.coffee
+++ scripts/players.coffee
@@ -257,11 +257,10 @@
             if video.readyState >= HTMLMediaElement.HAVE_METADATA and ass
                 width = video.offsetWidth
                 height = video.offsetHeight
-                ass.dpi = 96
                 renderer = new libjass.renderers.DefaultRenderer(video, ass, {
-                    preLoadFonts: true,
                     fontMap: libjass.renderers.RendererSettings.makeFontMapFromStyleElement(style)
                 })
+                renderer.resizeVideo(width, height)

         if video.readyState < HTMLMediaElement.HAVE_METADATA
             video.addEventListener('loadedmetadata', () ->


action: Fix typo.

action: Fix typo.

--- Oghma/action.coffee
+++ Oghma/action.coffee
@@ -16,7 +16,7 @@

 # Oghma Action Environment
 #
-# This class will be extended by code in action/.  It provides "actions",
+# This class will be extended by code in action.  It provides "actions",
 # code that modifies the table, with this class serving as a holding location
 # and environment.
 #


Update zh-HANS.coffee (#5556)

Update zh-HANS.coffee (#5556)

Very tiny change, for GD level, might be used for hackthon

--- app/locale/zh-HANS.coco.coffee
+++ app/locale/zh-HANS.coco.coffee
@@ -551,7 +551,7 @@

   play_level:
     back_to_map: "返回地图"
-    directions: "方向"
+    directions: "游戏指引"
     edit_level: "编辑关卡"
     keep_learning: "不断学习"
     explore_codecombat: "探索CodeCombat"


Clip the screen position after translating it from a buffer position

This is instead of applying redundant clipping logic directly within the translation method.

--- src/app/line-map.coffee
+++ src/app/line-map.coffee
@@ -56,7 +56,7 @@
     else if column < 0
       column = 0

-    screenLine = @lineForScreenRow(row)
+    screenLine = options.screenLine ? @lineForScreenRow(row)
     maxScreenColumn = screenLine.getMaxScreenColumn()

     if screenLine.isSoftWrapped() and column >= maxScreenColumn
@@ -73,35 +73,21 @@
     new Point(row, column)

   screenPositionForBufferPosition: (bufferPosition, options={}) ->
-    { wrapBeyondNewlines, wrapAtSoftNewlines } = options
     { row, column } = Point.fromObject(bufferPosition)
-
     [screenRow, screenLines] = @screenRowAndScreenLinesForBufferRow(row)
-
     for screenLine in screenLines
       maxBufferColumn = screenLine.getMaxBufferColumn()
-      if screenLine.isSoftWrapped()
+      if screenLine.isSoftWrapped() and column > maxBufferColumn
+        screenRow++
+      else
         if column <= maxBufferColumn
-          if column == maxBufferColumn
-            if wrapAtSoftNewlines
-              screenRow++
-              screenColumn = 0
-            else
-              screenColumn = screenLine.screenColumnForBufferColumn(column - 1, options)
-          else
-            screenColumn = screenLine.screenColumnForBufferColumn(column, options)
-          break
+          screenColumn = screenLine.screenColumnForBufferColumn(column)
         else
-          screenRow++
-      else
-        if wrapBeyondNewlines and column > maxBufferColumn and screenRow < @lastScreenRow()
-          screenRow++
-          screenColumn = 0
-        else
-          screenColumn = screenLine.screenColumnForBufferColumn(column)
+          screenColumn = Infinity
         break

-    new Point(screenRow, screenColumn)
+    options.screenLine = screenLine
+    @clipScreenPosition([screenRow, screenColumn], options)

   screenRowAndScreenLinesForBufferRow: (bufferRow) ->
     screenLines = []


updating tutor datepicker to have a click handler and parse function for invalid dates

--- src/components/tutor-input.cjsx
+++ src/components/tutor-input.cjsx
@@ -37,27 +37,49 @@
   expandCalendar: ->
     @setState({expandCalendar: true})

+  isValid: (value) ->
+    valid = true
+    valid = false if (@props.min and value < @props.min)
+    valid = false if (@props.max and value > @props.max)
+    valid
+
   dateSelected: (value) ->
-    @setState({expandCalendar: false})
-    @props.onChange(value)
+    valid = @isValid(value)
+    @props.onChange(value) if (valid)
+    @setState({expandCalendar: false, valid: valid})

   onToggle: (open) ->
-    @setState({expandCalendar: false})
-    console.log(open)
+    @setState({expandCalendar: open})
+
+  clickHandler: (event) ->
+    if (event.target.tagName is "INPUT" and not @state.expandCalendar)
+      @setState({expandCalendar: true})
+
+  parseDate: (dateStr) ->
+    date = new Date(dateStr)
+    min = @props.min or ""
+    if (not date or not @isValid(date))
+      date = new Date(min)
+
+    date

   render: ->
     classes = ['form-control']
     unless @props.value then classes.push('empty')
     open = false

-    if @state.expandCalendar and not @props.readOnly
+    if @state.expandCalendar and not @props.readOnly and not @state.justToggled
       open = 'calendar'
       onToggle = @onToggle

+    value = @props.value
+

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

{@props.label}

-      <DateTimePicker onFocus={@expandCalendar}
+      <DateTimePicker onClick={@clickHandler}
+        onFocus={@expandCalendar}
+        parse={@parseDate}
         id={@props.id}
         format='MMM dd, yyyy'
         time={false}
@@ -68,7 +90,7 @@
         onChange={@dateSelected}
         readOnly={@props.readOnly}
         min={@props.min}
-        value={@props.value}
+        value={value}
       />


weather script

weather script

--- scripts/weather.coffee
+++ scripts/weather.coffee
@@ -37,9 +37,10 @@
             dailyData = data.daily.data[0]
             maxTemp = dailyData.temperatureMax
             minTemp = dailyData.temperatureMin
-            msg.send('Current Temp: ' + currentTemp + '\n' +
-                    'Max Temp:      ' + maxTemp + '\n' +
-                    'Min Temp:      ' + minTemp )
+            deg = '°F'
+            msg.send('Current Temp: ' + currentTemp + deg  + '\n' +
+                    'Max Temp:         ' + maxTemp + deg + '\n' +
+                    'Min Temp:         ' + minTemp + deg )
           )
           .catch( (err) ->
             msg.send('Error in forecast call.\n' + err)


Using vanilla foundation

Using vanilla foundation

--- app/assets/javascripts/darkswarm/all.js.coffee
+++ app/assets/javascripts/darkswarm/all.js.coffee
@@ -36,4 +36,4 @@
 $ ->
   # Hacky fix for issue - http://foundation.zurb.com/forum/posts/2112-foundation-5100-syntax-error-in-js
   Foundation.set_namespace ""
-  #$(document).foundation()
+  $(document).foundation()


added additional manifest description to how-to

--- app/pages/lab/how-to-page.cjsx
+++ app/pages/lab/how-to-page.cjsx
@@ -77,7 +77,7 @@

           And here are some examples of not-cats (you can ignore these):

-          ![A sample of things that are not cats.](http://zooniverse-resources.s3.amazonaws.com/bigblogfiles/cat_demo/notcats.png)"
+          ![A sample of things that are not cats.](http://zooniverse-resources.s3.amazonaws.com/bigblogfiles/cat_demo/notcats.png)

       We can set subsequent tasks to depend on the answer to this question. Right now we haven't made any other tasks,
       so the only option is "End of Classification." Once we create more tasks, we'll go back through and link them.
@@ -144,10 +144,11 @@
       Get started uploading a set of subjects for your project by clicking on the "New Subject Set" button on
       the left hand side of the screen. That will bring you to the _Subject Uploader_.

-      You'll need to have all of your images in a single folder along with a manifest. The manifest file lists
-      all the images we want to upload and links each image to additional information, also called metadata,
-      such as date, time, and photographer. Read more about the manifest in the subsequent "Details" sections,
-      but for now we've done this for you in the ["Kitteh" zip file](https://data.zooniverse.org/tutorial/kitteh_zoo.zip).
+      It's easiest if you have all of your images in a single folder along with a manifest file, and you will
+      upload both at the same time. The manifest file lists all the images we want to upload and links each
+      image to additional information, also called metadata, such as date, time, and photographer. There is more
+      information about the manifest in the "Details" section below, but for now you can see an example manifest
+      file in the ["Kitteh" zip file](https://data.zooniverse.org/tutorial/kitteh_zoo.zip).

       Click on the "Upload Subjects" box and navigate to the "Kitteh" folder you downloaded. The easiest thing
       to do is simply hit _cmd + a_ (on Mac, or _ctrl + a_ on Windows) to select everything in that folder.
@@ -255,13 +256,50 @@

       _Translator_: Translators will have access to the project builder as well as the translation site, so they can translate all of your project text into a different language.

-      ### DETAILS - Subject sets and manifest details:
-
-      The manifest should have one row per subject, with columns for _SubjectID_, _FileName_, and whatever other metadata you would like to include.
-
-      If you want to make subjects with multiple images, then create a column for each filename (_Filename1_, _Filename2_, etc.). Note that drawing tasks do not work on multi-image subjects.
-
-      We recommend that you create a unique identifier per subject before uploading to the Zooniverse, so that you have a secure way of linking the data we provide back to the rest of your metadata. Note that all filenames for subjects you upload will need to be unique!
+      ### DETAILS - Subject sets and manifest details, a.k.a. "What is a manifest?"
+
+      *The condensed answer:*
+
+      A manifest is a file that tells our software how to combine the images you have into units of data (subjects) to be classified. The manifest also allows you to link your classifications back to the rest of your data. A manifest is formatted as a CSV file with 1 line per subject, with a unique identifier and the names of images to be associated with a subject on each row (with additional information often included in other fields as well). There is an example in the "Kitteh" zip file.
+
+      *The full answer:*
+
+      What we call a "manifest" is really just a plain text file with a specific format to each line.
+
+      To understand the format, let's start with the first few lines from the Kitteh Zoo manifest:
+
+          subject_id,image_name_1,origin,link,attribution,license,#secret_description
+          1,6672150457_420d61007d_b.jpg,Flickr,https://www.flickr.com/photos/aigle_dore/6672150457,Moyan Brenn,Creative Commons - share adapt attribute,sleepy striped kitteh is unsuspecting of paparazzi
+          2,8300920648_d4a21bba59_z,Flickr,https://www.flickr.com/photos/aigle_dore/8300920648,Moyan Brenn,Creative Commons - share adapt attribute,grandfather kitteh has ear hair. a lot of it
+          3,6713782851_82fc8c73e5_z.jpg,Flickr,https://www.flickr.com/photos/hellie55/6713782851,hehaden,Creative Commons - share adapt attribute,juvenile kittehs practice break-in at the catnip factory
+
+      The first line of the file is a header line that specifies the name of each of the manifest fields. In this case, our manifest has 7 fields (or columns), called "subject_id", "image_name", "origin", "link", "attribution", "license" and “#secret_description”. They are separated by commas: this is what's known as a "comma separated values" file, or CSV file.
+
+      After the first line, each row of the file contains information about 1 subject. The first field, corresponding with the "subject_id" header, is a unique number that identifies the subject. The second field, which aligns with the "image_name" header, contains the name of the image that's associated with that subject. These 2 fields are critically important: the image name is obviously important, and a unique identifier is important for matching your classifications to the rest of your data.
+
+      All the other fields are optional, but in general having more information in the manifest is better. Most projects include additional information in the manifest that helps them match the classifications and subjects to the other data they need for their research. The additional information in the manifest can also be made available to volunteers as they classify (some very keen volunteers find this extremely useful). Any fields with names that begin with “#” or “//” will *not* be shown to volunteers, such as the “#secret_description” field in Kitteh Zoo. These hidden fields will still be returned to you in the classification file, so you can use these to include information helpful to your research without worrying about whether it might affect the classifications themselves. Information in fields that *don’t* begin with either “#” or “//” will be accessible to volunteers.
+
+      For now, let’s assume you’re just including the bare minimum of information, like:
+
+          id,image
+          1,kitteh_in_box.jpg
+          2,kitteh_stalking.jpg
+          3,kitteh_losing_balance.jpg
+
+      Note the field names have changed from the previous example. That's because, aside from marking whether a field is hidden or not, it doesn't actually matter to the Zooniverse what the fields are called (or what order they’re in), so you can name and order them according to whatever works best for your project.
+
+      Using a manifest CSV file also makes it very easy to create subjects with multiple images:
+
+          id,image1,image2
+          1,kitteh_in_box.jpg,kitteh_eating_box.jpg
+          2,kitteh_stalking.jpg,kitteh_pounced.jpg
+          3,kitteh_losing_balance.jpg,kitteh_falling_off_sofa.jpg
+
+      If you upload this manifest plus the 6 images named in it, the Zooniverse software will create 3 subjects with 2 images each. When these subjects come up in the classification interface, volunteers will be able to flash between the images or switch between them manually.
+
+      You can create a manifest file in a simple text editor (such as TextEdit or Notepad), although this method is prone to errors like missed or extra commas. People often find it easiest to create manifest files using spreadsheet software such as Google Sheets, iWork Numbers or Microsoft Excel. Creating and maintaining a manifest using a spreadsheet makes the manifest easy to read, and you can export it to CSV format when you're ready to upload your subjects. You can also open existing CSV files in spreadsheet software.
+
+      *Note:* if you have a large subject set it may be cumbersome to manually create a manifest. We suggest using a command-line or other tool to copy-paste a directory list of files into a spreadsheet to help you get started.
     '''

 module.exports = React.createClass


Sendgrid email validation user schema (#457)

--- app/schemas/models/user.coffee
+++ app/schemas/models/user.coffee
@@ -111,6 +111,12 @@
         type: c.shortString() # E.g 'share progress modal parent'
         email: c.shortString()
         sent: c.date() # Set when sent
+
+    validations: c.array, { title: 'Sendgrid email validation results' },
+      c.object {},
+        validationDate: c.date()
+        result: c.object({ additionalProperties: true })
+
   unsubscribedFromMarketingEmails: { type: 'boolean' }

   consentHistory: c.array {title: 'History of consent actions'},


add prefilled choropleths to resize logic

--- app/scripts/services/livingdocs_service.coffee
+++ app/scripts/services/livingdocs_service.coffee
@@ -43,7 +43,7 @@
         angularTemplateService.insertAngularTemplate(snippet) if angularTemplateService.isAngularTemplate(snippet)

       doc.snippetWasDropped (snippet) ->
-        if snippet.identifier == 'livingmaps.choropleth'
+        if snippet.identifier == 'livingmaps.choropleth' || choroplethDataService.isPrefilledChoropleth(snippet)
           snippet.data('lastPositioned', (new Date()).toJSON())
           snippet.data('lastChangeTime', (new Date()).toJSON())


removed useless eyes require

removed useless eyes require

--- server/controllers/album.coffee
+++ server/controllers/album.coffee
@@ -1,7 +1,6 @@
 Album = require '../models/album'
 Photo = require '../models/photo'
 async = require 'async'
-eyes = require 'eyes'

 module.exports =


Reduce logs from successful translations.

--- server/translate-helpers.coffee
+++ server/translate-helpers.coffee
@@ -42,8 +42,6 @@
             (err, res) ->
                 translatedMsg = 'Google translation failed.'
                 if not err
-                    console.log('Google translate API response:')
-                    console.log(JSON.stringify(res, null, 2))
                     try
                         translatedMsg = res.data.data
                                            .translations[0].translatedText
@@ -52,6 +50,9 @@
                             'Google translation failed with exception:',
                             except
                         )
+                else
+                    console.log('Google translate API response:')
+                    console.log(JSON.stringify(res, null, 2))

                 cb(err, translatedMsg)
         )
@@ -86,9 +87,6 @@
             (err, res) ->
                 translatedMsg = 'Bing translation failed.'
                 if not err
-                    console.log('Bing translate API response:')
-                    console.log(JSON.stringify(res, null, 2))
-
                     # Data format:
                     #  {
                     #  ...
@@ -107,6 +105,9 @@
                             'Bing translation failed with exception:',
                             except
                         )
+                else
+                    console.log('Bing translate API response:')
+                    console.log(JSON.stringify(res, null, 2))

                 cb(err, translatedMsg)
         )


fix fair template

fix fair template

--- apps/fair/test/templates.coffee
+++ apps/fair/test/templates.coffee
@@ -1,4 +1,5 @@
 _ = require 'underscore'
+_s = require 'underscore.string'
 jade = require 'jade'
 path = require 'path'
 fs = require 'fs'
@@ -47,6 +48,7 @@
         sd: sd
         fair: fair
         profile: profile
+        _s: _s
         asset: (->)
       @$template = cheerio.load template
       done()
@@ -75,6 +77,7 @@
       profile = new Profile sd.PROFILE
       template = render('index')
         sd: sd
+        _s: _s
         fair: fair
         profile: profile
         asset: (->)
@@ -133,6 +136,7 @@
       fairResults[0].updateForFair fair
       template = render('index')
         sd: sd
+        _s: _s
         fair: fair
         profile: profile
         results: results


Remove "articleCount" property from events

--- server/updaters.coffee
+++ server/updaters.coffee
@@ -28,6 +28,10 @@
       $addToSet: incidents: incidentData
     Incidents.update _id: incident._id,
       $unset: userEventId: ''
+
+  UserEvents.update {},
+    $unset: articleCount: ''
+    {multi: true}

   Incidents.find(disease: $exists: false).forEach (incident) ->
     disease = UserEvents.findOne(incident.userEventId)?.disease


Nodejs: metrics: replace invalid characters with underscore

--- servers/datadog.coffee
+++ servers/datadog.coffee
@@ -9,9 +9,10 @@
   @sanitizeMetricName : (string) ->

     return string
-      .replace /\/$/g,      'home'  # replace last / with home
-      .replace /[\/-]/g,    '.'     # replace all / with .
-      .replace /\.{2,}/g,   '.'     # remove adjacent dots
+      .replace /\/$/g,                 'home'  # replace last / with home
+      .replace /[\/-]/g,               '.'     # replace all / with .
+      .replace /\.{2,}/g,              '.'     # remove adjacent dots
+      .replace /[^a-zA-Z0-9_\.]/g,     '_'     # replace invalid chars with _

   @generateName : (req) ->


Making unused serverdatasources non-fatal

--- bokehjs/src/coffee/renderer/glyph/glyph.coffee
+++ bokehjs/src/coffee/renderer/glyph/glyph.coffee
@@ -56,11 +56,7 @@
            @plot_view.y_range,
            transform_params)
       else
-        throw new Error("Unkonwn resample op '#{resample_op}'")
-
-
-
-
+        console.log("Unkonwn resample op '#{resample_op}'")

     initialize: (options) ->
       super(options)


Disable Tests

Disable Tests

--- test/scripts/services/personSpec.coffee
+++ test/scripts/services/personSpec.coffee
@@ -9,6 +9,7 @@

 					{pass}

+	###
 	it 'should get people', inject ['$httpBackend', 'personService', ($httpBackend, personService) ->
 		expected = [{name: 'foo'}]
 		notExpected = [{name: 'bar'}]
@@ -59,4 +60,6 @@

 		$httpBackend.flush()
-	]+
+	]
+	###


Fix exception "cannot read property payload from undefined"

--- src/base/convert.coffee
+++ src/base/convert.coffee
@@ -10,8 +10,12 @@
 			else
 				messageToUse = messages
 			debug "message to use",messageToUse
-			result = messageToUse[0].payload.replace(new RegExp(" ", "g"), "")
-			callback(error, result)
+			if messages.length > 1
+				result = messageToUse[0].payload.replace(new RegExp(" ", "g"), "")
+				callback(error, result)
+			else
+				callback
+					msg: "No usable messages received, but no error returned."
 		else
 			callback(error)


Delete pattern-block added in 6705510

Delete pattern-block added in 6705510

In hindsight, this probably wasn't a good idea, since we don't know this
language well enough to justify introducing the visual discrepancy.

--- grammars/runoff.cson
+++ grammars/runoff.cson
@@ -100,18 +100,6 @@
 			patterns: [include: "#arguments"]
 		},{

-			# .var: Variable declaration
-			name: "meta.function.runoff"
-			begin: "(?i)(?:^|(?<=;))((\\.)VAR|VR|VARIABLE)(?=$|\\s)"
-			end: "$|;|(?=!)"
-			beginCaptures:
-				1: name: "storage.type.var.runoff"
-				2: name: "punctuation.definition.function.runoff"
-			endCaptures:
-				1: name: "punctuation.terminator.statement.runoff"
-			patterns: [include: "#arguments"]
-		},{
-
 			# "Comment" command (debugging feedback?)
 			name: "meta.function.debugging-comment.runoff"
 			begin: "(?i)((\\.)COMMENT)\\b"


added basic error detection

added basic error detection

--- grammars/schelp.cson
+++ grammars/schelp.cson
@@ -89,6 +89,18 @@
     'name': 'keyword.other.schelp'
   }

+
+
+  {
+    'match': '\\b(?i:KEYWORD):: (\\w+,\\s*)* \\w+'
+    'name': 'keyword.other.schelp'
+  }
+
+  {
+    'match': '\\b(?i:CLASSTREE)::.*$'
+    'name': 'keyword.other.schelp'
+  }
+
 #METHOD TAGS
 # inside the method definition, we can recognize other tags: argument, returns, discussion,
   {
@@ -98,7 +110,7 @@
             'name': 'storage.type.function.schelp'
         '2':
             'name': 'entity.name.function.schelp'
-    'end': '((\\bSECTION::|\\bDESCRIPTION::|\\bINSTANCEMETHODS::|\\bCLASSMETHODS::|\\bEXAMPLES::|\\bSUBSECTION::))'
+    'end': '((?i:(\\bSECTION::|\\bDESCRIPTION::|\\bINSTANCEMETHODS::|\\bCLASSMETHODS::|\\bEXAMPLES::|\\bSUBSECTION::)))'
     'endCaptures':
         '1':
             'name': 'keyword.other.schelp'
@@ -129,9 +141,6 @@
     ]
   }

-
-
-
 # NOTES, WARNINGS and FOOTNOTES
   {
     'contentName': 'markup.note'
@@ -186,11 +195,21 @@
         }
     ]
   }
+
+# Mark all tags that are not recognized as illegal
+  {
+    'match': '\\b\\w+::'
+    'name': 'invalid.illegal'
+  }
+# Mark hanging end tags
+  {
+    'match': '::'
+    'name': 'invalid.illegal'
+  }
+
 ]

-
-################ PYTHON STUFF TO REMOVE ###################


quota looks ugly for non-admin: fix

quota looks ugly for non-admin: fix

--- salvus/page/project_settings.cjsx
+++ salvus/page/project_settings.cjsx
@@ -289,9 +289,13 @@

     render_upgrades_button : ->
         if not @state.upgrading
-            <Button bsStyle='primary' style={float:'right'} onClick={@show_upgrade_quotas}>
-                <Icon name='arrow-circle-up' /> Adjust your quotas...
-            </Button>
+            <Row>
+                <Col sm=12>
+                    <Button bsStyle='primary' style={float:'right'} onClick={@show_upgrade_quotas}>
+                        <Icon name='arrow-circle-up' /> Adjust your quotas...
+                    </Button>
+                </Col>
+            </Row>

     # the max button will set the upgrade input box to the number given as max
     render_max_button : (name, max) ->


Return pull request created string to oneline

--- src/bitbucket-pr.coffee
+++ src/bitbucket-pr.coffee
@@ -161,10 +161,7 @@
         }
         {
           title: @repo_name
-          value: """
-            Merge to #{@destination_branch}
-            #{@pr_link}
-          """
+          value: "Merge to #{@destination_branch}\n#{@pr_link}"
           short: true
         }
       ]


[atom] dictionary update

[atom] dictionary update

--- .atom/config.cson
+++ .atom/config.cson
@@ -58,9 +58,82 @@
       "ns"
     ]
     strict: false
+  parinfer:
+    "file-extensions": [
+      ".boot"
+      ".cl"
+      ".clj"
+      ".cljc"
+      ".cljs"
+      ".edn"
+      ".el"
+      ".lfe"
+      ".lisp"
+      ".lsp"
+      ".rkt"
+      ".scm"
+    ]
   "proto-repl":
     autoPrettyPrint: true
     leinPath: "/home/rd905075/.dotfiles/bin/lein"
+  "spell-check":
+    addKnownWords: true
+    knownWords: [
+      "multiclass"
+      "attunement"
+      "mithral"
+      "melee"
+      "dwarves"
+      "cantrip"
+      "multiclassing"
+      "undead"
+      "mooks"
+      "machina"
+      "debuffing"
+      "debuff"
+      "Pelor"
+      "spellcasting"
+      "anydice"
+      "MMORPGs"
+      "roleplaying"
+      "MMOs"
+      "sourcebooks"
+      "spell's"
+      "cantrips"
+      "goodberries"
+      "goodberry"
+      "darkvision"
+      "AoE"
+      "spellcaster"
+      "blasty"
+      "Thunderwave"
+      "Polearm"
+      "stormborn"
+      "dragonborn"
+      "subrace"
+      "halflings"
+      "spellcasters"
+      "Dwarven"
+      "subraces"
+      "pre"
+      "drow"
+      "drows"
+      "halfling"
+      "lightfoot"
+      "sourcebook"
+      "dragonborns"
+      "orc"
+      "orcs"
+      "tiefling"
+      "Thaumaturgy"
+      "mage"
+      "polearm"
+      "battleaxe"
+      "eldritch"
+      "thaumaturgy"
+      "scrying"
+      "antimagic"
+    ]
   "tool-bar":
     visible: false
   "tree-view":


Adjusts the terminal colors.

Adjusts the terminal colors.

--- .atom/config.cson
+++ .atom/config.cson
@@ -3,6 +3,25 @@
     autoBuildTagsWhenActive: true
     cmdArgs: "--exclude=core --exclude=deploy --exclude=dist --exclude=gulp --exclude=node_modules --exclude=sql --exclude=tests --exclude=vendor"
   "atom-ide-ui":
+    "atom-ide-diagnostics-ui":
+      autoVisibility: true
+    "atom-ide-terminal":
+      black: "#3f3f3f"
+      red: "#705050"
+      green: "#60b48a"
+      yellow: "#dfaf8f"
+      blue: "#506070"
+      magenta: "#dc8cc3"
+      cyan: "#8cd0d3"
+      white: "#dcdccc"
+      brightBlack: "#709080"
+      brightRed: "#dca3a3"
+      brightGreen: "#c6dda7"
+      brightYellow: "#f0dfaf"
+      brightBlue: "#94bff3"
+      brightMagenta: "#ec93d3"
+      brightCyan: "#93e0e3"
+      brightWhite: "#ffffff"
     use:
       "atom-ide-debugger": "default"
       "atom-ide-terminal": "default"


May no longer need event-watch.

May no longer need event-watch.

--- .atom/config.cson
+++ .atom/config.cson
@@ -48,6 +48,7 @@
       "update-package-dependencies"
       "welcome"
       "wrap-guide"
+      "event-watch"
     ]
     openEmptyEditorOnStart: false
     packagesWithKeymapsDisabled: [
@@ -85,26 +86,6 @@
     displayColorWarningTooltip: "IndianRed"
     displayFontWeightStatusbar: "400"
     displayFontWeightTooltip: "400"
-    schedules:
-      N: '''
-        at 7:03am and 7:34am and 8:05am and 8:41am and 9:15am and 9:44am and
-        10:15am and 11:15am and 12:15am and  1:15pm and 2:15pm and 3:15pm
-        and 3:44pm and 4:19pm and 4:55pm and 5:27pm and 5:57pm and 6:30pm on
-        Mon,Tue,Wed,Thu,Fri also at 7:30pm and 8:30pm and 9:30pm and 10:30pm
-        and 11:30pm on Fri also at 12:30am on Sat also at 4:41pm and 5:15pm
-        and 5:15pm and 5:49pm and 6:23pm and 6:57pm and 7:31pm and 8:05pm
-        and 8:39pm and 9:13pm and 9:47pm and 10:21pm and 10:55pm and 11:29pm
-        on Sat also at 12:03am on Sun
-      '''
-      S: '''
-        at 6:24am and 6:56am and 7:27am and 7:58am and 8:34am and 9:09am and
-        9:38am and 10:38am and 11:38am and 12:38am and 1:38pm and 2:38pm and
-        3:09pm and 3:38pm and 4:13pm and 4:43pm and 5:19pm and 5:51pm on
-        Mon,Tue,Wed,Thu,Fri also at 6:53pm and 7:53pm and 8:24pm and 9:24pm
-        and 10:24pm and 11:24pm on Fri also at 4:00pm and 4:34pm and 5:08pm
-        and 5:42pm and 6:16pm and 6:50pm and 7:24pm and 7:58pm and 8:32pm
-        and 9:06pm and 9:40pm and 10:14pm and 10:48pm and 11:22pm on Sat
-      '''
     tooltipFontWeight: "700"
     warnThresholdMinutes: 30
   "format-shell":


ミリ秒を時分秒に変換するメソッドを追加

ミリ秒を時分秒に変換するメソッドを追加

--- src/coffee/Utils.coffee
+++ src/coffee/Utils.coffee
@@ -19,6 +19,12 @@
 			_isIOs = true
 		return _isIOs

+	@computeDuration: (ms) ->
+		h = String(Math.floor(ms / 3600000) + 100).substring(1)
+		m = String(Math.floor((ms - h * 3600000)/60000)+ 100).substring(1)
+		s = String(Math.round((ms - h * 3600000 - m * 60000)/1000)+ 100).substring(1)
+		return h + ":" + m + ":" + s
+
 	@getUrlParams: () ->
 		pl     = /\+/g
 		search = /([^&=]+)=?([^&]*)/g


Prevent doubling up of `<body>` tag.

Prevent doubling up of `<body>` tag.

Documents with images caused updates to the model, using XMLSerializer.
Unfortunately I serialized the body container along with all the content
causing the body tag to double up.

--- scripts/gh-book/xhtml-file.coffee
+++ scripts/gh-book/xhtml-file.coffee
@@ -174,8 +174,7 @@

             # $img.attr('data-src', attrs.id) TODO: Aloha keeps stripping this attribute off.

-          @set 'body',
-            (new XMLSerializer).serializeToString($html.find('body')[0]),
+          @set 'body', $html.find('body')[0].innerHTML?.trim(),
             imageupdate: true

     # Since the titles are purely cosmetic do not mark the model as dirty
@@ -253,7 +252,7 @@
           # Set `parse:true` so the dirty flag for saving is not set
           if counter == 0
             @set 'body',
-              (new XMLSerializer).serializeToString($html.find('body')[0]),
+              $html.find('body')[0].innerHTML?.trim(),
               parse:true, loading:true
           deferred.resolve()
           return
@@ -270,8 +269,7 @@
           counter--
           # Set `parse:true` so the dirty flag for saving is not set
           if counter == 0
-            @set 'body',
-              (new XMLSerializer).serializeToString($html.find('body')[0]),
+            @set 'body', $html.find('body')[0].innerHTML?.trim(),
               parse:true, loading:true
           deferred.resolve()
         .fail ->


Add missing DisplayMarkerLayer.prototype.onDidDestroy

--- src/display-marker-layer.coffee
+++ src/display-marker-layer.coffee
@@ -10,9 +10,13 @@
     @markersById = {}
     @emitter = new Emitter
     @bufferMarkerLayer.onDidUpdate(@emitDidUpdate.bind(this))
+    @bufferMarkerLayer.onDidDestroy(@emitDidDestroy.bind(this))

   destroy: ->
     @bufferMarkerLayer.destroy()
+
+  onDidDestroy: (callback) ->
+    @emitter.on('did-destroy', callback)

   onDidUpdate: (callback) ->
     @emitter.on('did-update', callback)
@@ -75,6 +79,9 @@
   emitDidUpdate: ->
     @emitter.emit('did-update')

+  emitDidDestroy: ->
+    @emitter.emit('did-destroy')
+
   translateToBufferMarkerLayerFindParams: (params) ->
     bufferMarkerLayerFindParams = {}
     for key, value of params


no more knockout for config diagnostics

no more knockout for config diagnostics

--- js/inner/config_diagnostic.coffee
+++ js/inner/config_diagnostic.coffee
@@ -1,10 +1,7 @@
 # The collection of errors that will be displayed.
-CI.inner.Diagnostics = class Diagnostics extends CI.inner.Obj
-  observables: =>
-    selected: 0
-
+CI.inner.Diagnostics = class Diagnostics
   constructor: (@config, @errors, properties={}) ->
-    super properties
+    @selected = 0
     # Create an annotated representation of the configuration.
     @ann = new CI.inner.Annotated @config
     # Keep track of the first and the last lines that are parts of errors.
@@ -58,11 +55,11 @@
       ix >= @first - 3 and ix <= @last + 3

 # An individual error.
-CI.inner.ConfigError = class ConfigError extends CI.inner.Obj
+CI.inner.ConfigError = class ConfigError
   constructor: (error, @index, @diagnostics) ->
-    super error
-    # Whether this error is currently selected / expanded.
-    @selected = @komp => @diagnostics.selected() == @index
+    for k,v of error
+      @[k] = v
+
     # Render the path a la JS field / array member accessing.
     @path = @path?.map (part, index) =>
       if typeof part == 'string'
@@ -82,14 +79,18 @@
     @flag =
       error_flag: true
       number: @index
-      selected: @selected
+      get_selected: @get_selected
       select: @select

+  # Whether this error is currently selected / expanded.
+  get_selected: () =>
+    @diagnostics.selected is @index
+
   select: () =>
-    @diagnostics.selected @index
+    @diagnostics.selected = @index

   select_next: () =>
-    @diagnostics.selected (@index + 1) % @diagnostics.errors.length
+    @diagnostics.selected = (@index + 1) % @diagnostics.errors.length

 # A data structure for annotating text. Handles overlapping annotations
 # automatically.


Extend ditroff(7) grammar with Heirloom extensions

The graphics-drawing command now also recognises the "rule" instruction,
supported by grodvi(1) and various Groff printer drivers. It essentially
draws a solid black rectangle.

--- grammars/output.cson
+++ grammars/output.cson
@@ -165,12 +165,12 @@
 			},{
 				# Set character height or slant
 				name: "meta.set-character-property.ditroff"
-				match: "\\G([HS]\\S*)(?:\\s+(-?\\d+)|\\s+(-23)\\s+(-?[\\d.]+))?\\s*?(?=$|#)"
-				captures:
-					1: name: "keyword.device.control.ditroff"
-					2: name: "constant.numeric.integer.ditroff"
-					3: name: "constant.numeric.ignored.ditroff"
-					4: name: "constant.numeric.float.ditroff"
+				match: "\\G([HS]\\S*)\\s+(?:(-23)\\s+(-?[\\d.]+)|(-?\\d+))?\\s*?(?=$|#)"
+				captures:
+					1: name: "keyword.device.control.ditroff"
+					2: name: "constant.numeric.ignored.ditroff"
+					3: name: "constant.numeric.float.ditroff"
+					4: name: "constant.numeric.integer.ditroff"
 			},{
 				# Unrecognised device control command
 				name: "meta.unknown-command.ditroff"
@@ -222,7 +222,7 @@
 		},{
 			# Everything else
 			name: "meta.graphics.ditroff"
-			begin: "^\\s*(D)\\s*([lceafptP~])"
+			begin: "^\\s*(D)\\s*([lceafptPR~])"
 			end:   "(?=$|#)"
 			patterns: [include: "text.roff#number"]
 			beginCaptures:
@@ -280,12 +280,13 @@
 		},{
 			# Print literal characters
 			name: "meta.print-character.ditroff"
-			match: "(c)\\s*(\\S)|(C)\\s*(\\S+)"
+			match: "(c)(?:\\s*(\\S)|(\\s))|(CPS|C)\\s*(\\S+)"
 			captures:
 				1: name: "entity.name.function.ditroff"
 				2: name: "constant.character.ditroff"
-				3: name: "entity.name.function.ditroff"
-				4: name: "string.unquoted.ditroff"
+				3: name: "constant.character.whitespace.ditroff"
+				4: name: "entity.name.function.ditroff"
+				5: name: "string.unquoted.ditroff"
 		},{
 			# Print sequence of named glyphs (GNU extension)
 			name: "meta.print-text.gnu.ditroff"
@@ -311,10 +312,12 @@

 	# Change point-size
 	size:
-		match: "(s)\\s*(\\d+)"
+		match: "(s)\\s*(?:(-23)\\s+(-?[\\d.]+)|(-?\\d+))"
 		captures:
 			1: name: "entity.name.function.ditroff"
-			2: name: "constant.numeric.integer.ditroff"
+			2: name: "constant.numeric.ignored.ditroff"
+			3: name: "constant.numeric.float.ditroff"
+			4: name: "constant.numeric.integer.ditroff"

 	# Paddable word-space: information only, no action


Updated name to be consistant with vscode sister

--- grammars/sshtml.cson
+++ grammars/sshtml.cson
@@ -1,5 +1,5 @@
 scopeName: 'text.html.ss'
-name: 'HTML (Silverstripe)'
+name: 'HTML Silverstripe'
 fileTypes: [
 	'ss'
 ]


get rid of a hanging console.log

get rid of a hanging console.log

--- spec/deploy-control-flow.coffee
+++ spec/deploy-control-flow.coffee
@@ -338,7 +338,6 @@
     , (err) =>
       throw err if err?
       @expects.should.have.length 0
-      console.log "done!"
       done()


Use invalid key-code to get correct behavior in test

--- spec/keymap-manager-spec.coffee
+++ spec/keymap-manager-spec.coffee
@@ -424,7 +424,7 @@

       it "always includes the shift modifier in the keystroke", ->
         expect(keymapManager.keystrokeForKeyboardEvent(keydownEvent('9', ctrl: true, shift: true))).toBe 'ctrl-('
-        expect(keymapManager.keystrokeForKeyboardEvent(keydownEvent('/', ctrl: true, shift: true))).toBe 'ctrl-?'
+        expect(keymapManager.keystrokeForKeyboardEvent(keydownEvent('U+00bf', ctrl: true, shift: true))).toBe 'ctrl-?'
         expect(keymapManager.keystrokeForKeyboardEvent(keydownEvent('a', ctrl: true, shift: true))).toBe 'ctrl-shift-A'

   describe "::findKeyBindings({command, target, keystrokes})", ->


Test that `onDidChangeActivePane()` works correctly

--- spec/pane-container-spec.coffee
+++ spec/pane-container-spec.coffee
@@ -118,6 +118,23 @@
       pane2.destroy()
       expect(container.getActivePane()).toBe pane1
       expect(pane1.isActive()).toBe true
+
+  describe "::onDidChangeActivePane()", ->
+    [container, pane1, pane2, observed] = []
+
+    beforeEach ->
+      container = new PaneContainer(params)
+      container.getRoot().addItems([new Object, new Object])
+      container.getRoot().splitRight(items: [new Object, new Object])
+      [pane1, pane2] = container.getPanes()
+
+      observed = []
+      container.onDidChangeActivePane (pane) -> observed.push(pane)
+
+    it "invokes observers when the active pane changes", ->
+      pane1.activate()
+      pane2.activate()
+      expect(observed).toEqual [pane1, pane2]

   describe "::onDidChangeActivePaneItem()", ->
     [container, pane1, pane2, observed] = []


Input: New opts.rows to limit textarea input.

Also: Allow opts.regexp + opts.prevent_invalid_input.

--- src/elements/Input/Input.coffee
+++ src/elements/Input/Input.coffee
@@ -23,6 +23,7 @@

 		if @_checkInput
 			@addClass("cui-input-has-check-input")
+			@__checkInput = @_checkInput

 		if @_prevent_invalid_input
 			@addClass("cui-input-has-prevent-invalid-input")
@@ -113,6 +114,10 @@
 				check: Boolean
 			textarea:
 				check: Boolean
+			# limit the amount of rows in textarea input
+			rows:
+				check: (v) ->
+					v >= 1
 			content_size:
 				default: false
 				check: Boolean
@@ -124,9 +129,6 @@
 				check: Boolean

 	readOpts: ->
-		if not isEmpty(@opts.regexp)
-			assert(not (@opts.checkInput), "new Input", "opts.regexp conflicts with opts.checkInput.")
-			assert(not @opts.hasOwnProperty("prevent_invalid_input"), "new Input", "opts.prevent_invalid_input conflicts with opts.regexp.")

 		if @opts.readonly
 			assert(not (@opts.getCursorBlocks or @opts.getInputBlocks or @opts.checkInput), "new Input", "opts.readonly conflicts with opts.getCursorBlocks, opts.getInputBlocks, opts.checkInput.")
@@ -143,17 +145,31 @@

 		if @_regexp
 			@__regexp = new RegExp(@_regexp, @_regexp_flags)
-			@_prevent_invalid_input = false
-			@_checkInput = @__checkInputRegexp
-
-		if @_required and not @_checkInput
-			@_checkInput = (opts) =>
-				opts.value.trim().lengt > 0
+			# @_prevent_invalid_input = false
+			@__checkInput = (value) =>
+				if not @__checkInputRegexp(value)
+					false
+				else if @_checkInput
+					@_checkInput(value)
+				else
+					true
+
+		if @_required
+			@__checkInput = (value) =>
+				if value.trim().length == 0
+					false
+				else if @_checkInput
+					@_checkInput(value)
+				else
+					true

 		if @_spellcheck == false
 			@__spellcheck = "false"
 		else
 			@__spellcheck = "default"
+
+		if @_rows
+			assert(@_content_size, "new Input", "opts.rows can only be used with opts.content_size set.", opts: @opts)

 		if @_autocomplete == true
 			@__autocomplete = "on"
@@ -783,7 +799,7 @@
 		@

 	preventInvalidInput: ->
-		if @_checkInput and @_prevent_invalid_input
+		if @__checkInput and @_prevent_invalid_input
 			true
 		else
 			false
@@ -839,6 +855,9 @@
 	__shadowInput: (ev) ->
 		shadow_v = @__shadow0.value

+		if @_rows and shadow_v.split("\n").length > @_rows
+			return
+
 		if @preventInvalidInput() and shadow_v.length > 0
 			ret = @checkInput(@correctValueForInput(shadow_v))
 			# console.debug "checking shadow input", ret, shadow_v
@@ -932,8 +951,8 @@
 		state

 	__checkInputInternal: (value = @__input0.value) ->
-		if @_checkInput
-			@_checkInput(value)
+		if @__checkInput
+			@__checkInput(value)
 		else
 			true


:memo: Add backticks around keybindings

:memo: Add backticks around keybindings

--- dot-atom/keymap.cson
+++ dot-atom/keymap.cson
@@ -22,7 +22,7 @@
 # * https://atom.io/docs/latest/behind-atom-keymaps-in-depth
 #
 # If you're having trouble with your keybindings not working, try the
-# Keybinding Resolver: Cmd+. on OS X and Ctrl+. on other platforms. See the
+# Keybinding Resolver: `Cmd+.` on OS X and `Ctrl+.` on other platforms. See the
 # Debugging Guide for more information:
 # * https://atom.io/docs/latest/hacking-atom-debugging#check-the-keybindings
 #


Suggest people bind to .workspace instead of body

--- dot-atom/keymap.cson
+++ dot-atom/keymap.cson
@@ -12,7 +12,7 @@
 # '.editor':
 #   'enter': 'editor:newline'
 #
-# 'body':
+# '.workspace':
 #   'ctrl-P': 'core:move-up'
 #   'ctrl-p': 'core:move-down'
 #


added logging

added logging

--- server/collection-security.coffee
+++ server/collection-security.coffee
@@ -1,4 +1,8 @@
 class CollectionSecurity
+  setLogging: (value) ->
+    @_logging = value
+  @_log: (msg, level = 'debug') ->
+    console[level] msg if @_logging or level is 'error'
   @_utilities:
     any: (array, value) ->
       return true for element in array when element is value


Tests: Add failing test for source mount removal (#77)

--- test/api/source_mount.coffee
+++ test/api/source_mount.coffee
@@ -87,4 +87,15 @@
                 expect(res.statusCode).to.be.eql 200
                 expect(json).to.contain.all.keys mount_config

-                done()+                done()
+
+    it "allows source mount to be removed via DELETE /sources/:source", (done) ->
+        request.del url:"#{m.api_uri}/sources/#{mount_config.key}", (err,res) ->
+            throw err if err
+
+            expect(res.statusCode).to.be.eql 200
+
+            # master should no longer know our source
+            expect(m.master.master.source_mounts).to.not.have.key mount_config.key
+
+            done()


rpc://user.login wasn't doing redaction before sending the user over the wire

--- server/packages/user/login.coffee
+++ server/packages/user/login.coffee
@@ -12,7 +12,7 @@
 				return fn new LoginError() unless user

 				req.login user, (error) ->
-					return fn attempted: error.message if error?
-					fn null, user
+					return fn error if error?
+					user.redactFor(user).nodeify fn

 			) req, res = {}


JPasswordRecovery: Filter all sharedEvents

--- workers/social/lib/social/models/passwordrecovery.coffee
+++ workers/social/lib/social/models/passwordrecovery.coffee
@@ -14,7 +14,10 @@
   @share()

   @set
-    sharedMethods:
+    sharedEvents  :
+      static      : []
+      instance    : []
+    sharedMethods :
       static:
         validate:
           (signature String, Function)


fix potential bug in elasticsearch

fix potential bug in elasticsearch

--- elasticsearch.coffee
+++ elasticsearch.coffee
@@ -108,7 +108,7 @@
           #console.log gist.id, fileName
           return fileCb()
     else
-      return fileCb()
+      return process.nextTick(fileCb)
   , () ->
     if Object.keys(gapiHash).length > 0
       gist.api = gapiHash


:white_check_mark: Add find-and-replace test.

- Find and replace clears jumpy.
- Fixed the fuzzy find test to mirror the find and replace one.

--- spec/jumpy-spec.coffee
+++ spec/jumpy-spec.coffee
@@ -320,15 +320,18 @@
                 expect(labels.length)
                     .toBe (expectedTotalNumberWith2TabsOpenInOnePane)

-    # TODO: find-and-replace test doesn't work for some reason.
-    xdescribe "when a jump mode is enabled", ->
+    describe "when a jump mode is enabled", ->
+        activationPromise = []
+        beforeEach ->
+            activationPromise = atom.packages.activatePackage('find-and-replace')
+
         it "clears when a find-and-replace mini pane is opened", ->
+            atom.commands.dispatch textEditorElement, 'find-and-replace:show'
+
             waitsForPromise ->
-                atom.packages.activatePackage 'find-and-replace'
+                activationPromise

             runs ->
-                atom.commands.dispatch textEditorElement,
-                    'find-and-replace:show'
                 expect(textEditorElement
                     .classList.contains('jumpy-jump-mode')).toBe false
                 expect(textEditorElement
@@ -337,9 +340,16 @@
                     .querySelectorAll('.find-and-replace')).toHaveLength 1

     describe "when a jump mode is enabled", ->
+        activationPromise = []
+        beforeEach ->
+            activationPromise = atom.packages.activatePackage('fuzzy-finder')
+
         it "clears when a fuzzy-finder mini pane is opened", ->
+            atom.commands.dispatch textEditorElement,
+                'fuzzy-finder:toggle-file-finder'
+
             waitsForPromise ->
-                atom.packages.activatePackage 'fuzzy-finder'
+                activationPromise

             runs ->
                 atom.commands.dispatch textEditorElement,


:white_check_mark: Added spec to Point class

Fixed a couple of functions to treat point-compatible array as a Point

--- spec/point-spec.coffee
+++ spec/point-spec.coffee
@@ -7,3 +7,70 @@
       expect(new Point( 1,  2).negate().toString()).toBe "(-1, -2)"
       expect(new Point(-1, -2).negate().toString()).toBe "(1, 2)"
       expect(new Point(-1,  2).negate().toString()).toBe "(1, -2)"
+
+  describe "::fromObject(object, copy)", ->
+    it "returns a new Point if object is point-compatible array ", ->
+      expect(Point.fromObject([1, 3]).isEqual(Point(1, 3)))
+      expect(Point.fromObject([Infinity, Infinity]).isEqual(Point.infinity()))
+
+    it "returns the copy of object if it is an instanceof Point", ->
+      origin = Point(0, 0)
+      expect(Point.fromObject(origin, false) is origin).toBe true
+      expect(Point.fromObject(origin, true) is origin).toBe false
+
+  describe "::isEqual()", ->
+    it "returns if whether two points are equal", ->
+      expect(Point(1, 1).isEqual(Point(1, 1))).toBe true
+      expect(Point(1, 1).isEqual([1, 1])).toBe true
+      expect(Point(1, 2).isEqual(Point(3, 3))).toBe false
+      expect(Point(1, 2).isEqual([3, 3])).toBe false
+
+  describe "::isPositive()", ->
+    it "returns true if the point represents a forward traversal", ->
+      expect(Point(-1, -1).isPositive()).toBe false
+      expect(Point(-1, 0).isPositive()).toBe false
+      expect(Point(-1, Infinity).isPositive()).toBe false
+      expect(Point(0, 0).isPositive()).toBe false
+
+      expect(Point(0, 1).isPositive()).toBe true
+      expect(Point(5, 0).isPositive()).toBe true
+      expect(Point(5, -1).isPositive()).toBe true
+
+  describe "::isZero()", ->
+    it "returns true if the point is zero", ->
+      expect(Point(1, 1).isZero()).toBe false
+      expect(Point(0, 1).isZero()).toBe false
+      expect(Point(1, 0).isZero()).toBe false
+      expect(Point(0, 0).isZero()).toBe true
+
+  describe "::min(a, b)", ->
+    it "returns the minimum of two points", ->
+      expect(Point.min(Point(3, 4), Point(1, 1)).isEqual(Point(1, 1)))
+      expect(Point.min(Point(1, 2), Point(5, 6)).isEqual(Point(1, 2)))
+      expect(Point.min([3, 4], [1, 1]).isEqual([1, 1]))
+      expect(Point.min([1, 2], [5, 6]).isEqual([1, 2]))
+
+  describe "::max(a, b)", ->
+    it "returns the minimum of two points", ->
+      expect(Point.max(Point(3, 4), Point(1, 1)).isEqual(Point(3, 4)))
+      expect(Point.max(Point(1, 2), Point(5, 6)).isEqual(Point(5, 6)))
+      expect(Point.min([3, 4], [1, 1]).isEqual([3, 4]))
+      expect(Point.min([1, 2], [5, 6]).isEqual([5, 6]))
+
+  describe "::sanitizeNegatives()", ->
+    it "returns the point so that it has valid buffer coordinates", ->
+      expect(Point(-1, -1).sanitizeNegatives().isEqual(Point(0, 0)))
+      expect(Point(-1, 0).sanitizeNegatives().isEqual(Point(0, 0)))
+      expect(Point(-1, Infinity).sanitizeNegatives().isEqual(Point(0, 0)))
+
+      expect(Point(5, -1).sanitizeNegatives().isEqual(Point(5, 0)))
+      expect(Point(5, -Infinity).sanitizeNegatives().isEqual(Point(5, 0)))
+      expect(Point(5, 5).sanitizeNegatives().isEqual(Point(5, 5)))
+
+  describe "::traverse(delta)", ->
+    it "returns a new point by traversing given rows and columns", ->
+      expect(Point(2, 3).traverse(Point(0, 3)).isEqual(Point(2, 5)))
+      expect(Point(2, 3).traverse([0, 3]).isEqual([2, 5]))
+
+      expect(Point(1, 3).traverse(Point(4, 2)).isEqual([5, 2]))
+      expect(Point(1, 3).traverse([5, 4]).isEqual([6, 4]))


enable a disabled test

enable a disabled test

--- spec/specs/main.coffee
+++ spec/specs/main.coffee
@@ -187,13 +187,9 @@
           -> Bacon.constant(-> "hello").map((f) -> f())
           ["hello"])
       describe "case 4", ->
-        # TODO: gets stuck in a loop, but this is nothing new: using Bacon.later instead of Bacon.once
-        # this gets stuck in master
-        ###
-        expectStreamEvents(
-          -> Bacon.constant(-> "hello").flatMap(Bacon.once)
+        expectStreamEvents(
+          -> Bacon.constant(-> "hello").flatMap(Bacon.once).map((f) -> f())
           ["hello"])
-        ###
     it "handles one subscriber added twice just like two separate subscribers (case Bacon.noMore)", ->
       values = []
       bus = new Bacon.Bus()


Mark responses errors as already being displayed

--- src/course/model.coffee
+++ src/course/model.coffee
@@ -84,11 +84,12 @@
       data: { id: @id, student_identifier: studentId}
     )

-  _onConfirmed:  ({data}) ->
-    if data.to
+  _onConfirmed:  (response) ->
+    if response.data?.to
       _.extend(@, data.to.course)
       @periods = [ data.to.period ]
-    @errors = data.errors
+    @errors = response.data.errors
+    response.stopErrorDisplay = true if @errors
     delete @status unless @hasErrors() # blank status indicates good to go
     @channel.emit('change')

@@ -101,10 +102,11 @@
       book_uuid: @ecosystem_book_uuid, enrollment_code: inviteCode
     })

-  _onRegistered: ({data}) ->
+  _onRegistered: (response) ->
     # confirmation has completed
-    _.extend(@, data)
-    @errors = data.errors
+    _.extend(@, response.data)
+    @errors = response.data.errors
+    response.stopErrorDisplay = true if @errors
     @channel.emit('change')


remove csrf here too

remove csrf here too

though I think the real problem is session management

--- helpers/index.coffee
+++ helpers/index.coffee
@@ -83,7 +83,8 @@
     req: (req, res) -> req

     _csrf: (req, res) ->
-      """<input type="hidden" name="_csrf" value="#{req.session._csrf}"/>"""
+      ""
+      # """<input type="hidden" name="_csrf" value="#{req.session._csrf}"/>"""

     title: (req, res) ->
       (title) ->


log exception

log exception

--- web/doppio-jvm/scripts/demo/node.coffee
+++ web/doppio-jvm/scripts/demo/node.coffee
@@ -91,7 +91,15 @@
         event.cancelBubble = true
       if (timeouts.length > 0)
         fn = timeouts.shift()
-        fn()
+        try
+            fn()
+        catch err
+             vDebug = "";
+             for prop in err
+                vDebug += "property: "+ prop+ " value: ["+ err[prop]+ "]\n";
+             vDebug += "toString(): " + " value: [" + err.toString() + "]";
+             console?.log vDebug
+

   if window.addEventListener
     # IE10 and all modern browsers


Parse on add button to add an else if

Parse on add button to add an else if

--- src/languages/javascript.coffee
+++ src/languages/javascript.coffee
@@ -659,8 +659,37 @@

 JavaScriptParser.handleButton = (text, button, oldBlock) ->
   if button is 'add-button' and 'IfStatement' in oldBlock.classes
-    return text + ''' else if (__) {
-      __
-    }'''
+    # Parse to find the last "else" or "else if"
+    node = acorn.parse(text, {
+      locations: true
+      line: 0
+      allowReturnOutsideFunction: true
+    }).body[0]
+    currentElif = node
+    elseLocation = null
+    while true
+      if currentElif.type is 'IfStatement'
+        if currentElif.alternate?
+          elseLocation = {
+            line: currentElif.alternate.loc.start.line
+            column: currentElif.alternate.loc.start.column
+          }
+          currentElif = currentElif.alternate
+        else
+          elseLocation = null
+          break
+      else
+        break
+
+    if elseLocation?
+      lines = text.split('\n')
+      elseLocation = lines[...elseLocation.line].join('\n').length + elseLocation.column
+      return text[...elseLocation].trimRight() + ' if (__) ' + text[elseLocation..].trimLeft() + ''' else {
+        __
+      }'''
+    else
+      return text + ''' else {
+        __
+      }'''

 module.exports = parser.wrapParser JavaScriptParser


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine_clear.coffee
+++ src/engine_clear.coffee
@@ -30,4 +30,3 @@

   # No public methods
   return {}
-


improve

improve

--- lib/blockwise-selection.coffee
+++ lib/blockwise-selection.coffee
@@ -20,8 +20,12 @@
       range = selection.getBufferRange()
       if range.end.column is 0
         range.end.row = range.end.row - 1
-      if @goalColumn? and not wasReversed
-        range.end.column = @goalColumn + 1
+
+      if @goalColumn?
+        if wasReversed
+          range.start.column = @goalColumn
+        else
+          range.end.column = @goalColumn + 1

       if range.start.column >= range.end.column
         reversed = not reversed
@@ -107,17 +111,18 @@
     @initialize(head)

   getBufferRange: ->
-    start = @getHead().getHeadBufferPosition()
-    end = @getTail().getTailBufferPosition()
+    head = @getHead().getHeadBufferPosition()
+    tail = @getTail().getTailBufferPosition()
+
     if @isReversed()
-      end.row += 1 if end.column is 0
+      tail.row += 1 if tail.column is 0
     else
-      start.row += 1 if start.column is 0
+      head.row += 1 if head.column is 0

     if @isSingleLine() or @headReversedStateIsInSync()
-      new Range(start, end)
+      new Range(head, tail)
     else
-      new Range(start, end).translate([0, -1], [0, +1])
+      new Range(head, tail).translate([0, -1], [0, +1])

   # which must be 'start' or 'end'
   setPositionForSelections: (which) ->


Prepare for testing

Prepare for testing

--- lib/models/git-difftool.coffee
+++ lib/models/git-difftool.coffee
@@ -20,7 +20,7 @@
   isFolder = false
   if packageObj?.selectedPath
     isFolder = fs.isDirectorySync packageObj.selectedPath
-    file = repo.relativize(packageObj.selectedPath)
+    file ?= repo.relativize(packageObj.selectedPath)

   file ?= repo.relativize(atom.workspace.getActiveTextEditor()?.getPath())
   if not file


minor

minor

--- src/Observatory.coffee
+++ src/Observatory.coffee
@@ -99,8 +99,10 @@
   # View formatters take a message accepted by loggers and further format it for nice output,
   # e.g., adding ANSI colors or html markup.
   viewFormatters:
-    _convertDate: (timestamp)->
-      timestamp.getUTCDate() + '/' + (timestamp.getUTCMonth()+1) + '/'+timestamp.getUTCFullYear()
+    _convertDate: (timestamp, long = false)->
+      ds = timestamp.getUTCDate() + '/' + (timestamp.getUTCMonth()+1)
+      ds = ds +  + '/'+timestamp.getUTCFullYear() if long
+      ds
     _convertTime: (timestamp, ms=true)->
       ts = timestamp.getUTCHours()+ ':' + timestamp.getUTCMinutes() + ':' + timestamp.getUTCSeconds()
       ts += '.' + timestamp.getUTCMilliseconds() if ms
@@ -241,8 +243,8 @@
       @[m] = @_emitWithSeverity.bind this, i

   # Trace a error - format stacktrace nicely and output with ERROR level
-  trace: (error, module)->
-    message = error.stack ? error
+  trace: (error, msg, module)->
+    message = msg + '\n' + (error.stack ? error)
     @_emitWithSeverity Observatory.LOGLEVEL.ERROR, message, module

   # Low-level emitting method that formats message and emits it


- fixed TOC non working folders

- fixed TOC non working folders

--- lib/utils/style_helpers.coffee
+++ lib/utils/style_helpers.coffee
@@ -47,7 +47,8 @@
     nodes    = []
     prevPath = []
     for file in files
-      targetChunks = file.targetPath.split path.join('/')
+      #targetChunks = file.targetPath.split path.join('/')
+      targetChunks = file.targetPath.split '/'
       pathChunks   = targetChunks[0...-1]

       # * If a file has the same name as a directory, it takes ownership of that directory's folder.
@@ -85,7 +86,8 @@
       nodes.push
         type:   'file'
         data:    fileData
-        depth:   file.targetPath.split( path.join('/') ).length
+        #depth:   file.targetPath.split( path.join('/') ).length
+        depth:   file.targetPath.split( '/' ).length
         outline: outlines[file.targetPath]

     @buildNodeTree nodes


fix bug

fix bug

--- src/businesscat.coffee
+++ src/businesscat.coffee
@@ -24,7 +24,7 @@
   index = arrayToDeleteFrom.indexOf term
   if index > -1
     arrayToDeleteFrom.splice index, 1
-    return arrayToDeleteFrom
+  arrayToDeleteFrom

 if process.env.HUBOT_BUSINESS_CAT_JARGON?
   additionalJargon = (process.env.HUBOT_BUSINESS_CAT_JARGON).split(',')


Do some further sources optimization

Do some further sources optimization

--- library/membrane/visual.coffee
+++ library/membrane/visual.coffee
@@ -162,7 +162,10 @@
     # one blob using the library called UglifyJS2. See it for info.
     compressSources: (context) ->
         sources = _.toArray context.sources
+        scripts = _.toArray context.scripts
         emptySources = "the JS sources are empty"
+        assert context.scripts = _.unique scripts
+        assert _.isArray sources = _.unique sources
         assert not _.isEmpty(sources), emptySources
         assert minify = require("uglify-js").minify
         minified = minify sources, fromString: yes


Fix edge case with the composer sys

Fix edge case with the composer sys

--- library/nucleus/compose.coffee
+++ library/nucleus/compose.coffee
@@ -54,6 +54,7 @@
         differentiated = _.take current, orphan
         duplicate = _.identity unless shader
         alternative = _.map differentiated, duplicate
+        return @rebased compound if _.isEmpty alternative
         tails = alternative.pop().rebased compound
         rebased = (acc, cls) -> cls.rebased acc; cls
         @rebased _.foldr alternative, rebased, tails
@@ -85,8 +86,8 @@
 # a valid __super__ descriptor, among some other prototypal things.
 Object.defineProperty Object::, "rebased",
     enumerable: no, value: (baseclass) ->
-        isClass = _.isObject baseclass.__super__
-        noClass = "The argument is not a class"
+        isClass = _.isObject baseclass?.__super__
+        noClass = "The #{baseclass} is not a class"
         throw new Error noClass unless isClass
         _.extend r = receiver = this, baseclass
         `function ctor() {this.constructor = r}`


Unify the syntax in the scoping module

Unify the syntax in the scoping module

--- library/nucleus/scoping.coffee
+++ library/nucleus/scoping.coffee
@@ -24,6 +24,7 @@
 ###

 _ = require "lodash"
+assert = require "assert"
 connect = require "connect"
 moment = require "moment"
 logger = require "winston"
@@ -46,10 +47,10 @@
     # The scope startup logic should be implemented in the method.
     constructor: (tag, synopsis) ->
         @tag = tag if _.isString(tag)
-        @synopsis = synopsis if _.isString(synopsis)
+        @synopsis = synopsis if _.isString synopsis
         @directory = @constructor.DIRECTORY or __dirname
-        initializer = _.find(arguments, _.isFunction)
-        initializer?.call(@, tag, synopsis)
+        initializer = _.find arguments, _.isFunction
+        initializer?.call this, tag, synopsis

     # Push the current scope instance into the global registry
     # of scopes, unless this instance already exists there. This
@@ -58,8 +59,8 @@
     pushToRegistry: (override, aliases...) ->
         registry = @constructor.REGISTRY ?= {}
         existent = (tag) -> tag of registry and not override
-        valids = _.filter(aliases, (a) -> not existent(a))
-        registry[@tag] = this unless existent(@tag)
+        valids = _.filter aliases, (a) -> not existent(a)
+        registry[@tag] = this unless existent @tag
         registry[alias] = this for alias in valids

     # Lookup the possibly existent scope with one of the following
@@ -71,8 +72,8 @@
         notFound = "Could not found any of #{joined} scoped"
         logger.info("Looking up any of this scopes: %s".grey, joined)
         found = (v for own k, v of registry when k in aliases)
-        throw new Error(notFound) unless found.length > 0
-        _.head(found)
+        throw new Error notFound unless found.length > 0
+        assert.ok _.isObject scope = _.head(found); scope

     # The utilitary method that is being called by implementation of
     # the incorporation method to establish the desirable facade for
@@ -82,9 +83,9 @@
         format = "DD/MM/YYYY @ hh:mm:ss"
         stamp = -> moment().format format
         options = timestamp: stamp, colorize: yes
-        options.level = nconf.get("log:level")
+        options.level = nconf.get "log:level"
         noLevel = "No logging level specified"
-        throw new Error(noLevel) unless options.level
+        throw new Error noLevel unless options.level
         logger.remove logger.transports.Console
         logger.add logger.transports.Console, options

@@ -96,8 +97,8 @@
         fpath = "#{@directory}/#{@tag}.json"
         nconf.defaults(@defaults or @constructor.DEFAULTS or {})
         nconf.overrides(@overrides or @constructor.OVERRIDES or {})
-        logger.info("Incorporating up the #{@tag} scope".cyan)
-        logger.info("Reading the #{fpath} config".cyan)
+        logger.info "Incorporating up the #{@tag} scope".cyan
+        logger.info "Reading the #{fpath} config".cyan
         exists = fs.existsSync fpath; nconf.file fpath if exists
         @constructor.setupLoggingFacade kernel

@@ -107,5 +108,5 @@
     # implementation does not do almost anything, so it is up to you.
     disperse: (kernel) ->
         fpath = "#{@directory}/#{@tag}.json"
-        logger.info("Dissipating the #{@tag} scope".grey)
-        logger.info("Scope used #{fpath} for config".grey)
+        logger.info "Dissipating the #{@tag} scope".grey
+        logger.info "Scope used #{fpath} for config".grey


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera_fade.coffee
+++ src/camera_fade.coffee
@@ -51,10 +51,10 @@

       camera.fadeOut()
       # => Sets the effect variables to their default state. This will the screen to fade from ransparent to black over the next 30 frames.
-
+
       camera.fadeOut
         color: blue
-        duration:
+        duration: 30
       # => This effect will start off transparent and change to blue over 50 frames.

   @name fadeOut


return promise during authentication

return promise during authentication

--- src/client/auth.coffee
+++ src/client/auth.coffee
@@ -199,7 +199,14 @@

   login : () =>

-    return console.info "Login in progress. Use `Hull.on('hull.user.login', callback)` to call `callback` when done." if @isAuthenticating()
+    if @isAuthenticating()
+      # Return promise even if login is in progress.
+      msg = "Login in progress. Use `Hull.on('hull.user.login', callback)` to call `callback` when done."
+      console.info msg
+      dfd = promises.deferred()
+      dfd.reject {error: {reason:'in_progress', message: msg}}
+      return dfd.promise;
+
     # Handle Legacy Format,
     # Ensure New Format: Hash signature
     # Preprocess Options


Fixed autosave of files opened via url [#164375620]

This fix updates the state metadata before sending file open events.  The listeners to that event used the state to set the autosaves (and to update the top nav)

--- src/code/client.coffee
+++ src/code/client.coffee
@@ -721,6 +721,9 @@

   _fileOpened: (content, metadata, additionalState={}, hashParams=null) ->
     eventData = { content: content?.getClientContent() }
+    # update state before sending 'openedFile' events so that 'openedFile' listeners that
+    # reference state have the updated state values
+    @_updateState content, metadata, additionalState, hashParams
     # add metadata contentType to event for CODAP to load via postmessage API (for SageModeler standalone)
     contentType = metadata.mimeType or metadata.contentType
     eventData.metadata = {contentType} if contentType
@@ -730,6 +733,7 @@
       metadata?.overwritable ?= true
       if not @appOptions.wrapFileContent
         content.addMetadata iSharedMetadata
+      # and then update state again for the metadata and content changes
       @_updateState content, metadata, additionalState, hashParams
       @ready()


Finish Intro Section

Finish Intro Section

--- src/coffee/game.coffee
+++ src/coffee/game.coffee
@@ -20,26 +20,39 @@

     quality 'illusionHunch', qualityType.item,
       'Illusion Hunch'
-      'Regenerates at the start of each day.'
+      '''
+      Used to see the true form of things.
+
+      _Regenerates at the start of each day._
+      '''
       value: 1

     quality 'hallucinationHunch', qualityType.item,
       'Hallucination Hunch'
-      'Regenerates at the start of each day.'
+      '''
+      Used to spot nonexistant things.
+
+      _Regenerates at the start of each day._
+      '''
       value: 1

     quality 'hypnotismHunch', qualityType.item,
       'Hypnotism Hunch'
-      'Regenerates at the start of each day.'
-      value: 1
-
-    quality 'blueback', qualityType.item,
-      'Blueback'
+      '''
+      Used to notice when others are being controlled.
+
+      _Regenerates at the start of each day._
+      '''
+      value: 1
+
+    quality 'bluebacks', qualityType.item,
+      'Bluebacks'
       'Magical counterfeit-proof bills. They\'re actually more of an iridescent black.'

     quality 'cantripUp', qualityType.item,
       'Cantrip-Up'
       'The beverage of mages. Contains concentrated warpweft that mildly enhances magical powers.'
+      value: 1

     quality 'rumor', qualityType.item,
       'Rumor'
@@ -81,11 +94,14 @@
       '''
       The years that followed the commoditization of sorcery were tumultous and difficult. People
       were faced with an ever-expanding array of new possibilities for their lives. The social
-      order was shaken like an uptown Old Fashioned. There was, naturally, a war. You came out of it
-      with a few interesting scars to share with lovers and one very uninteresting scar on your
-      psyche that left you permanently unable to access the sorceric warpweave ever again. It was a
-      shame, too. You'd had such a promising career before that, studying… Which branch of magic was
-      it again?
+      order was shaken like an uptown Old Fashioned.
+
+      There was, naturally, a war. You came out of it with a few interesting scars to share with
+      those most intimate and one very uninteresting scar on your psyche that left you permanently
+      unable to access the sorceric warpweave ever again. It was a shame, too. You'd had such a
+      promising career before that, studying…
+
+      Which branch of magic was it again?
       '''
       choices: [
         choice 'begin-illusion',
@@ -101,21 +117,25 @@

     beginSnippet =
       '''
-      Yes, that was it. Anyway, that's all over now. The war left you without a scrap of magic
+      Yes, that was it.
+
+      Anyway, that's all over now. The war left you without a scrap of magic
       to your name, just a strange itch whenever you're in the presence of your old field of work.
+
       But you, you were enterprising. You parleyed that subliminal itch, that instinct about magic,
       into a career as a detective. When people have problems that seem magical in nature, they come
       to you. You are The Disillusionist.
       '''
     beginChoice =
       choice 'begin-case',
-        'Tell me about my latest case.'
+        'Tell me about my latest case'

     storylet 'begin-illusion',
       'Ah, Illusion'
       beginSnippet
       consequences:
         origin: consq.set origins.illusionist
+        illusionHunch: consq.increase 1
       choices: [beginChoice]

     storylet 'begin-hallucination',
@@ -123,6 +143,7 @@
       beginSnippet
       consequences:
         origin: consq.set origins.hallucinist
+        hallucinationHunch: consq.increase 1
       choices: [beginChoice]

     storylet 'begin-hypnotism',
@@ -130,16 +151,40 @@
       beginSnippet
       consequences:
         origin: consq.set origins.hypnotist
+        hypnotismHunch: consq.increase 1
       choices: [beginChoice]

     storylet 'begin-case',
       'It Was Late on a Tuesday…'
       '''
-      It was late on a Tuesday when Mrs. Abigail Brown walked into your office. Immediately, you
-      knew this wasn't the usual cheating-husband job. There was a tear to her eye before she even
+      It was late on a Tuesday when Mrs. Abigail Brown walked into your office. Normally, when a
+      lady that well-dressed sees fit to cross your door, there's infidelity behind it. It means
+      long nights staking out a love nest in your future, waiting for Mr. Client to come dally with
+      Not-Mrs. Client.
+
+      But something was off. This wasn't the usual cheating-husband job. There was a tear to her
+      eye before she even
       got in your door, not the mixture of determination and shame that eventually gave way to
       either sobbing or rage.
-      '''
+
+      Over the next forty-five minutes, you drew the whole story out of her. How she was the wife
+      of famed sorcerer Horace Brown. How he had gone missing two weeks ago from a sealed parlor
+      in their Silver Street home. How the police had dismissed the case with suspicious ease,
+      insisting Mr.
+      Brown must have simply had an accident with forces beyond his control. As if "Demon Eyes
+      Brown" had ever met a force he could not match.
+
+      You took the case in a heartbeat. This was not only the most interesting case to cross your
+      desk since there was snow on the ground, but it stood to be the most lucrative in a long time,
+      and the landlord was starting to resemble a hungry wolf more and more each day. Mrs. Brown
+      paid the retainer for your first two days and didn't even haggle the rate.
+
+      Now the only question was: Where to start looking?
+      '''
+      consequences:
+        bluebacks: consq.increase 2
+
+    ## Front Categories

     return