update date

update date

--- app/assets/javascripts/charts.js.coffee
+++ app/assets/javascripts/charts.js.coffee
@@ -47,8 +47,8 @@
     yyyy = today.getFullYear()

     $('.datepicker').pickadate
-      min: new Date(2015, 6,30)
-      max: new Date(yyyy, mm, dd-2)
+      min: new Date(2015, 8, 20)
+      max: new Date(yyyy, mm, dd-1)
       # selectMonths: true
       # selectYears: 15
       formatSubmit: 'yyyy/mm/dd'


at least two columns on groups

at least two columns on groups

--- app/assets/javascripts/global.js.coffee
+++ app/assets/javascripts/global.js.coffee
@@ -22,5 +22,5 @@
   # Columnize groups list
   $(".groups_container").each ->
     number_of_columns = Math.ceil( $(this).find("li").size() / 30 )
-    console.log(number_of_columns)
-    $(this).find("ul").makeacolumnlists({ cols: number_of_columns }) if number_of_columns > 1+    number_of_columns++ if number_of_columns == 1
+    $(this).find("ul").makeacolumnlists({ cols: number_of_columns })


Detach placeholder only if it exists

Detach placeholder only if it exists

--- src/mentions-kinder.coffee
+++ src/mentions-kinder.coffee
@@ -132,7 +132,7 @@

   handlePlaceholder: (event)=>
     if event.type == 'focus'
-      @$placeholder.detach()
+      @$placeholder?.detach()
     else if event.type == 'blur'
       if @_strip(@serializeEditable()) == ''
         @$editable.empty().append(@$placeholder)


date picker change month/year registers as a selection.

--- app/assets/javascripts/people.js.coffee
+++ app/assets/javascripts/people.js.coffee
@@ -10,6 +10,13 @@
     changeMonth: true,
     dateFormat: "mm/dd/yy",
     yearRange: '-80:+1'
+    onChangeMonthYear: (year, month, inst) ->
+      selectedDate = $(this).datepicker("getDate")
+      if(selectedDate == null)
+        return
+      selectedDate.setMonth month - 1 # 0-11
+      selectedDate.setFullYear year
+      $(this).datepicker "setDate", selectedDate

   $('.input-group-btn button').click ->
     $('.date_picker').datepicker("show")


Adjusted coffeescript indentation

Adjusted coffeescript indentation

--- app/assets/javascripts/places.js.coffee
+++ app/assets/javascripts/places.js.coffee
@@ -10,14 +10,14 @@
 @convert = (objects) ->
   array = []

-for x in objects
-  pin =
-    lat: x.lat
-    lng: x.lng
-    infowindow: x.name
-  array.push pin
+  for x in objects
+    pin =
+      lat: x.lat
+      lng: x.lng
+      infowindow: x.name
+    array.push pin

-googleMap array
+  googleMap array

 @googleMap = (content) ->
   handler = Gmaps.build("Google")


cleaned up unused coffeescript

cleaned up unused coffeescript

--- app/assets/javascripts/reactions.coffee
+++ app/assets/javascripts/reactions.coffee
@@ -3,10 +3,3 @@
 # You can use CoffeeScript in this file: http://coffeescript.org/

 $(document).on 'page:change', ->
-
-  # When the user leaves the URL field check to see if the image is valid client-side.
-  $(document).on 'focusout', '#reaction_url', ->
-    img_url = $('#reaction_url').val()
-    img = $('<img />', {src : img_url});
-    $('#img_preview').empty()
-    img.appendTo('#img_preview')


调试完成obj_parser.coffee

--- _lib/obj_parser.coffee
+++ _lib/obj_parser.coffee
@@ -3,26 +3,52 @@
 # # # # # # # # # # # # # # # # # # # #

 CHAR_INT = 0x1
-CHAR_FLOAT = 0x2
+CHAR_INDEX = 0x2
+CHAR_FLOAT = 0x4

 CHAR_SIZE = 128
 CHAR_TABLE = new Uint32Array(CHAR_SIZE)
-CHAR_TABLE[48] = CHAR_INT | CHAR_FLOAT # -
+CHAR_TABLE[45] = CHAR_FLOAT # -
 CHAR_TABLE[46] = CHAR_FLOAT # .
-CHAR_TABLE[48] = CHAR_INT | CHAR_FLOAT # 0
-CHAR_TABLE[49] = CHAR_INT | CHAR_FLOAT # 1
-CHAR_TABLE[50] = CHAR_INT | CHAR_FLOAT # 2
-CHAR_TABLE[51] = CHAR_INT | CHAR_FLOAT # 3
-CHAR_TABLE[52] = CHAR_INT | CHAR_FLOAT # 4
-CHAR_TABLE[53] = CHAR_INT | CHAR_FLOAT # 5
-CHAR_TABLE[54] = CHAR_INT | CHAR_FLOAT # 6
-CHAR_TABLE[55] = CHAR_INT | CHAR_FLOAT # 7
-CHAR_TABLE[56] = CHAR_INT | CHAR_FLOAT # 8
-CHAR_TABLE[57] = CHAR_INT | CHAR_FLOAT # 9
+CHAR_TABLE[47] = CHAR_INDEX # /
+CHAR_TABLE[48] = CHAR_INT | CHAR_FLOAT | CHAR_INDEX # 0
+CHAR_TABLE[49] = CHAR_INT | CHAR_FLOAT | CHAR_INDEX # 1
+CHAR_TABLE[50] = CHAR_INT | CHAR_FLOAT | CHAR_INDEX # 2
+CHAR_TABLE[51] = CHAR_INT | CHAR_FLOAT | CHAR_INDEX # 3
+CHAR_TABLE[52] = CHAR_INT | CHAR_FLOAT | CHAR_INDEX # 4
+CHAR_TABLE[53] = CHAR_INT | CHAR_FLOAT | CHAR_INDEX # 5
+CHAR_TABLE[54] = CHAR_INT | CHAR_FLOAT | CHAR_INDEX # 6
+CHAR_TABLE[55] = CHAR_INT | CHAR_FLOAT | CHAR_INDEX # 7
+CHAR_TABLE[56] = CHAR_INT | CHAR_FLOAT | CHAR_INDEX # 8
+CHAR_TABLE[57] = CHAR_INT | CHAR_FLOAT | CHAR_INDEX # 9
 CHAR_TABLE[69] = CHAR_FLOAT # E
 CHAR_TABLE[101] = CHAR_FLOAT # e

-VERTEX_REGEXP = /^v\s+([\d\.\+-eE]+)\s+([\d\.\+-eE]+)\s+([\d\.\+-eE]+)\s+$/
+VERTEX_STRIDE = 3
+TEXTURE_STRIDE = 2
+NORMAL_STRIDE = 3
+
+createContext = () ->
+  return {
+    vertex: new Array(512 * VERTEX_STRIDE)
+    vertexLen: 0
+    texture: new Array(512 * TEXTURE_STRIDE)
+    textureLen: 0
+    normal: new Array(512 * NORMAL_STRIDE)
+    normalLen: 0
+
+    initFlag: false
+    vertexStride: 0
+    textureStride: 0
+    normalStride: 0
+    totalStride: 0
+
+    vtxBuf: null
+    vtxBufLen: 0
+    idxBuf: null
+    idxBufLen: 0
+    idxMap: Object.create(null)
+  }

 ignoreSpace = (text, ptr) ->
   if ' ' != text[ptr] and '\t' != text[ptr]
@@ -33,7 +59,7 @@
   return ptr

 ignoreLine = (text, ptr) ->
-  while true
+  while undefined != text[ptr]
     if '\r' == text[ptr]
       if '\r' != text[ptr + 1]
         ptr = ptr + 1
@@ -49,42 +75,42 @@
     ptr = ptr + 1
   return ptr

-parseIntx = do ->
+parseIntEx = do ->
   result =
     ptr: 0
     num: 0
   return (text, ptr) ->
     start = ptr
     charCode = text.charCodeAt(ptr)
-    if charCode >= CHAR_SIZE or 0 == CHAR_INT | CHAR_TABLE[charCode]
+    if (charCode >= CHAR_SIZE) or (0 == (CHAR_INT & CHAR_TABLE[charCode]))
       throw new Error()
     ptr = ptr + 1
     while true
       charCode = text.charCodeAt(ptr)
-      if charCode >= CHAR_SIZE or 0 == CHAR_INT | CHAR_TABLE[charCode]
+      if (charCode >= CHAR_SIZE) or (0 == (CHAR_INT & CHAR_TABLE[charCode]))
         break
       ptr = ptr + 1
     result.ptr = ptr
     result.num = parseInt(text[start...ptr])
     return result

-parseFloatx = do ->
+parseFloatEx = do ->
   result =
     ptr: 0
     num: 0
   return (text, ptr) ->
     start = ptr
     charCode = text.charCodeAt(ptr)
-    if charCode >= CHAR_SIZE or 0 == CHAR_FLOAT | CHAR_TABLE[charCode]
+    if (charCode >= CHAR_SIZE) or (0 == (CHAR_FLOAT & CHAR_TABLE[charCode]))
       throw new Error()
     ptr = ptr + 1
     while true
       charCode = text.charCodeAt(ptr)
-      if charCode >= CHAR_SIZE or 0 == CHAR_FLOAT | CHAR_TABLE[charCode]
+      if (charCode >= CHAR_SIZE) or (0 == (CHAR_FLOAT & CHAR_TABLE[charCode]))
         break
       ptr = ptr + 1
     result.ptr = ptr
-    result.num = parseInt(text[start...ptr])
+    result.num = parseFloat(text[start...ptr])
     return result

 parseVertex = do ->
@@ -93,11 +119,11 @@
     vx: 0, vy: 0, vz: 0
   (text, ptr) ->
     ptr = ignoreSpace(text, ptr)
-    {num: result.vx, ptr} = parseFloatx(text, ptr)
-    ptr = ignoreSpace(text, ptr)
-    {num: result.vy, ptr} = parseFloatx(text, ptr)
-    ptr = ignoreSpace(text, ptr)
-    {num: result.vz, ptr} = parseFloatx(text, ptr)
+    {num: result.vx, ptr} = parseFloatEx(text, ptr)
+    ptr = ignoreSpace(text, ptr)
+    {num: result.vy, ptr} = parseFloatEx(text, ptr)
+    ptr = ignoreSpace(text, ptr)
+    {num: result.vz, ptr} = parseFloatEx(text, ptr)
     result.ptr = ignoreLine(text, ptr)
     return result

@@ -107,134 +133,210 @@
     tx: 0, ty: 0
   (text, ptr) ->
     ptr = ignoreSpace(text, ptr)
-    {num: result.tx, ptr} = parseFloatx(text, ptr)
-    ptr = ignoreSpace(text, ptr)
-    {num: result.ty, ptr} = parseFloatx(text, ptr)
+    {num: result.tx, ptr} = parseFloatEx(text, ptr)
+    ptr = ignoreSpace(text, ptr)
+    {num: result.ty, ptr} = parseFloatEx(text, ptr)
     result.ptr = ignoreLine(text, ptr)
     return result

-parseVertex = do ->
+parseNormal = do ->
   result =
     ptr: 0
     nx: 0, ny: 0, nz: 0
   (text, ptr) ->
     ptr = ignoreSpace(text, ptr)
-    {num: result.nx, ptr} = parseFloatx(text, ptr)
-    ptr = ignoreSpace(text, ptr)
-    {num: result.ny, ptr} = parseFloatx(text, ptr)
-    ptr = ignoreSpace(text, ptr)
-    {num: result.nz, ptr} = parseFloatx(text, ptr)
+    {num: result.nx, ptr} = parseFloatEx(text, ptr)
+    ptr = ignoreSpace(text, ptr)
+    {num: result.ny, ptr} = parseFloatEx(text, ptr)
+    ptr = ignoreSpace(text, ptr)
+    {num: result.nz, ptr} = parseFloatEx(text, ptr)
     result.ptr = ignoreLine(text, ptr)
     return result

 parseThird = do ->
   result =
     ptr: 0
-    vi: 0, ti: 0, ni: 0
-  return (text, ptr) ->
-    result.vi = null
-    result.ti = null
-    result.ni = null
-    # vi
-    {ptr: ptr, num: result.vi} = parseIntx(text, ptr)
-    # ti
-    if '/' != text[ptr]
-      result.ptr = ptr
-      return result
-    ptr = ptr + 1
-    if '/' != text[ptr]
-      {ptr: ptr, num: result.ti} = parseIntx(text, ptr)
-    # ni
-    if '/' != text[ptr]
-      result.ptr = ptr
-      return result
-    ptr = ptr + 1
-    {ptr: ptr, num: result.ni} = parseIntx(text, ptr)
+    idxText: ""
+  (text, ptr) ->
+    start = ptr
+    while true
+      charCode = text.charCodeAt(ptr)
+      if 0 == (CHAR_INDEX & CHAR_TABLE[charCode])
+        break
+      ptr = ptr + 1
     result.ptr = ptr
-    return result
-
-parseFace = do ->
-  result =
-    ptr: 0
-    vi1: 0, ti1: 0, ni1: 0
-    vi2: 0, ti2: 0, ni2: 0
-    vi3: 0, ti3: 0, ni3: 0
-  return (text, ptr) ->
-    {ptr, vi: result.vi1, ti: result.ti1, ni: result.ni1} = parseThird(text, ptr)
-    ptr = ignoreSpace(text, ptr)
-    {ptr, vi: result.vi2, ti: result.ti2, ni: result.ni2} = parseThird(text, ptr)
-    ptr = ignoreSpace(text, ptr)
-    {ptr, vi: result.vi3, ti: result.ti3, ni: result.ni3} = parseThird(text, ptr)
-    ptr = ignoreLine(text, ptr)
-    result.ptr = ptr
-    return result
+    result.idxText = text[start...ptr]
+    if '' == result.idxText
+      throw new Error()
+    return result
+
+writeIndex = (ctx, idxText) ->
+  numArray = idxText.split('/')
+  # init once
+  if not ctx.initFlag
+    ctx.initFlag = true
+    ctx.totalStride = 0
+    if '' != numArray[0]
+      ctx.vertexStride = VERTEX_STRIDE
+      ctx.totalStride = ctx.totalStride + VERTEX_STRIDE
+    if '' != numArray[1]
+      ctx.textureStride = TEXTURE_STRIDE
+      ctx.totalStride = ctx.totalStride + TEXTURE_STRIDE
+    if '' != numArray[2]
+      ctx.normalStride = NORMAL_STRIDE
+      ctx.totalStride = ctx.totalStride + NORMAL_STRIDE
+    ctx.vtxBuf = new Array(ctx.totalStride * 512)
+    ctx.idxBuf = new Array(4 * 512)
+  # 重复的idx不再写入
+  idx = ctx.idxMap[idxText]
+  if 'number' == typeof(idx)
+    ctx.idxBuf[ctx.idxBufLen++] = idx
+    return
+  # 解析新的idx
+  ptr = 0
+  offset = 0
+  # 填充vertex
+  if ctx.vertexStride
+    {ptr: ptr, num: vi} = parseIntEx(idxText, ptr)
+    vi = vi - 1
+    if vi > ctx.vertexLen
+      throw new Error()
+    ctx.vtxBuf[ctx.totalStride * ctx.vtxBufLen + offset++] = ctx.vertex[VERTEX_STRIDE * vi + 0]
+    ctx.vtxBuf[ctx.totalStride * ctx.vtxBufLen + offset++] = ctx.vertex[VERTEX_STRIDE * vi + 1]
+    ctx.vtxBuf[ctx.totalStride * ctx.vtxBufLen + offset++] = ctx.vertex[VERTEX_STRIDE * vi + 2]
+  # 填充texture
+  if ctx.textureStride
+    if '/' != idxText[ptr]
+      throw new Error()
+    ptr = ptr + 1
+    {ptr: ptr, num: ti} = parseIntEx(idxText, ptr)
+    ti = ti - 1
+    if ti > ctx.textureLen
+      throw new Error()
+    ctx.vtxBuf[ctx.totalStride * ctx.vtxBufLen + offset++] = ctx.texture[TEXTURE_STRIDE * ti + 0]
+    ctx.vtxBuf[ctx.totalStride * ctx.vtxBufLen + offset++] = ctx.texture[TEXTURE_STRIDE * ti + 1]
+  # 填充normal
+  if ctx.normalStride
+    if '/' != idxText[ptr]
+      throw new Error()
+    ptr = ptr + 1
+    if '/' == idxText[ptr]
+      ptr = ptr + 1
+    {ptr: ptr, num: ni} = parseIntEx(idxText, ptr)
+    ni = ni - 1
+    if ni > ctx.normalLen
+      throw new Error()
+    ctx.vtxBuf[ctx.totalStride * ctx.vtxBufLen + offset++] = ctx.vertex[NORMAL_STRIDE * ni + 0]
+    ctx.vtxBuf[ctx.totalStride * ctx.vtxBufLen + offset++] = ctx.vertex[NORMAL_STRIDE * ni + 1]
+    ctx.vtxBuf[ctx.totalStride * ctx.vtxBufLen + offset++] = ctx.vertex[NORMAL_STRIDE * ni + 2]
+  # 填充index
+  if ptr != idxText.length
+    throw new Error()
+  ctx.idxBuf[ctx.idxBufLen++] = ctx.vtxBufLen
+  ctx.idxMap[idxText] = ctx.vtxBufLen
+  ctx.vtxBufLen = ctx.vtxBufLen + 1
+  return

 parseObj = (text) ->
-  vertex = new Array(512 * 3)
-  vertexLen = 0
-  texture = new Array(512 * 2)
-  textureLen = 0
-  normal = new Array(512 * 3)
-  normalLen = 0
-
-  initBufFormat = false
-  vertexFlag = false
-  textureFlag = false
-  normalFlag = false
-
-  vtxBuf = null
-  vtxBufLen = 0
-  vtxBufPtr = 0
-  vtxStride = 0
-  idxBuf = null
-  idxBufLen = 0
-  idxMap = Object.create(null)
-
+  ctx = createContext()
   ptr = 0
-  switch text[ptr]
-    when 'v'
-      ptr = ptr + 1
-      if 't' == text[ptr]
-        ptr = ptr + 1
-        ptr = ignoreSpace(text, ptr)
-        {ptr, tx, ty} = parseTexture(text, ptr)
-        texture[textureLen++] = tx
-        texture[textureLen++] = ty
-      else if 'n' == text[ptr]
-        ptr = ptr + 1
-        ptr = ignoreSpace(text, ptr)
-        {ptr, nx, ny, nz} = parseVertex(text, ptr)
-        normal[normalLen++] = nx
-        normal[normalLen++] = ny
-        normal[normalLen++] = nz
+  textLen = text.length
+  while ptr < textLen
+    switch text[ptr]
+      when 'v'
+        ptr = ptr + 1
+        if 't' == text[ptr]
+          ptr = ptr + 1
+          {ptr, tx, ty} = parseTexture(text, ptr)
+          ctx.texture[TEXTURE_STRIDE * ctx.textureLen + 0] = tx
+          ctx.texture[TEXTURE_STRIDE * ctx.textureLen + 1] = ty
+          ctx.textureLen = ctx.textureLen + 1
+        else if 'n' == text[ptr]
+          ptr = ptr + 1
+          {ptr, nx, ny, nz} = parseNormal(text, ptr)
+          ctx.normal[NORMAL_STRIDE * ctx.normalLen + 0] = nx
+          ctx.normal[NORMAL_STRIDE * ctx.normalLen + 1] = ny
+          ctx.normal[NORMAL_STRIDE * ctx.normalLen + 2] = nz
+          ctx.normalLen = ctx.normalLen + 1
+        else
+          {ptr, vx, vy, vz} = parseVertex(text, ptr)
+          ctx.vertex[VERTEX_STRIDE * ctx.vertexLen + 0] = vx
+          ctx.vertex[VERTEX_STRIDE * ctx.vertexLen + 1] = vy
+          ctx.vertex[VERTEX_STRIDE * ctx.vertexLen + 2] = vz
+          ctx.vertexLen = ctx.vertexLen + 1
+      when 'f'
+        ptr = ptr + 1
+        ptr = ignoreSpace(text, ptr)
+        {ptr, idxText} = parseThird(text, ptr)
+        writeIndex(ctx, idxText)
+        ptr = ignoreSpace(text, ptr)
+        {ptr, idxText} = parseThird(text, ptr)
+        writeIndex(ctx, idxText)
+        ptr = ignoreSpace(text, ptr)
+        {ptr, idxText} = parseThird(text, ptr)
+        writeIndex(ctx, idxText)
+        ptr = ignoreLine(text, ptr)
+      when '#'
+        ptr = ignoreLine(text, ptr)
+      when 'o'
+        ptr = ptr + 1
+        ptr = ignoreSpace(text, ptr)
+        ptr = ignoreLine(text, ptr)
+      when 's'
+        ptr = ptr + 1
+        ptr = ignoreSpace(text, ptr)
+        ptr = ignoreLine(text, ptr)
+      when 'm'
+        if 'mtllib' != text[ptr...(ptr+6)]
+          throw new Error()
+        ptr = ptr + 6
+        ptr = ignoreSpace(text, ptr)
+        ptr = ignoreLine(text, ptr)
+      when 'u'
+        if 'usemtl' != text[ptr...(ptr+6)]
+          throw new Error()
+        ptr = ptr + 6
+        ptr = ignoreSpace(text, ptr)
+        ptr = ignoreLine(text, ptr)
+      when '\r', '\n'
+        ptr = ignoreLine(text, ptr)
+      when ' ', '\t'
+        ptr = ignoreSpace(text, ptr)
+        ptr = ignoreLine(text, ptr)
       else
-        ptr = ignoreSpace(text, ptr)
-        {ptr, vx, vy, vz} = parseVertex(text, ptr)
-        vertex[vertexLen++] = vx
-        vertex[vertexLen++] = vy
-        vertex[vertexLen++] = vz
-    when 'f'
-      ptr = ptr + 1
-      ptr = ignoreSpace(text, ptr)
-      {ptr, vi1, ti1, ni1, vi2, ti2, ni2, vi3, ti3, ni3} = parseFace(text, ptr)
-
-    when 'o'
-      ptr = ptr + 1
-      ptr = ignoreSpace(text, ptr)
-      ptr = ignoreLine(text, ptr)
-    when 's'
-      ptr = ptr + 1
-      ptr = ignoreSpace(text, ptr)
-      ptr = ignoreLine(text, ptr)
-    when 'm'
-      if 'mtllib' != text[ptr...(ptr+6)]
         throw new Error()
-      ptr = ptr + 6
-      ptr = ignoreSpace(text, ptr)
-      ptr = ignoreLine(text, ptr)
-    when 'u'
-      if 'usemtl' != text[ptr...(ptr+6)]
-        throw new Error()
-      ptr = ptr + 6
-      ptr = ignoreSpace(text, ptr)
-      ptr = ignoreLine(text, ptr)
+  ctx.vtxBuf.length = ctx.vtxBufLen * ctx.totalStride
+  ctx.idxBuf.length = ctx.idxBufLen
+  return {
+    vtxBuf: new Float32Array(ctx.vtxBuf)
+    idxBuf: new Uint32Array(ctx.idxBuf)
+    vertexStride: ctx.vertexStride
+    textureStride: ctx.textureStride
+    normalStride: ctx.normalStride
+  }
+
+objFile = """
+# ......
+
+v  -0.5000 0.0000 0.5000
+v  -0.5000 0.0000 0.3750
+v  -0.3750 0.0000 0.3750
+v  -0.3750 0.0000 0.5000
+
+vn 0.0000 -1.0000 -0.0000
+vn 0.0000 -1.0000 -0.0000
+vn 0.0000 -1.0000 -0.0000
+vn 0.0000 -1.0000 -0.0000
+
+vt 1.0000 0.0000 0.0000
+vt 1.0000 0.1250 0.0000
+vt 0.8750 0.1250 0.0000
+vt 0.8750 0.0000 0.0000
+
+f 1/1/1 2/2/1 3/3/1
+f 3/3/1 4/4/1 1/1/1
+"""
+
+debugger
+res = parseObj(objFile)
+console.log(res)


Listen on keydown so you can keep arrow keys down

--- app/assets/javascripts/search.js.coffee
+++ app/assets/javascripts/search.js.coffee
@@ -11,7 +11,7 @@

   constructor: (root) ->
     @$root = $(root)
-    @$root.on('keyup', '.stack-search', @onKeyUp)
+    @$root.on('keydown', '.stack-search', @onKeyUp)

   onKeyUp: (event) =>
     @$items = @$root.find('[data-search]')


Remove console.log

Remove console.log

--- src/ngRepeatReorder.coffee
+++ src/ngRepeatReorder.coffee
@@ -299,8 +299,7 @@
 							block.scope.$destroy()
 					index = 0
 					length = collectionKeys.length
-					console.log nextBlockMap
-					console.log nextBlockOrder
+
 					while index < length
 						key = (if (collection is collectionKeys) then index else collectionKeys[index])
 						value = collection[key]
@@ -341,4 +340,4 @@

 				return
 		)
-]+]


429 error resolved

429 error resolved

--- src/openweather/api.coffee
+++ src/openweather/api.coffee
@@ -31,8 +31,13 @@
     deferred = Q.defer()
     request path, (error, response, body) ->
       deferred.reject error if error
-      result = JSON.parse body
-      deferred.resolve result
+      if response.statusCode is 429
+        setTimeout((->
+          new Api().get_forecasts(location, count, unit)), 1000)
+        deferred.resolve
+      else
+        result =  JSON.parse body
+        deferred.resolve result
     deferred.promise

   get_forecasts: (location, count, unit) ->
@@ -46,12 +51,15 @@
     ++options.cnt
     options.unit = unit if unit?
     path = "#{Api::paths.forecast}?#{querystring.stringify options}"
-    console.log path
     deferred = Q.defer()
     request path, (error, response, body) ->
       deferred.reject error if error
-      console.log body
-      results =  JSON.parse body
-      results.list = _.drop results.list
-      deferred.resolve results #JSON.parse body
+      if response.statusCode is 429
+        setTimeout((->
+          new Api().get_forecasts(location, count, unit)), 1000)
+        deferred.resolve #JSON.parse body
+      else
+        results =  JSON.parse body
+        results.list = _.drop results.list
+        deferred.resolve results
     deferred.promise


social: add default error  name

social: add default error  name

--- workers/social/lib/social/models/socialapi/error.coffee
+++ workers/social/lib/social/models/socialapi/error.coffee
@@ -5,5 +5,5 @@

 module.exports = class ApiError extends Error
   constructor:({ description, error }) ->
-    name = KnownApiErrors[error]
+    name = KnownApiErrors[error] ? 'BadRequest'
     return new KodingError description, name


Uppercase disclaimers of warranties

Uppercase disclaimers of warranties

--- ui/scripts/templates/pages/terms.coffee
+++ ui/scripts/templates/pages/terms.coffee
@@ -19,13 +19,17 @@

-        Sagefy is provided as-is and as-available.
-        Sagefy makes no warranties.
+        SAGEFY IS PROVIDED AS-IS AND AS-AVAILABLE.
+        SAGEFY MAKES NO WARRANTIES.
+        SAGEFY DOES NOT OFFER WARRANTIES
+        ON MERCHANTABILITY,
+        FITNESS FOR A PARTICULAR USE,
+        OR NON-INFRINGEMENT OF INTELLECTUAL PROPERTY.

-        Sagefy is not liable for any damages
-        coming from use of Sagefy.
+        SAGEFY IS NOT LIABLE FOR ANY DAMAGES COMING FROM USE
+                OF SAGEFY.


Fetch channel so it can be added back

Fetch channel so it can be added back

Maybe all data can be returned in the activity json?

--- core/app/backbone/views/notification_view.coffee
+++ core/app/backbone/views/notification_view.coffee
@@ -33,9 +33,9 @@
     @activity = @model.get('activity')

   channel: ->
-    channel = new Channel
-      id: @activity.to_channel_id
-      containing_channel_ids: @activity.to_channel_containing_channel_ids
+    channel = new Channel(id: @activity.to_channel_id)
+    channel.fetch()
+    channel

   onRender: ->
     super()


+ ExpressionStack: tested pushing without stack name

--- test/unit/expression-stack.coffee
+++ test/unit/expression-stack.coffee
@@ -11,10 +11,16 @@
   it ".stackName is 'bottom'", ->
     testedStack.stackName.should.equal "bottom"

+  describe "after .push()", ->
+    beforeEach ->
+      testedStack.push()
+
+    it ".stackName is 'unnamed'", ->
+      testedStack.stackName.should.equal "unnamed"
+
   describe "after .push('next')", ->
     beforeEach ->
       testedStack.push "next"

     it ".stackName is 'next'", ->
       testedStack.stackName.should.equal "next"
-


log error

log error

--- modules/eval.coffee
+++ modules/eval.coffee
@@ -36,5 +36,7 @@
           icon_url: msg.mentions[0].avatarURL
           text: args.slice(1).join(' ')
         }
+      .catch (e)=>
+        console.error(e)

 module.exports = EvalModule


do not truncate task keys

do not truncate task keys

--- app/pages/lab/workflow-viewer/task.cjsx
+++ app/pages/lab/workflow-viewer/task.cjsx
@@ -257,7 +257,7 @@
     <Resizable className={box_class} style={clone(@state.style)} id={@props.plumbId} ref={@props.plumbId} embedCss={false} onResize={@onResize}>

-          {"Sub-" if @state.task.subtask}{"Task #{@props.taskKey.substr(1)}: "}{icon}{" #{@state.task.type.charAt(0).toUpperCase()}#{@state.task.type.substr(1)}"}
+          {"Sub-" if @state.task.subtask}{"Task #{@props.taskKey}: "}{icon}{" #{@state.task.type.charAt(0).toUpperCase()}#{@state.task.type.substr(1)}"}


cleanup key interactions; disable on form focus, etc.

--- app/assets/javascripts/ample_assets/classes/ample_assets.js.coffee
+++ app/assets/javascripts/ample_assets/classes/ample_assets.js.coffee
@@ -11,6 +11,7 @@

   set_options: (opts) ->
     @current = 0
+    @keys_enabled = true
     ref = this
     default_options =
       debug: false
@@ -45,7 +46,6 @@
         height: 81
         enabled: true
         distance: 10
-        keyboard_nav: true
         auto: false
         orientation: 'horizontal'
         key_orientation: 'horizontal'
@@ -272,7 +272,9 @@
       @log "panels(#{i})"
       el = "##{@options.id} .pages .page:nth-child(#{(i+1)}) ul"
       @options.pages[i]['panel_selector'] = el
+      @active_panel = el
       @options.pages[i][''] = $(el).attr('id',"#{@options.pages[i]['id']}-panel")
+
       $(el).amplePanels(@options.pages_options)
         .bind 'slide_horizontal', (e,d,dir) ->
           ref.load(i) if dir == 'next'
@@ -285,7 +287,9 @@

   enable_panel: (i) ->
     @log "enable_panel(#{i})"
-    $(@options.pages[i]['panel_selector']).amplePanels('enable') if @options.pages[i]['panel_selector']
+    if @options.pages[i]['panel_selector']
+      @active_panel = @options.pages[i]['panel_selector']
+      $(@options.pages[i]['panel_selector']).amplePanels('enable')

   already_loaded: (i) ->
     @log "already_loaded(#{i})"
@@ -322,21 +326,19 @@
         false

   field_events: ->
-    ref = this
-    $('textarea, input').live 'blur', ->
-      console.log '>>>>>>>>> blur'
-      ref.modal_active = false
-    $('textarea, input').live 'focus', ->
-      console.log '>>>>>>>>> focus'
-      ref.modal_active = true
+    $('textarea, input').bind 'blur', =>
+      @keys_enabled = true
+    $('textarea, input').bind 'focus', =>
+      @keys_enabled = false

   modal_events: ->
     @modal_active = false
-    ref = this
-    $(document).bind 'afterClose.facebox', ->
-      ref.modal_active = false
-    $(document).bind 'loading.facebox', ->
-      ref.modal_active = true
+    $(document).bind 'afterClose.facebox', =>
+      @keys_enabled = true
+      @modal_active = false
+    $(document).bind 'loading.facebox', =>
+      @keys_enabled = false
+      @modal_active = true

   search: ->
     @log 'search_events()'
@@ -354,17 +356,24 @@

   key_down: ->
     ref = this
-    previous = 38
-    next = 40
+    previous = 37
+    next = 39
+    up = 38
+    down = 40
     escape = 27
-    $(document).keydown (e) ->
+    $(document).keydown (e) =>
+      return unless @keys_enabled
       switch e.keyCode
         when previous
-          ref.previous()
+          $(@active_panel).amplePanels('previous')
         when next
-          ref.next()
+          $(@active_panel).amplePanels('next')
+        when up
+          @previous()
+        when down
+          @next()
         when escape
-          ref.toggle() unless ref.modal_active
+          @toggle() unless @modal_active

   tpl: (view) ->
     @tpls()[view]


Ensures that glossary is only matched when case matches

--- app/assets/javascripts/application/translation_workbench.js.coffee
+++ app/assets/javascripts/application/translation_workbench.js.coffee
@@ -305,7 +305,7 @@
   renderGlossaryTooltip: ->
     glossaryTips = []
     for term in @parent.glossary
-      if @element.find('.source-copy').text().toLowerCase().search(term[0].toLowerCase()) == 0
+      if @element.find('.source-copy').text().search(term[0]) >= 0
         glossaryTips.push("<em>" + term[0] + "</em>: " + term[1])

     if glossaryTips.length == 0


rails ujs data-confirm changes

rails ujs data-confirm changes

--- app/assets/javascripts/effective_datatables/bulk_actions.js.coffee
+++ app/assets/javascripts/effective_datatables/bulk_actions.js.coffee
@@ -41,13 +41,25 @@

   if present then $bulkActions.removeAttr('disabled') else $bulkActions.attr('disabled', 'disabled')

-#### Bulk Action link behaviour
+# rails_ujs data-confirm requires special attention
+# https://github.com/rails/rails/blob/main/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee
+$(document).on 'confirm:complete', '.dataTables_wrapper .buttons-bulk-actions a', (event) ->
+  if event.originalEvent.detail && event.originalEvent.detail[0] == true
+    doBulkActionPost(event)
+    (window.Rails || $.rails).stopEverything(event)
+    return false
+
 $(document).on 'click', '.dataTables_wrapper .buttons-bulk-actions a', (event) ->
-  event.preventDefault() # prevent the click
-  document.cookie = 'ids=; expires = Thu, 01 Jan 1970 00:00:00 GMT'
+  unless $(event.currentTarget).data('confirm')
+    doBulkActionPost(event)
+    event.preventDefault()
+
+doBulkActionPost = (event) ->
+  $bulkAction = $(event.currentTarget)  # This is the regular  tag maybe with data-confirm
+
+  document.cookie = 'ids=; expires=Thu, 01 Jan 1970 00:00:00 GMT'
   localStorage.removeItem('ids')

-  $bulkAction = $(event.currentTarget)  # This is a regular  tag
   $wrapper = $bulkAction.closest('.dataTables_wrapper')
   $table = $wrapper.find('table.dataTable').first()
   $processing = $table.siblings('.dataTables_processing').first()


Add `context.json` Korean translation

Add `context.json` Korean translation

--- def/ko/context.cson
+++ def/ko/context.cson
@@ -1,120 +1,120 @@
 Context:
   "atom-workspace":
-    "application:inspect": "Inspect Element"
+    "application:inspect": "요소 검사"
   "atom-text-editor":
-    "color-picker:open": "Color Picker"
-    "minimap:toggle": "Toggle Minimap"
+    "color-picker:open": "색상 선택기"
+    "minimap:toggle": "미니맵 토글"
   "atom-text-editor, .overlayer":
-    "core:undo": "Undo"
-    "core:redo": "Redo"
-    "core:cut": "Cut"
-    "core:copy": "Copy"
-    "core:paste": "Paste"
-    "core:delete": "Delete"
-    "core:select-all": "Select All"
-    "pane:split-up-and-copy-active-item": "Split Up"
-    "pane:split-down-and-copy-active-item": "Split Down"
-    "pane:split-left-and-copy-active-item": "Split Left"
-    "pane:split-right-and-copy-active-item": "Split Right"
-    "pane:close": "Close Pane"
+    "core:undo": "실행취소"
+    "core:redo": "다시실행"
+    "core:cut": "잘라내기"
+    "core:copy": "복사"
+    "core:paste": "붙여넣기"
+    "core:delete": "삭제"
+    "core:select-all": "전체 선택"
+    "pane:split-up-and-copy-active-item": "위로 분할"
+    "pane:split-down-and-copy-active-item": "아래로 분할"
+    "pane:split-left-and-copy-active-item": "완쪽으로 분할"
+    "pane:split-right-and-copy-active-item": "오른쪽으로 분할"
+    "pane:close": "분할 창 닫기"
   "atom-pane":
-    "pane:split-up-and-copy-active-item": "Split Up"
-    "pane:split-down-and-copy-active-item": "Split Down"
-    "pane:split-left-and-copy-active-item": "Split Left"
-    "pane:split-right-and-copy-active-item": "Split Right"
-    "pane:close": "Close Pane"
+    "pane:split-up-and-copy-active-item": "위로 분할"
+    "pane:split-down-and-copy-active-item": "아래로 분할"
+    "pane:split-left-and-copy-active-item": "완쪽으로 분할"
+    "pane:split-right-and-copy-active-item": "오른쪽으로 분할"
+    "pane:close": "분할 창 닫기"
   "atom-text-editor:not([mini])":
-    "encoding-selector:show": "Change Encoding"
-    "spell-check:correct-misspelling": "Correct Spelling"
-    "symbols-view:go-to-declaration": "Go to Declaration"
+    "encoding-selector:show": "인코딩 변경"
+    "spell-check:correct-misspelling": "맞춤법 검사"
+    "symbols-view:go-to-declaration": "정의로 이동"
   ".overlayer":
-    "grammar-selector:show": "Change Grammar"
+    "grammar-selector:show": "문법 변경"
   ".image-view":
-    "image-view:reload": "Reload Image"
+    "image-view:reload": "이미지 새로고침"
   ".tab":
-    "tabs:close-tab": "Close Tab"
-    "tabs:close-other-tabs": "Close Other Tabs"
-    "tabs:close-tabs-to-right": "Close Tabs to the Right"
-    "tabs:close-tabs-to-left": "Close Tabs to the Left"
-    "tabs:close-saved-tabs": "Close Saved Tabs"
-    "tabs:close-all-tabs": "Close All Tabs"
-    "tabs:split-up": "Split Up"
-    "tabs:split-down": "Split Down"
-    "tabs:split-left": "Split Left"
-    "tabs:split-right": "Split Right"
+    "tabs:close-tab": "탭 닫기"
+    "tabs:close-other-tabs": "다른 탭 닫기"
+    "tabs:close-tabs-to-right": "오른쪽 탭 닫기"
+    "tabs:close-tabs-to-left": "왼쪽 탭 닫기"
+    "tabs:close-saved-tabs": "저장된 탭 닫기"
+    "tabs:close-all-tabs": "모든 탭 닫기"
+    "tabs:split-up": "위로 분할"
+    "tabs:split-down": "아래로 분할"
+    "tabs:split-left": "왼쪽으로 분할"
+    "tabs:split-right": "오른쪽으로 분할"
   ".tab.texteditor":
-    "tabs:open-in-new-window": "Open In New Window"
+    "tabs:open-in-new-window": "새 창에서 열기"
   ".tab.pending-tab":
-    "tabs:keep-pending-tab": "Keep Pending Tab"
+    "tabs:keep-pending-tab": "임시 탭 유지"
   ".tab-bar":
-    "pane:reopen-closed-item": "Reopen Closed Tab"
+    "pane:reopen-closed-item": "닫힌 탭 다시 열기"
   ".tree-view.full-menu":
-    "tree-view:add-file": "New File"
-    "tree-view:add-folder": "New Folder"
-    "tree-view:move": "Rename"
-    "tree-view:duplicate": "Duplicate"
-    "tree-view:remove": "Delete"
-    "tree-view:copy": "Copy"
-    "tree-view:cut": "Cut"
-    "tree-view:paste": "Paste"
-    "tree-view:open-selected-entry-up": "Split Up"
-    "tree-view:open-selected-entry-down": "Split Down"
-    "tree-view:open-selected-entry-left": "Split Left"
-    "tree-view:open-selected-entry-right": "Split Right"
-    "application:add-project-folder": "Add Project Folder"
-    "tree-view:copy-full-path": "Copy Full Path"
-    "tree-view:copy-project-path": "Copy Project Path"
-    "tree-view:open-in-new-window": "Open In New Window"
+    "tree-view:add-file": "새 파일"
+    "tree-view:add-folder": "새 폴더"
+    "tree-view:move": "이름 변경"
+    "tree-view:duplicate": "복제"
+    "tree-view:remove": "삭제"
+    "tree-view:copy": "복사"
+    "tree-view:cut": "잘라내기"
+    "tree-view:paste": "붙여넣기"
+    "tree-view:open-selected-entry-up": "위로 분할"
+    "tree-view:open-selected-entry-down": "아래로 분할"
+    "tree-view:open-selected-entry-left": "왼쪽으로 분할"
+    "tree-view:open-selected-entry-right": "오른쪽으로 분할"
+    "application:add-project-folder": "프로젝트 폴더 추가"
+    "tree-view:copy-full-path": "전체 경로 복사"
+    "tree-view:copy-project-path": "프로젝트 경로 복사"
+    "tree-view:open-in-new-window": "새 창에서 열기"
   '.tree-view.full-menu [is="tree-view-file"]':
-    "tree-view:open-selected-entry-up": "Split Up"
-    "tree-view:open-selected-entry-down": "Split Down"
-    "tree-view:open-selected-entry-left": "Split Left"
-    "tree-view:open-selected-entry-right": "Split Right"
+    "tree-view:open-selected-entry-up": "위로 분할"
+    "tree-view:open-selected-entry-down": "아래로 분할"
+    "tree-view:open-selected-entry-left": "왼쪽으로 분할"
+    "tree-view:open-selected-entry-right": "오른쪽으로 분할"
   ".tree-view.full-menu .project-root > .header":
-    "tree-view:add-file": "New File"
-    "tree-view:add-folder": "New Folder"
-    "tree-view:move": "Rename"
-    "tree-view:duplicate": "Duplicate"
-    "tree-view:remove": "Delete"
-    "tree-view:copy": "Copy"
-    "tree-view:cut": "Cut"
-    "tree-view:paste": "Paste"
-    "tree-view:open-selected-entry-up": "Split Up"
-    "tree-view:open-selected-entry-down": "Split Down"
-    "tree-view:open-selected-entry-left": "Split Left"
-    "tree-view:open-selected-entry-right": "Split Right"
-    "application:add-project-folder": "Add Project Folder"
-    "tree-view:remove-project-folder": "Remove Project Folder"
-    "tree-view:copy-full-path": "Copy Full Path"
-    "tree-view:copy-project-path": "Copy Project Path"
-    "tree-view:open-in-new-window": "Open In New Window"
+    "tree-view:add-file": "새 파일"
+    "tree-view:add-folder": "새 폴더"
+    "tree-view:move": "이름 변경"
+    "tree-view:duplicate": "복제"
+    "tree-view:remove": "삭제"
+    "tree-view:copy": "복사"
+    "tree-view:cut": "잘라내기"
+    "tree-view:paste": "붙여넣기"
+    "tree-view:open-selected-entry-up": "위로 분할"
+    "tree-view:open-selected-entry-down": "아래로 분할"
+    "tree-view:open-selected-entry-left": "왼쪽으로 분할"
+    "tree-view:open-selected-entry-right": "오른쪽으로 분할"
+    "application:add-project-folder": "프로젝트 폴더 추가"
+    "tree-view:remove-project-folder": "프로젝트 폴더 삭제"
+    "tree-view:copy-full-path": "전체 경로 복사"
+    "tree-view:copy-project-path": "프로젝트 경로 복사"
+    "tree-view:open-in-new-window": "새 창에서 열기"
   ".platform-darwin .tree-view.full-menu":
-    "tree-view:show-in-file-manager": "Show in Finder"
+    "tree-view:show-in-file-manager": "Finder에서 열기"
   ".platform-win32 .tree-view.full-menu":
-    "tree-view:show-in-file-manager": "Show in Explorer"
+    "tree-view:show-in-file-manager": "탐색기에서 열기"
   ".platform-linux .tree-view.full-menu":
-    "tree-view:show-in-file-manager": "Show in File Manager"
+    "tree-view:show-in-file-manager": "파일 관리자에서 열기"
   ".tree-view > li.directory":
-    "project-find:show-in-current-directory": "Search in Directory"
+    "project-find:show-in-current-directory": "이 디렉토리에서 검색"
   ".tree-view.multi-select":
-    "tree-view:remove": "Delete"
-    "tree-view:copy": "Copy"
-    "tree-view:cut": "Cut"
-    "tree-view:paste": "Paste"
+    "tree-view:remove": "삭제"
+    "tree-view:copy": "복사"
+    "tree-view:cut": "잘라내기"
+    "tree-view:paste": "붙여넣기"
   "atom-pane[data-active-item-path] .item-views":
-    "tree-view:reveal-active-file": "Reveal in Tree View"
+    "tree-view:reveal-active-file": "Tree View에서 보이기"
   "atom-pane[data-active-item-path] .tab.active":
-    "tree-view:rename": "Rename"
-    "tree-view:reveal-active-file": "Reveal in Tree View"
+    "tree-view:rename": "이름 변경"
+    "tree-view:reveal-active-file": "Tree View에서 보이기"
   ".platform-darwin atom-pane[data-active-item-path] .tab.active":
-    "tree-view:show-current-file-in-file-manager": "Show In Finder"
+    "tree-view:show-current-file-in-file-manager": "Finder에서 보기"
   ".platform-win32 atom-pane[data-active-item-path] .tab.active":
-    "tree-view:show-current-file-in-file-manager": "Show In Explorer"
+    "tree-view:show-current-file-in-file-manager": "탐색기에서 보기"
   ".platform-linux atom-pane[data-active-item-path] .tab.active":
-    "tree-view:show-current-file-in-file-manager": "Show In File Manager"
+    "tree-view:show-current-file-in-file-manager": "파일 관리자에서 보기"
   ".platform-darwin atom-text-editor:not([mini])":
-    "tree-view:show-current-file-in-file-manager": "Show In Finder"
+    "tree-view:show-current-file-in-file-manager": "Finder에서 보기"
   ".platform-win32 atom-text-editor:not([mini])":
-    "tree-view:show-current-file-in-file-manager": "Show In Explorer"
+    "tree-view:show-current-file-in-file-manager": "탐색기에서 보기"
   ".platform-linux atom-text-editor:not([mini])":
-    "tree-view:show-current-file-in-file-manager": "Show In File Manager"
+    "tree-view:show-current-file-in-file-manager": "파일 관리자에서 보기"


Login: minor copy change

Login: minor copy change

--- client/Login/Views/loginform.coffee
+++ client/Login/Views/loginform.coffee
@@ -39,7 +39,7 @@
             required  : "Please enter your password."

     @button = new KDButtonView
-      title       : "SIGN ME IN"
+      title       : "SIGN IN"
       style       : "solid green"
       type        : 'submit'
       loader      :


KDView: bugfix: handle link clicks during bubble (not capture)

Conflicts:
	core/KDWindowController.coffee

--- core/KDWindowController.coffee
+++ core/KDWindowController.coffee
@@ -101,19 +101,19 @@
     # internal links (including "#") should prevent default, so we don't end
     # up with duplicate entries in history: e.g. /Activity and /Activity#
     # also so that we don't redirect the browser
-    addListener 'click', (e)->
-      isInternalLink = e.target?.nodeName.toLowerCase() is 'a' and\           # html nodenames are uppercase, so lowercase this.
-                       e.target.target?.length is 0                           # targeted links should work as normal.
-
-      if isInternalLink
-        href   = e.target.getAttribute "href"
+    (addListener 'click', (e)->
+      nearestLink = KD.utils.getNearestElementByTagName e.target, 'a'
+
+      if nearestLink?.target?.length is 0 # links with a target attribute should work as normal.
+        href   = nearestLink.getAttribute "href"
         isHttp = href?.indexOf("http") is 0
         if isHttp
-          e.target.target = "_blank"
+          nearestLink.target = "_blank"
         else
           e.preventDefault()
           if href and not /^#/.test href
             KD.getSingleton("router").handleRoute href
+    , no)

     unless location.hostname is 'localhost'
       window.addEventListener 'beforeunload', @bound "beforeUnload"


fixed mousetrap

fixed mousetrap

--- core/KDWindowController.coffee
+++ core/KDWindowController.coffee
@@ -151,7 +151,7 @@
       view.setClass "mousetrap"
       @currentCombos = superizeCombos combos
       for combo, cb of @currentCombos
-        Mousetrap.bind combo, cb
+        Mousetrap.bind combo, cb, 'keydown'

   unregisterKeyCombos:->


remove useless superclass and move isRef inside the class body

--- source/serializer.coffee
+++ source/serializer.coffee
@@ -1,18 +1,16 @@
-Mapper = require './mapper'
 Context = require './context'

-isRef = (data) ->
-  return false unless data? or data instanceof Object
+module.exports = class Serializer
+  isRef = (data) ->
+    return false unless data? or data instanceof Object

-  count = 0
-  for key of data
-    if ++count > 1 then return false
+    count = 0
+    for key of data
+      if ++count > 1 then return false

-  unless key is '$' then return false
+    unless key is '$' then return false

-  return data.$
-
-module.exports = class Serializer
+    return data.$

   constructor: (@parentContext) ->
     return (data) =>


Add current-year variable.

Add current-year variable.

--- src/project_creator.coffee
+++ src/project_creator.coffee
@@ -66,6 +66,7 @@
     values[varName] = if defaults[varName] then defaults[varName] else null
   values["target-dir"] = targetDir
   values["template-name"] = templateName
+  values["current-year"] = new Date().getFullYear()
   values

 sortValues = (values) ->


adding replies updates users modifiedat

adding replies updates users modifiedat

--- workers/social/lib/social/models/messages/post.coffee
+++ workers/social/lib/social/models/messages/post.coffee
@@ -339,66 +339,44 @@
   reply: permit 'reply to posts',
     success:(client, replyType, comment, callback)->
       {delegate} = client.connection
-      unless delegate instanceof JAccount
-        callback new Error 'Log in required!'
-      else
-        comment = new replyType body: comment
-        exempt = delegate.checkFlag('exempt')
-        if exempt
-          comment.isLowQuality = yes
-        comment
-          .sign(delegate)
-          .save (err)=>
-            if err
-              callback err
-            else
-              delegate.addContent comment, (err)->
-                if err
-                  log 'error adding content to delegate with err', err
-              @addComment comment,
-                flags:
-                  isLowQuality    : exempt
-              , (err, docs)=>
-                if err
-                  callback err
-                else
-                  if exempt
-                    callback null, comment
-                  else
-                    Relationship.count {
-                      sourceId                    : @getId()
-                      as                          : 'reply'
-                      'data.flags.isLowQuality'   : $ne: yes
-                    }, (err, count)=>
-                      if err
-                        callback err
-                      else
-                        @update $set: repliesCount: count, (err)=>
-                          if err
-                            callback err
-                          else
-                            callback null, comment
-                            @fetchActivityId (err, id)->
-                              CActivity.update {_id: id}, {
-                                $set: 'sorts.repliesCount': count
-                              }, (err)-> log err if err
-                            @fetchOrigin (err, origin)=>
-                              if err
-                                console.log "Couldn't fetch the origin"
-                              else
-                                unless exempt
-                                  @emit 'ReplyIsAdded', {
-                                    origin
-                                    subject       : ObjectRef(@).data
-                                    actorType     : 'replier'
-                                    actionType    : 'reply'
-                                    replier       : ObjectRef(delegate).data
-                                    reply         : ObjectRef(comment).data
-                                    repliesCount  : count
-                                    relationship  : docs[0]
-                                  }
-                                @follow client, emitActivity: no, (err)->
-                                @addParticipant delegate, 'commenter', (err)-> #TODO: what should we do with this error?
+      exempt = delegate.checkFlag('exempt')
+      comment = new replyType body: comment
+      comment.sign(delegate).save (err)=>
+        return callback err if err
+
+        delegate.addContent comment, (err)-> log 'error adding content to delegate with err', err if err
+
+        delegate.update $set: 'meta.modifiedAt': new Date, ->
+
+        @addComment comment, flags: {isLowQuality: exempt}, (err, docs)=>
+          return callback err if err
+          return callback null, comment if exempt
+
+          Relationship.count {sourceId: @getId(),as:'reply'}, (err, count)=>
+            return callback err if err
+            @update $set: repliesCount: count, (err)=>
+              return callback err if err
+              callback null, comment
+
+              @fetchActivityId (err, id)-> CActivity.update {_id: id}, {
+                  $set: 'sorts.repliesCount': count
+                }, (err)-> log err if err
+
+              @fetchOrigin (err, origin)=>
+                return console.log "Couldn't fetch the origin" if err
+                unless exempt
+                  @emit 'ReplyIsAdded', {
+                    origin
+                    subject       : ObjectRef(@).data
+                    actorType     : 'replier'
+                    actionType    : 'reply'
+                    replier       : ObjectRef(delegate).data
+                    reply         : ObjectRef(comment).data
+                    repliesCount  : count
+                    relationship  : docs[0]
+                  }
+                @follow client, emitActivity: no, (err)->
+                @addParticipant delegate, 'commenter', (err)-> #TODO: what should we do with this error?

   # TODO: the following is not well-factored.  It is not abstract enough to belong to "Post".
   # for the sake of expedience, I'll leave it as-is for the time being.


Close dropdown menu after after clicking in any field

--- dist/DropDown.coffee
+++ dist/DropDown.coffee
@@ -1,5 +1,6 @@
-Holder =(Backbone, MixinBackbone)->
+Holder = (Backbone, MixinBackbone)->
   View = MixinBackbone Backbone.View
+  $ = Backbone.$

 #------------------- model ---------------------------#
   DropDownModel = Backbone.Model.extend
@@ -43,22 +44,23 @@
       @updateActive()

     onClick: ->
-      @model.set active:true
+      @model.set active: true

 #------------------- list ----------------------------#
   DropDownList = View.extend
+    OPEN_CLASS: "open"
     className: "dropdown_list"
     itemView: DropDownItem

     templateFunc: ->
       "
-        <button class=dropdown-list-button type='button' data-js-button></button>
-

+        <button class=dropdown-list-button type='button'></button>
+

       "

     ui:
-      menu: "[data-js-menu]"
-      button: "[data-js-button]"
+      menu: ".dropdown-list-menu"
+      button: ".dropdown-list-button"

     events:
       "click": "onClick"
@@ -70,7 +72,15 @@
         "add": @onAddCollection
         "remove": @onRemoveCollection
       @currentActiveModel = null
+      @isMenuOpen = false
       @views = {}
+      @__onBackdropClick = (e)=> @onBackdropClick(e)
+
+    onShow:->
+      $(window).on "click", @__onBackdropClick
+
+    onClose:->
+      $(window).off "click", @__onBackdropClick

     bindToInput: (@$input)->

@@ -81,8 +91,19 @@
       @collection.remove @collection.models
       @collection.add data

-    onClick: ->
-      @$el.toggleClass "open"
+    onClick: (e)->
+      if @isMenuOpen
+        @$el.removeClass @OPEN_CLASS
+        @isMenuOpen = false
+      else
+        @$el.addClass @OPEN_CLASS
+        @isMenuOpen = true
+
+    onBackdropClick: (e)->
+      isListClick = $(e.target).parents(".#{@className}")[0] is @el
+      return if isListClick or not @isMenuOpen
+      @$el.removeClass @OPEN_CLASS
+      @isMenuOpen = false

     onAddCollection: (model)->
       @views[model.cid] = itemView = new @itemView {model}
@@ -109,4 +130,4 @@
   ], (Backbone, MixinBackbone)->
     Holder Backbone, MixinBackbone
 else
-  window.DropDown = Holder(Backbone, MixinBackbone)+  window.DropDown = Holder Backbone, MixinBackbone


Much cleaner server-side DOM handling

Much cleaner server-side DOM handling

--- nodejs/palsu.coffee
+++ nodejs/palsu.coffee
@@ -1,10 +1,10 @@
 express = require 'express'
 io = require 'socket.io'
-Backbone = require 'backbone'
 VIE = require '../js/vie.js'
 require './vie-redis.coffee'
 fs = require 'fs'
 jQuery = require 'jquery'
+jsdom = require 'jsdom'

 server = express.createServer()
 server.configure ->
@@ -23,9 +23,11 @@
 # Serve the index file for /
 server.get '/meetings', (request, response) ->
     return fs.readFile "#{process.cwd()}/meetings.html", "utf-8", (err, data) ->
-        html = jQuery data
+        document = jsdom.jsdom data
+        window = document.createWindow()
+        jQ = jQuery.create window
         # Find RDFa entities and load them
-        VIE.RDFaEntities.getInstances html
+        VIE.RDFaEntities.getInstances jQ "*"
         # Get the Calendar object
         calendar = VIE.EntityManager.getBySubject '/meetings'

@@ -36,8 +38,7 @@
         return events.fetch
             success: (eventCollection) ->
                 VIE.cleanup()
-                data = data.replace(data.substring(data.indexOf('<body'),data.lastIndexOf('</body')).replace(/^[^>]+>/,''), jQuery('body', html).html())
-                return response.send data
+                return response.send window.document.innerHTML

 server.get '/foo', (req, resp) ->
     html = jQuery('html')


delete a comment that doesn't make sense where it is located.

--- src/smc-hub/sign-in.coffee
+++ src/smc-hub/sign-in.coffee
@@ -262,10 +262,6 @@
     async.series([
         (cb) ->
             dbg("get account and check credentials")
-            # NOTE: Despite people complaining, we do give away info about whether
-            # the e-mail address is for a valid user or not.
-            # There is no security in not doing this, since the same information
-            # can be determined via the invite collaborators feature.
             opts.database.get_auth_token_account_id
                 auth_token : mesg.auth_token
                 cb         : (err, _account_id) ->


Refactor my own knowledge on vanilla DOM manipulation

Gosh I didn't remember we could just use document.body instead of
document.getElementsByTagName('body')[0]! -_-'

--- app/application.coffee
+++ app/application.coffee
@@ -12,7 +12,7 @@
   init: ->
     tmpl = require 'views/list'
     html = tmpl items: App.items
-    document.getElementsByTagName('body')[0].innerHTML += html
+    document.body.innerHTML += html
     return

 module.exports = App


Compound actions

Compound actions

Define compound actions that trigger a list of other actions. Example:

actions:
    mouseScrollUp:
        type: 'mouse'
        action: 'scroll'
        direction: 'up'
        magnitude: 20
    mouseScrollDown:
        type: 'mouse'
        action: 'scroll'
        direction: 'down'
        magnitude: 20
    keyboardButtonADown:
        type: 'keyboard'
        action: 'down'
        button: 'a'
    keyboardButtonAUp:
        type: 'keyboard'
        action: 'up'
        button: 'a'
    mouseMove:
        type: 'mouse'
        action: 'move'
    toggleFreeze:
        type: 'mouse'
        action: 'toggleFreeze'
        feedback:
            audio: 'asset/audio/mallet.ogg'
    keyboardButtonA:
        type: 'keyboard'
        action: 'tap'
        button: 'a'
    keyboardButtonB:
        type: 'keyboard'
        action: 'tap'
        button: 'b'
    keyboardButtonC:
        type: 'keyboard'
        action: 'tap'
        button: 'c'
    compoundActionTest:
        type: 'compound'
        actions:
         - 'keyboardButtonA'
         - 'keyboardButtonB'
         - 'keyboardButtonC'
        feedback:
            audio: 'asset/audio/mallet.ogg'

--- client/src/ActionController.coffee
+++ client/src/ActionController.coffee
@@ -99,6 +99,8 @@
         if(cmd.type == 'keyboard')
             if(cmd.action in ['up', 'down', 'tap'])
                 @keyboard cmd.action, cmd.button
+        if(cmd.type == 'compound')
+            @executeAction action for action in cmd.actions

     activateRecipe: (recipeName) =>
         recipe = @recipes[recipeName]


Moved template definition

Moved template definition

--- core/app/backbone/views/sub_comments/sub_comments_list_view.coffee
+++ core/app/backbone/views/sub_comments/sub_comments_list_view.coffee
@@ -1,7 +1,5 @@
 class BaseSubCommentsListView extends Backbone.Marionette.CompositeView
   itemViewContainer: '.js-region-sub-comments-collection'
-
-  template: 'sub_comments/sub_comments_list'

   initialize: ->
     @collection.fetch()
@@ -18,6 +16,8 @@
   className: 'evidence-sub-comments-list'
   itemView: SubCommentView

+  template: 'sub_comments/sub_comments_list'
+
   subCommentsAddView: ->
     @_subCommentsAddView ?= new SubCommentsAddView(addToCollection: @collection)


Fix up comment prefixes in the AST.

Fix up comment prefixes in the AST.

--- js2coffee.coffee
+++ js2coffee.coffee
@@ -36,6 +36,7 @@
 # All options are optional. Available options are:
 #
 # ~ filename (String): the filename, used in source maps and errors.
+# ~ comments (Boolean): set to `false` to disable comments.
 ###

 js2coffee.build = (source, options = {}) ->
@@ -57,7 +58,7 @@

 js2coffee.transform = (ast, options = {}) ->
   FunctionTransforms.run(ast, options)
-  CommentTransforms.run(ast, options)
+  CommentTransforms.run(ast, options) unless options.comments is false
   OtherTransforms.run(ast, options)

 js2coffee.codegen = (ast, options = {}) ->
@@ -256,6 +257,9 @@
   SwitchCase: (node) ->
     @injectComments(node, 'consequent')

+  Block: (node) ->
+    @convertCommentPrefixes(node)
+
   ###
   # injectComments():
   # Injects comment nodes into a node list.
@@ -264,6 +268,13 @@
   injectComments: (node, key) ->
     node[key] = @addCommentsToList(node.range, node[key])
     node
+
+  ###
+  # addCommentsToList():
+  # Checks out the `@comments` list for any relevants comments, and injects
+  # them into the correct places in the given `body` Array. Returns the
+  # transformed `body` array.
+  ###

   addCommentsToList: (range, body) ->
     return body unless range?
@@ -284,6 +295,28 @@
       if item.range
         left = item.range[1]
     list
+
+  ###
+  # convertCommentPrefixes():
+  # Changes JS block comments into CoffeeScript block comments.
+  # This involves changing prefixes like `*` into `#`.
+  ###
+
+  convertCommentPrefixes: (node) ->
+    lines = node.value.split("\n")
+    lines = lines.map (line, i) ->
+      isTrailingSpace = i is lines.length-1 and line.match(/^\s*$/)
+      isSingleLine = i is 0 and lines.length is 1
+
+      if isTrailingSpace
+        ''
+      else if isSingleLine
+        line
+      else
+        line = line.replace(/^ \*/, '#')
+        line + "\n"
+    node.value = lines.join("")
+    node

 # ----------------------------------------------------------------------------

@@ -807,19 +840,7 @@

   # Block comments
   Block: (node) ->
-    lines = node.value.split("\n")
-    lines = lines.map (line, i) ->
-      isTrailingSpace = i is lines.length-1 and line.match(/^\s*$/)
-      isSingleLine = i is 0 and lines.length is 1
-
-      if isTrailingSpace
-        ''
-      else if isSingleLine
-        line
-      else
-        line = line.replace(/^ \*/, '#')
-        line + "\n"
-    [ "###", lines, "###\n" ]
+    [ "###", node.value, "###\n" ]

   ReturnStatement: (node) ->
     if node.argument


Don't display the score list header when there is only 1 score

--- resources/assets/coffee/react/beatmapset-page/scoreboard.coffee
+++ resources/assets/coffee/react/beatmapset-page/scoreboard.coffee
@@ -42,12 +42,13 @@
       el BeatmapSetPage.ScoreboardFirst,
         score: @props.scores[0]

-      div className: 'beatmapset-scoreboard__row',
-        header.map (m) =>
-          className = 'beatmapset-scoreboard__row-item beatmapset-scoreboard__row-item--header'
-          className += " beatmapset-scoreboard__row-item--#{m}"
+      if @props.scores.length > 1
+        div className: 'beatmapset-scoreboard__row',
+          header.map (m) =>
+            className = 'beatmapset-scoreboard__row-item beatmapset-scoreboard__row-item--header'
+            className += " beatmapset-scoreboard__row-item--#{m}"

-          span className: className, key: m, Lang.get "beatmaps.beatmapset.show.extra.scoreboard.list.#{m}"
+            span className: className, key: m, Lang.get "beatmaps.beatmapset.show.extra.scoreboard.list.#{m}"

       @props.scores[1..].map (s, i) =>
         el BeatmapSetPage.ScoreboardItem, score: s, position: i + 2, key: i


factor out title method; add copyright

factor out title method; add copyright

--- intervals.coffee
+++ intervals.coffee
@@ -235,6 +235,20 @@
   ctx.fillStyle = 'white'
   ctx.fillRect 0, 0, canvas.width, canvas.height

+draw_title = (text, {font, x, y, gravity}) ->
+  gravity ||= ''
+  ctx.font = font if font
+  m = ctx.measureText(text)
+  # x -= m.width / 2
+  y -= m.emHeightDescent if gravity.match(/^bottom$/)
+  y += m.emHeightAscent if gravity.match(/^top|topLeft|topRight$/)
+  ctx.fillText text, x, y
+
+with_context = (ctx, fn) ->
+  ctx.save()
+  fn()
+  ctx.restore()
+
 filename = (name) -> DefaultFilename = name

 save_canvas_to_png = (canvas, fname) ->
@@ -253,10 +267,19 @@
   ctx = canvas.getContext('2d')
   ctx.textDrawingMode = 'glyph' if pdf
   erase_background()
+
   ctx.save()
   ctx.translate page_margin, page_margin
   draw_page ctx
   ctx.restore()
+
+  ctx.fillStyle = 'black'
+  license = "©2013 by Oliver Steele. "
+  license += "This work is licensed under a Creative Commons Attribution 3.0 United States License."
+  draw_title license
+  , font: "4pt Times", x: page_margin, y: canvas.height
+  , gravity: 'bottom'
+
   unless pdf
     filename = "#{DefaultFilename or 'test'}.png"
     fs.writeFile BuildDirectory + filename, canvas.toBuffer()
@@ -550,12 +573,6 @@
 chord_page = (chord, options) ->
   {best_fingering} = options || {}

-  header_height = 50
-  diagram_title_height = 30
-  if draw_diagrams
-    diagram_title_height = 35
-    diagram_gutter = 10
-
   pitch_fingers = []
   finger_positions_each (finger_position) ->
     pitch = pitch_number_for_position(finger_position) % 12
@@ -564,19 +581,21 @@
   degree_colors = ['red', 'blue', 'green', 'orange']
   other_colors = ['rgba(255,0,0 ,.1)', 'rgba(0,0,255, 0.1)', 'rgba(0,255,0, 0.1)', 'rgba(255,0,255, 0.1)']

-  grid cols: 3, rows: 4
+  grid cols: 4, rows: 3
   , cell_width: padded_fretboard_width
   , cell_height: padded_fretboard_height
   , gutter_height: 20
-  , header_height: diagram_title_height
+  , header_height: 40
   , (cell) ->

-    ctx.font = '20px Impact'
     ctx.fillStyle = 'rgb(128, 128, 128)'
-    ctx.fillText "#{chord.name} Chords", diagram_gutter / 2, header_height / 2 - 10
+    draw_title "#{chord.name} Chords", font: '20px Impact'
+    , x: 0
+    , y: 0
+    , gravity: 'top'

     ctx.save()
-    ctx.translate 215, 15
+    ctx.translate 285, 20
     ctx.scale 0.85,0.85
     draw_pitch_diagram chord.pitch_classes, degree_colors
     ctx.restore()


add convertPointToLayer funtion on layer

--- framer/Layer.coffee
+++ framer/Layer.coffee
@@ -412,6 +412,9 @@
 	convertPointToCanvas: (point) ->
 		return Utils.convertPointToContext(point, @, true)

+	convertPointToLayer: (point, layer) =>
+		return Utils.convertPoint(point, @, layer, true)
+
 	@define "canvasFrame",
 		importable: true
 		exportable: false


Add 'complete' method in CompletionClosure

--- app/src/utils/completion_closure.coffee
+++ app/src/utils/completion_closure.coffee
@@ -37,7 +37,7 @@
     Completes the closure with an error. This method will call the onComplete function if possible or keep the error until
     a callback is submitted.

-    @param [Any] error A error to failed the closure
+    @param [Any] error An error to failed the closure
     @return [CompletionClosure] self
     @throw If the closure is already completed
   ###
@@ -46,6 +46,22 @@
     @_isFailed = yes
     @_complete = [null, error]
     @_tryNotify()
+    @
+
+  ###
+    Completes the closure either by a success or an error. If both error and result are null, the closure will be failed
+    with an 'Unknown Error'.
+
+    @param [Any] value A value to complete the closure (may be null)
+    @param [Any] error An error to failed the closure (may be null)
+    @return [CompletionClosure] self
+    @throw If the closure is already completed
+  ###
+  complete: (value, error) ->
+    if value?
+      @success(value)
+    else
+      @fail(if error? then error else "Unknown Error")
     @

   ###


Added first trivial test for empty string

--- coffee/spec/calculator.spec.coffee
+++ coffee/spec/calculator.spec.coffee
@@ -3,3 +3,8 @@
 describe 'calculator', ->
   it 'should create a new calculator', ->
     calculator = new calc.Calculator
+
+  it 'should return 0 for an empty string', ->
+    calculator = new calc.Calculator
+    expect(calculator.add '').toEqual 0
+


Changes to underscore & caret patterns

Changes to underscore & caret patterns

--- grammars/latex.cson
+++ grammars/latex.cson
@@ -84,27 +84,33 @@
     match: '(})'
   }
   metaUnderscore: {
-    comment: 'Underscore means subscript. It will take an argument in brackets if possible. If not, just the single next character. Only valid when within mathmode. Will address this later.'
-    # patterns: [
-    #   { include: '#underscoreWithArgument' }
-    #   { include: '#underscoreNoArgument' }
-    # ]
-    # name: 'punctuation.subscript.latex'
-    name: 'support.function.latex'
-    begin: '\\_'
-    end: '(?<=[^\\_])'
-    patterns: [
-      {
-        include: '$self'
-      }
+    comment: 'Underscore means subscript. It will take an argument in brackets if possible. If not, just the single next character. Only valid when within mathmode. May address this later.'
+    name: 'markup.other.subscript.latex'
+    begin: '(\\_)\\s*'
+    beginCaptures:
+      1:
+        name: 'punctuation.subscript.latex'
+    end: '(?<=[^\\_\\s])'
+    patterns: [
+      { include: '$self' }
     ]
   }
   metaCaret: {
     comment: 'Like underscore, but superscript'
     patterns: [
       { include: '#doubleCaret' }
-      { include: '#caretWithArgument' }
-      { include: '#caretNoArgument' }
+      {
+        comment: 'Matches a caret and following group'
+        name: 'markup.other.superscript.latex'
+        begin: '(\\^)\\s*'
+        beginCaptures:
+          1:
+            name: 'punctuation.superscript.latex'
+        end: '(?<=[^\\^\\s])'
+        patterns: [
+          { include: '$self' }
+        ]
+      }
     ]
   }
   metaHashtag: {
@@ -655,34 +661,6 @@
     name: 'keyword.control.latex'
     match: '\\\\item'
   }
-  underscoreWithArgument: {
-    comment: 'Matches an underscore and between curly brackets'
-    name: 'punctuation.subscript.latex'
-    begin: '_\\s*(?={)'
-    end: '(?<=\\})'
-    patterns: [
-      { include: '#argumentScope' }
-    ]
-  }
-  underscoreNoArgument: {
-    comment: 'Matches an underscore and next character or macro. Considering making this give a warning, as brackets is best practice'
-    name: 'punctuation.subscript.latex'
-    match: '_\\s*(\\\\[a-zA-Z]+|\\\\[.]|.)?'
-  }
-  caretWithArgument: {
-    comment: 'Matches a caret and between curly brackets'
-    name: 'punctuation.superscript.latex'
-    begin: '\\^\\s*(?={)'
-    end: '(?<=\\})'
-    patterns: [
-      { include: '#argumentScope' }
-    ]
-  }
-  caretNoArgument: {
-    comment: 'Matches a caret and next character or macro. Considering making this give a warning, as brackets is best practice'
-    name: 'punctuation.superscript.latex'
-    match: '\\^\\s*(\\\\[a-zA-Z]+|\\\\[.]|.)?'
-  }

   # Experimental
   unitMeasure: {
@@ -724,3 +702,34 @@
     name: 'support.function.general.latex'
     match: '\\\\[a-zA-Z@]+'
   }
+underscoreWithArgument: {
+  comment: 'Matches an underscore and between curly brackets'
+  name: 'punctuation.subscript.latex'
+  begin: '_\\s*(?={)'
+  end: '(?<=\\})'
+  patterns: [
+    { include: '#argumentScope' }
+  ]
+}
+underscoreNoArgument: {
+  comment: 'Matches an underscore and next character or macro. Considering making this give a warning, as brackets is best practice'
+  name: 'punctuation.subscript.latex'
+  match: '_\\s*(\\\\[a-zA-Z]+|\\\\[.]|.)?'
+}
+caretWithArgument: {
+  comment: 'Matches a caret and between curly brackets'
+  name: 'markup.other.superscript.latex'
+  begin: '\\^\\s*'
+  beginCaptures:
+    0:
+      name: 'punctuation.superscript.latex'
+  end: '(?<=[^\\^])'
+  patterns: [
+    { include: '$self' }
+  ]
+}
+caretNoArgument: {
+  comment: 'Matches a caret and next character or macro. Considering making this give a warning, as brackets is best practice'
+  name: 'punctuation.superscript.latex'
+  match: '\\^\\s*(\\\\[a-zA-Z]+|\\\\[.]|.)?'
+}


Added missing marko attributes and tags

Added missing marko attributes and tags

--- grammars/marko.cson
+++ grammars/marko.cson
@@ -254,7 +254,7 @@
     ]
   }
   {
-    'begin': '(</?)((?i:for|if|var|require|def|invoke|include|with)\\b)'
+    'begin': '(</?)((?i:for|if|else|else-if|var|require|def|invoke|include|with)\\b)'
     'captures':
       '1':
         'name': 'punctuation.definition.tag.html'
@@ -470,7 +470,7 @@
     'match': '(?<=[^=])\\b([a-zA-Z0-9:-]+)'
     'name': 'entity.other.attribute-name.html'
   'tag-marko-attribute':
-    'match': '(?<=[^=])\\b(if|for|else|attrs)'
+    'match': '(?<=[^=])\\b(if|for|else|else-if|attrs)'
     'name': 'support.function.marko_attribute'
   'tag-id-attribute':
     'begin': '\\b(id)\\b\\s*(=)'


fix: improve syntax highlighting

fix: improve syntax highlighting


Slight optimization

Slight optimization

--- grunt/quidoc.coffee
+++ grunt/quidoc.coffee
@@ -50,13 +50,11 @@
   # Remove the comment indicator (e.g., "*") on interior block comment lines.
   commentText: ( commentBlock ) ->
     text = ""
-    match = @regexes.commentText.exec commentBlock
-    while match != null
-      [ full, lineText ] = match
-      if text.length > 0
+    commentLines = @_matches @regexes.commentText, commentBlock
+    for commentLine in commentLines
+      if commentLine.length > 0
         text += "\n"
-      text += lineText
-      match = @regexes.commentText.exec commentBlock
+      text += commentLine
     text

   # Return the text used to define the control's content and other property


use xml output instead of localized svninfo

--- tasks/svninfo.coffee
+++ tasks/svninfo.coffee
@@ -5,7 +5,7 @@
 # * Copyright (c) 2013 liqweed
 # * Licensed under the MIT license.
 #
-#
+#
 # require("child_process").spawn("svn",["info"],{cwd:"./test/fixtures/svninfo"}).stdout.on('data',function(data){ console.log(data.toString()); });

 "use strict"
@@ -13,7 +13,7 @@

   # Please see the Grunt documentation for more information regarding task
   # creation: http://gruntjs.com/creating-tasks
-  grunt.registerTask 'svninfo', 'Get Subversion info from a working copy and populate grunt.config with the data', (output, argsKey) ->
+  grunt.registerTask 'svninfo','Get Subversion info from a working copy and populate grunt.config with the data', (output, argsKey) ->
     done = @async()
     options = @options
       cwd: '.'
@@ -25,31 +25,34 @@

     grunt.util.spawn
       cmd: 'svn'
-      args: if args then ['info'].concat(args) else ['info']
+      args: if args then ['info', '--xml'].concat(args) else ['info', '--xml']
       opts: options
     , (err, result) ->
       if err
         grunt.log.warn err
         return done()
       info = {}
-      # Split SVN info by lines:
-      result.stdout.split('\n').forEach (line) ->
-        lineParts =line.split(': ')
-        if lineParts.length == 2
-          # Populate info object
-          info[lineParts[0]] = lineParts[1].trim()
+
+      # Extract xml info with RegExp
+      revRE = /<entry[^>]+revision="([^"]*)"/
+      urlRE = /<url>([^<]*)<\/url>/
+      lastRevRE = /<commit[^>]+revision="([^"]*)"/
+      lastAuthorRE = /<author>([^<]*)<\/author>/
+      lastDateRE = /<date>([^<]*)<\/date>/
+      repoRootRE = /<root>([^<]*)<\/root>/
+      repoIdRE = /<uuid>([^<]*)<\/uuid>/

       # Populate grunt.config with nicely parsed object:
       grunt.config.set options.output,
-        rev: info['Revision']
-        url: info['URL']
+        rev: revRE.exec(result.stdout)[1]
+        url: urlRE.exec(result.stdout)[1]
         last:
-          rev: info['Last Changed Rev']
-          author: info['Last Changed Author']
-          date: info['Last Changed Date']
+          rev: lastRevRE.exec(result.stdout)[1]
+          author: lastAuthorRE.exec(result.stdout)[1]
+          date: lastDateRE.exec(result.stdout)[1]
         repository:
-          root: info['Repository Root']
-          id: info['Repository UUID']
+          root: repoRootRE.exec(result.stdout)[1]
+          id: repoIdRE.exec(result.stdout)[1]

       grunt.log.writeln "SVN info fetched (rev: #{grunt.config.get(options.output + '.rev')})"
       done()


pager_admin not work

pager_admin not work

--- test/pagerv2_commands_test.coffee
+++ test/pagerv2_commands_test.coffee
@@ -2147,6 +2147,13 @@
             .to.eql 'Sorry, I can\'t figure toto email address. ' +
                     'Can you help me with `.pager toto as <email>`?'

+        context 'pager who is toto', ->
+          hubot 'pager who is toto', 'pager_admin'
+          it 'returns information from pager', ->
+            expect(hubotResponse())
+            .to.eql 'Sorry, I can\'t figure toto email address. ' +
+                    'Can you help me with `.pager toto as <email>`?'
+

 # --------------------------------------------------------------------------------------------------


intercept mobile link clicks so we do not go to new tab, closes #8

--- components/layout/body/view.coffee
+++ components/layout/body/view.coffee
@@ -13,6 +13,7 @@
     'click a[data-disabled]'                      : 'disable'
     'click a[data-client]:not([data-disabled])'   : 'intercept'
     'click span[data-client]:not([data-disabled])': 'intercept'
+    'click a'                                     : 'maybeIntercept'
     'tap #scroll-top'                             : 'scrollToTop'

@@ -48,6 +49,16 @@

   bodyClick: (e) -> mediator.trigger 'body:click', e

+  maybeIntercept: (e)->
+    return unless $('body').hasClass 'is-mobile'
+
+    href = $(e.currentTarget).attr("href")
+
+    if href.indexOf(location.hostname) > -1 and href isnt "#" and $(e.currentTarget).attr("target") isnt "_blank"
+      e.preventDefault()
+      e.stopImmediatePropagation()
+      window.location = href
+
   scrollToTop: (e) ->
     e.preventDefault()
     e.stopPropagation()


Remove trailing whitespace to fix linting

--- src/hooks-worker-client.coffee
+++ src/hooks-worker-client.coffee
@@ -139,7 +139,7 @@
       unless which.which @handlerCommand
         msg = '''\
           Go hooks handler server command not found in $GOPATH/bin
-          Install go hooks handler by running:
+          Install go hooks handler by running:
           $ go get github.com/snikch/goodman
           $ cd $GOPATH/src/github.com/snikch/goodman
           $ go build -o $GOPATH/bin/goodman github.com/snikch/goodman/bin


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/animated.coffee
+++ test/animated.coffee
@@ -427,10 +427,10 @@
     data: animationData
     includedModules: ["Animated"]

-  # Bite cannot be interrupted. Hack to get to idle state
-  animation.I.activeAnimation.frames.length.times -> animation.update()
-
-  equals animation.I.activeAnimation.name, "Idle1", "Animation should be in idle1 state after bite finishes"
+  # Bite cannot be interrupted. Must force transition
+  animation.transition("Idle1", true)
+
+  equals animation.I.activeAnimation.name, "Idle1", "Animation should be in idle1 state forced transition from bite"

   animation.transition("Idle2")

@@ -481,8 +481,7 @@
     data: animationData
     includedModules: ["Animated"]

-  animation.I.activeAnimation.frames.length.times ->
-    animation.update()
+  animation.transition("Idle1", true)

   30.times -> animation.update()


add test for custom jsonp callback

add test for custom jsonp callback

--- test/app_test.coffee
+++ test/app_test.coffee
@@ -59,6 +59,15 @@
       res.status.should.equal 200
       callback user, data, get, post

+# Verify JSONP Helper - Given a JSONP response, invokes callback with json.
+verifyJSONP = (res, callback) ->
+  res.type.should.equal 'text/javascript'
+  res.text.should.exist
+  jsonp = res.text
+  jsonpCallback = (json) ->
+    callback(json)
+  eval jsonp
+
 # Superagent helpers.
 agent = superagent.agent()
 get = getFor agent
@@ -288,15 +297,6 @@
           sha1: prop.sha1
           text: prop.text

-    # Verify JSONP Helper - Given a JSONP response, invokes callback with json.
-    verifyJSONP = (res, callback) ->
-      res.type.should.equal 'text/javascript'
-      res.text.should.exist
-      jsonp = res.text
-      jsonpCallback = (json) ->
-        callback(json)
-      eval jsonp
-
     describe 'GET /arguments/:sha1/propositions.:format?', ->
       it "should return argument propositions as json", (done) ->
         sessionWithArgument (user, argument, get, post) ->
@@ -382,9 +382,7 @@
           get "/#{user.username}/#{argument.repo}.jsonp", (err, res) ->
             res.status.should.equal 200
             res.redirects.should.eql []
-            res.type.should.equal 'text/javascript'
-            jsonp = res.text
-            jsonpCallback = (json) ->
+            verifyJSONP res, (json) ->
               should.not.exist json.error
               json.user.should.include {username: user.username}
               json.repo.should.eql argument.repo
@@ -393,4 +391,22 @@
                 target_sha1: argument.sha1
               json.argument.should.eql argument
               done()
+
+    describe 'GET /:name/:repo.:format?callback=:cbName', ->
+      it 'should show a repo as jsonp with custom callback', (done) ->
+        sessionWithArgument (user, argument, get, post) ->
+          get "/#{user.username}/#{argument.repo}.jsonp?callback=myCb", (err, res) ->
+            res.status.should.equal 200
+            res.redirects.should.eql []
+            res.type.should.equal 'text/javascript'
+            jsonp = res.text
+            myCb = (json) ->
+              should.not.exist json.error
+              json.user.should.include {username: user.username}
+              json.repo.should.eql argument.repo
+              json.commit.should.include
+                target_type: 'argument'
+                target_sha1: argument.sha1
+              json.argument.should.eql argument
+              done()
             eval jsonp


🔧 Fix tests for Node.js 6.6

🔧 Fix tests for Node.js 6.6

--- test/cli-test.coffee
+++ test/cli-test.coffee
@@ -41,9 +41,7 @@
     if error
       exitStatus = error.code

-  exitEventName = if process.version.split('.')[1] is '6' then 'exit' else 'close'
-
-  cli.on exitEventName, (code) ->
+  cli.on 'close', (code) ->
     exitStatus = code if exitStatus == null and code != undefined
     callback()


cooldown finilazation

cooldown finilazation

--- test/cooldown.coffee
+++ test/cooldown.coffee
@@ -100,6 +100,7 @@

 # TODO: tests to make sure value isn't overwritten if the instance variable already exists
 # TODO: tests to make sure value is defaulted to 0, but only if the instance variable doesn't exist
-test "initialize property to 0 if no current value", -
+test "initialize property to 0 if no current value", ->
+

 module()


cooldown finilazation

cooldown finilazation

--- test/cooldown.coffee
+++ test/cooldown.coffee
@@ -76,7 +76,7 @@
   3.times ->
     obj.update()

-  equals obj.I.health, 0, "health cooldown should exist and equal 0"
+  equals obj.I.health, 97, "health cooldown should exist and equal 0"

   obj.cooldown 'turbo',
     target: 5


Adds id to test

Adds id to test

--- components/article/test/view.coffee
+++ components/article/test/view.coffee
@@ -132,7 +132,7 @@

       Backbone.sync
         .onCall 0
-        .yieldsTo 'success', article = fabricate 'article', { thumbnail_title: 'Callout title' }
+        .yieldsTo 'success', article = fabricate 'article', { thumbnail_title: 'Callout title', id: '123'}
         .returns Q.resolve(article)

       @view.renderCalloutSections()


integrate with callback-mac

integrate with callback-mac

--- app/assets/javascripts/app/controllers/nav.js.coffee
+++ app/assets/javascripts/app/controllers/nav.js.coffee
@@ -15,4 +15,5 @@

   renderCount: =>
     count = Conversation.unreadCount()
-    @messages.attr('data-count', count or '')    +    @messages.attr('data-count', count or '')
+    callback?.dock.badge = count


Improved App.Utils.signatureIdentify() again, ignore messages with less then 8 lines and less then 300 chars.

--- app/assets/javascripts/app/lib/app_post/utils.coffee
+++ app/assets/javascripts/app/lib/app_post/utils.coffee
@@ -250,11 +250,10 @@
   @signatureIdentify: (message, test = false) ->
     textToSearch = @html2text(message)

-    # count lines, if we do have lower the 8 or more then 300 chars ignore this
+    # if we do have less then 8 lines and less then 300 chars ignore this
     textToSearchInLines = textToSearch.split("\n")
     if !test
-      return message if textToSearch.length < 300
-      return message if textToSearchInLines.length < 8
+      return message if textToSearchInLines.length < 8 && textToSearch.length < 300

     quote = (str) ->
       (str + '').replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&")


reduced some logic around @visible

reduced some logic around @visible

--- app/assets/javascripts/mercury/page_editor.js.coffee
+++ app/assets/javascripts/mercury/page_editor.js.coffee
@@ -8,9 +8,8 @@
   constructor: (@saveUrl = null, @options = {}) ->
     throw Mercury.I18n('Mercury.PageEditor can only be instantiated once.') if window.mercuryInstance

-    @options.visible = true unless (@options.visible == false || @options.visible == 'no')
+    @visible = true unless (@options.visible == false || @options.visible == 'no')
     @options.saveDataType = 'json' unless (@options.saveDataType == false || @options.saveDataType)
-    @visible = @options.visible

     window.mercuryInstance = @
     @regions = []
@@ -71,7 +70,7 @@
   initializeRegions: ->
     @regions = []
     @buildRegion(jQuery(region)) for region in jQuery("[#{Mercury.config.regions.attribute}]", @document)
-    return unless @options.visible
+    return unless @visible
     for region in @regions
       if region.focus
         region.focus()
@@ -100,7 +99,7 @@
     @snippetToolbar = new Mercury.SnippetToolbar(@document)

     @hijackLinksAndForms()
-    Mercury.trigger('mode', {mode: 'preview'}) unless @options.visible
+    Mercury.trigger('mode', {mode: 'preview'}) unless @visible

   bindDocumentEvents: ->


EDSC-106: fix autofill issues and error message

--- app/assets/javascripts/models/data/account.js.coffee
+++ app/assets/javascripts/models/data/account.js.coffee
@@ -39,7 +39,20 @@

     createAccount: =>
+      # Fix chrome autocomplete issues
+      @username(document.getElementById('username')?.value) unless @username()?.length > 0
+      @password(document.getElementById('password')?.value) unless @password()?.length > 0
+      @email(document.getElementById('email')?.value) unless @email()?.length > 0
+      @firstName(document.getElementById('first_name')?.value) unless @firstName()?.length > 0
+      @lastName(document.getElementById('last_name')?.value) unless @lastName()?.length > 0
+      @organizationName(document.getElementById('organization_name')?.value) unless @organizationName()?.length > 0
+      @domain(document.getElementById('user_domain')?.value) unless @domain()?.length > 0
+      @userType(document.getElementById('user_type')?.value) unless @userType()?.length > 0
+      @primaryStudyArea(document.getElementById('primary_study_area')?.value) unless @primaryStudyArea()?.length > 0
+      @country(document.getElementById('country')?.value) unless @country()?.length > 0
+
       @_validateNewAccountForm()
+
       if @errors()?.length == 0
         xhr = doPost '/users', @_buildUserData(), (response) =>
           @user.username(@username())
@@ -138,7 +151,7 @@
         @countryError(true)

       if errors?.length > 1
-        @errors("Please fill in all required fields, identified highlighted below")
+        @errors("Please fill in all required fields, highlighted below")
       else
         @errors(errors.join(", "))


Fix issue where clearing filters would reset in-project status

--- app/assets/javascripts/models/data/project.js.coffee
+++ app/assets/javascripts/models/data/project.js.coffee
@@ -13,5 +13,11 @@
       else
         datasets.push(dataset)

+    hasDataset: (other) =>
+      id = other.id()
+      for dataset in @datasets()
+        return true if dataset.id() == id
+      false
+

   exports = Project


Fix missing `@addresses` attribute

Fix missing `@addresses` attribute

--- app/assets/javascripts/sprangular/models/user.coffee
+++ app/assets/javascripts/sprangular/models/user.coffee
@@ -8,9 +8,9 @@
     @orders      = Sprangular.extend(@orders, Sprangular.Order)
     @creditCards = Sprangular.extend(@payment_sources, Sprangular.CreditCard)

+    @_mergeAddressLists()
+
     @allowOneClick = @creditCards.length > 0 && @addresses.length > 0
-
-    @_mergeAddressLists()

   serialize: ->
     _.omit this, (value) ->


Click the button to answer the question

Click the button to answer the question

--- app/assets/javascripts/training/components/quiz.cjsx
+++ app/assets/javascripts/training/components/quiz.cjsx
@@ -3,23 +3,35 @@
 TrainingActions = require '../actions/training_actions'

 Quiz = React.createClass(
-  setSelectedAnswer: (e) ->
-    TrainingActions.setSelectedAnswer(e.currentTarget.dataset.answerId)
-  isChecked: (selectedAnswer, answerId) ->
-    selectedAnswer == answerId
+  setSelectedAnswer: (id) ->
+    TrainingActions.setSelectedAnswer(id)
+  verifyAnswer: (e) ->
+    e.preventDefault()
+    @setSelectedAnswer(@state.selectedAnswerId)
+  setAnswer: (e) ->
+    @setState selectedAnswerId: e.currentTarget.dataset.answerId
+  componentWillReceiveProps: (newProps) ->
+    @setState selectedAnswerId: newProps.selectedAnswerId
+  correctStatus: (answer) ->
+    if @props.correctAnswer == answer then ' correct' else ' incorrect'
+  visibilityStatus: (answer) ->
+    if @props.selectedAnswer == answer then ' shown' else ' hidden'
+  getInitialState: ->
+    selectedAnswerId: @props.selectedAnswerId
   render: ->
     answers = @props.answers.map (answer, i) =>
       explanationClass = "assessment__answer-explanation"
-      explanationClass += if @props.correctAnswer == answer.id then ' correct' else ' incorrect'
-      explanationClass += if @props.selectedAnswer == answer.id then ' shown' else ' hidden'
-      isChecked = @isChecked(@props.selectedAnswer, answer.id)
+      explanationClass += @correctStatus(answer.id)
+      explanationClass += @visibilityStatus(answer.id)
+      defaultChecked = parseInt(@props.selectedAnswer) == answer.id
+      checked = if @state.selectedAnswerId? then parseInt(@state.selectedAnswerId) == answer.id else defaultChecked
       <li>
         <label>
           <input
-            onChange={@setSelectedAnswer}
+            onChange={@setAnswer}
             data-answer-id={answer.id}
-            defaultChecked={isChecked}
-            checked={isChecked}
+            defaultChecked={defaultChecked}
+            checked={checked}
             type="radio"
             name="answer" />
           {answer.text}
@@ -34,6 +46,7 @@
           {answers}
         </ul>
       </fieldset>
+      <button onClick={@verifyAnswer}>Check Answer</button>
     </form>


Renaming condition

Renaming condition

--- tests/www/utils/conditions.coffee
+++ tests/www/utils/conditions.coffee
@@ -1 +1 @@
-exports.page_is_rendered = 'window.crawler && window.crawler.is_rendered == true;'+exports.is_rendered = 'window.crawler && window.crawler.is_rendered == true;'


fix(custom_field): tg-1276 input number value not show

--- app/coffee/modules/common/custom-field-values.coffee
+++ app/coffee/modules/common/custom-field-values.coffee
@@ -187,7 +187,7 @@
             }

             scope = $scope.$new()
-            scope.attributeHtml = wysiwygService.getHTML(value)
+            scope.attributeHtml = wysiwygService.getHTML("#{value}")  # value could be a number
             scope.extra = attributeValue.extra
             scope.model = value
             scope.project = $scope.project


Escape text box val before inserting into tinyMCE

HTML in the answer text field is saved raw. If the question is then
edited by some one else and they edit the answer using tinyMCE, they can
accidentally inject unknown html/js into their DOM. Fixed by escaping
the string in the answer_text input thus the content always shows up as
plain text in tinyMCE.

Fixes CNVS-22213

Test plan:
 - Create quiz
 - Set answer text to `<script>alert("XSS")</script>` using the
 answer_text text input field
 - Click the `pencil` edit icon
 - You should see the `<script>` html in the tinyMCE rich text editor
 - Click `Done`
 - You should not see a js alert.

Change-Id: I05c6d57159a02c098ef3d6320eb7768d4a0658a3
Reviewed-on: https://gerrit.instructure.com/59569
Tested-by: Jenkins
Reviewed-by: Matt Berns <e53c8988cd428dde240324ce2797c8eb7073560f@instructure.com>
QA-Review: Adam Stone <a0b2b61e2edcf99aa16080c2254a6ae6923d1fac@instructure.com>
Product-Review: Cameron Sutter <853bcc0cb8f5834b7805e8eb9a762054be43c731@instructure.com>

--- app/coffeescripts/editor/MultipleChoiceToggle.coffee
+++ app/coffeescripts/editor/MultipleChoiceToggle.coffee
@@ -1,4 +1,4 @@
-define ['jquery', 'compiled/editor/EditorToggle'], ($, EditorToggle) ->
+define ['jquery', 'str/htmlEscape', 'compiled/editor/EditorToggle'], ($, htmlEscape, EditorToggle) ->

   ##
   # Toggles a multiple choice quiz answer between an editor and an element
@@ -41,7 +41,7 @@
       id = @textArea.attr('id')
       @answerText.hide()
       if @content is ''
-        tinyMCE.get(id).setContent @answerText.val()
+        tinyMCE.get(id).setContent htmlEscape(@answerText.val())
       else
         tinyMCE.get(id).setContent @content


add comments

add comments

--- src/loaders/TiledMapLoader.coffee
+++ src/loaders/TiledMapLoader.coffee
@@ -1,10 +1,17 @@
+# Tiled map loader
+# Using JSON format is a pure generic choice : wanting to avoid FreshFlesh
+# mistakes with TMX format parsing
 class Rotten.Loaders.TiledMapLoader extends Rotten.EventManager

+
+    # Constructs a tiled map loader
+    # @param {String} url Map url
     constructor: (url) ->
         super
-
         @mapUrl = url

+
+    # Loads the tiled map
     load: () ->
         Rotten.Utils.ajax
             url: @mapUrl


Remove duplicated pandoc citation tests

Remove duplicated pandoc citation tests

--- spec/latexer-spec.coffee
+++ spec/latexer-spec.coffee
@@ -65,8 +65,6 @@
       runs ->
         spyOn(FindLabels, "getAbsolutePath").andReturn("bibfile.bib")
         spyOn(fs, "readFileSync").andReturn(bibText)
-        spyOn(pandoc, 'isPandocStyleCitation')
-          .andCallThrough() #.andCallThrough is old Jasmine 1.3 syntax

     describe "typing \\ref{", ->
       it "shows the labels to select from", ->
@@ -92,41 +90,6 @@
           expect(info.length).toBe 2
           expect(info[0].textContent).toBe "title#{i}"
           expect(info[1].textContent).toBe "author#{i}"
-
-    describe "Detecting and auto-completing pandoc-style citations", ->
-      it "detects the beginning of a pandoc-style citation key", ->
-        citeText = "[@"
-        result = pandoc.isPandocStyleCitation(citeText)
-        expect(pandoc.isPandocStyleCitation).toHaveBeenCalledWith(citeText)
-        expect(result).toBe true
-
-      it "detects the beginning of the Nth key in a citation", ->
-        expect(pandoc.isPandocStyleCitation(
-          "[@Fallows1997; @"
-        )).toBe true
-        expect(pandoc.isPandocStyleCitation(
-          "[@Turing1944; see also @Church1936; @"))
-          .toBe true
-        expect(pandoc.isPandocStyleCitation(
-          "[@Goldberg2014 et al.; @Forbus2014; @Finnegan; @Felber; @"))
-          .toBe true
-        expect(pandoc.isPandocStyleCitation(
-          "[a claim supported by multiple empirical studies [@Elby2001; @Gauss1989; and also @"))
-          .toBe true
-
-      it "Recognizes when the user has NOT yet begun to type/edit a cite key", ->
-        citeTexts = [ "[@Fallows1997; "
-                    , "[ @Richards2014;"
-                    , "[ @Gupta "
-                    , "[@Chemler2005 and others; @Suzuki1992; "
-        ]
-        citeTexts.map(
-          (currentValue) ->
-            expect(
-              pandoc.isPandocStyleCitation(currentValue)
-            ).toBe false # FALSE because the user isn't beginning a new cite key yet
-            return
-        )

   describe "typing \\begin{evironment} or \\[", ->
     [workspaceElement, editor] = []


Added tests for smartcase

Added tests for smartcase

--- spec/motions-spec.coffee
+++ spec/motions-spec.coffee
@@ -977,6 +977,22 @@
           keydown('n')
           expect(editor.getCursorBufferPosition()).toEqual [2, 0]

+        it "uses Smartcase insensitive search if useSmartcaseForSearch is true and searching lowercase", ->
+          atom.config.set 'vim-mode.useSmartcaseForSearch', true
+          editor.commandModeInputView.editor.setText 'abc'
+          editor.commandModeInputView.editor.trigger 'core:confirm'
+          expect(editor.getCursorBufferPosition()).toEqual [1, 0]
+          keydown('n')
+          expect(editor.getCursorBufferPosition()).toEqual [2, 0]
+
+        it "uses Smartcase sensitive search if useSmartcaseForSearch is true and searching uppercase", ->
+          atom.config.set 'vim-mode.useSmartcaseForSearch', true
+          editor.commandModeInputView.editor.setText 'ABC'
+          editor.commandModeInputView.editor.trigger 'core:confirm'
+          expect(editor.getCursorBufferPosition()).toEqual [2, 0]
+          keydown('n')
+          expect(editor.getCursorBufferPosition()).toEqual [2, 0]
+
       describe "repeating", ->
         it "does nothing with no search history", ->
           # This tests that no exception is raised


fix(empty): Use a much faster query for empty perspective

--- src/mailbox-perspective.coffee
+++ src/mailbox-perspective.coffee
@@ -200,7 +200,11 @@
     @accountIds = []

   threads: =>
-    query = DatabaseStore.findAll(Thread).where(accountId: -1).limit(0)
+    # We need a Thread query that will not return any results and take no time.
+    # We use lastMessageReceivedTimestamp because it is the first column on an
+    # index so this returns zero items nearly instantly. In the future, we might
+    # want to make a Query.forNothing() to go along with MailboxPerspective.forNothing()
+    query = DatabaseStore.findAll(Thread).where(lastMessageReceivedTimestamp: -1).limit(0)
     return new MutableQuerySubscription(query, {asResultSet: true})

   canReceiveThreadsFromAccountIds: =>


Create larger boundary to test cameras

Create larger boundary to test cameras

--- src/EngineBuilder.coffee
+++ src/EngineBuilder.coffee
@@ -17,20 +17,21 @@
       engine.e().c 'pos',
         x: 0
         y: 0,
-        radius: 240
+        radius: 1000
       .c 'boundary',{}
       .c 'render',
         fill: 'none'
         stroke: '#000'
-      for i in [0..200]
+      for i in [0..1000]
+        radius = Math.random() * 10 + 8
         engine.e().c 'pos',
-          x: Math.random()*400-200
-          y: Math.random()*400-200
-          radius: 8
+          x: Math.random()*2000-1000
+          y: Math.random()*2000-1000
+          radius: radius
         .c 'blob',
           velX: Math.random()*60-30
           velY: Math.random()*60-30
-          weight: 64
+          weight: radius*radius
         .c 'render', {}
       ###
       engine.e().c 'pos',


jqplot mekko: use gquery future value

jqplot mekko: use gquery future value

I've updated its gqueries, removing the future prefix. The table view
now shows present and future value. See #798

--- app/assets/javascripts/lib/views/mekko_chart_view.coffee
+++ app/assets/javascripts/lib/views/mekko_chart_view.coffee
@@ -12,8 +12,7 @@
       group = serie.get('group')
       if group
         if (!series[group]) then series[group] = []
-        val = serie.result_pairs()[0]
-        series[group].push(val)
+        series[group].push(serie.future_value())
     results = _.map series, (sector_values, sector) ->
       return _.map sector_values, (value) ->
         return value


Change AbstractButton.icon's type to Instance in bokehjs

--- bokehjs/src/coffee/models/widgets/abstract_button.coffee
+++ bokehjs/src/coffee/models/widgets/abstract_button.coffee
@@ -9,7 +9,7 @@
   @define {
       callback:    [ p.Instance          ]
       label:       [ p.String, "Button"  ]
-      icon:        [ p.String            ]
+      icon:        [ p.Instance          ]
       button_type: [ p.String, "default" ] # TODO (bev)
     }


Use appropriate icon name in PreviewSaveTool

--- bokehjs/src/coffee/tool/actions/preview_save_tool.coffee
+++ bokehjs/src/coffee/tool/actions/preview_save_tool.coffee
@@ -28,7 +28,7 @@
   default_view: PreviewSaveToolView
   type: "PreviewSaveTool"
   tool_name: "Preview/Save"
-  icon: "bk-tool-icon-preview-save"
+  icon: "bk-tool-icon-save"

 module.exports =
   Model: PreviewSaveTool


reactivity: chat list item: remove duplicate call of setting focus; remove a delay for setting focus

--- client/activity/lib/components/chatlistitem/index.coffee
+++ client/activity/lib/components/chatlistitem/index.coffee
@@ -191,8 +191,7 @@

     domNode = @refs.EditMessageTextarea.getDOMNode()

-    kd.utils.wait 100, ->
-      kd.utils.moveCaretToEnd domNode
+    kd.utils.moveCaretToEnd domNode

   editPost: ->
@@ -200,8 +199,6 @@
     messageId = @props.message.get '_id'

     ActivityFlux.actions.message.setMessageEditMode messageId
-    @focusInputOnEdit()
-

   showDeletePostPromptModal: ->


pagination: bug fixes

pagination: bug fixes

--- client/admin/lib/views/moderation/topiccommonview.coffee
+++ client/admin/lib/views/moderation/topiccommonview.coffee
@@ -63,7 +63,7 @@
     @listController.on 'LazyLoadThresholdReached', =>

       if @skip is 0 and @searchSkip is 0
-        @fetchChannels
+        @fetchChannels()
       else if @skip is 0
         @search()
       else
@@ -106,7 +106,7 @@

     @skip = 0  # revert skip of normal listing

-    @searchSkip += channels?.length
+    @searchSkip += channels.length
     @listChannels err, channels

@@ -118,7 +118,7 @@

     @searchSkip = 0

-    @skip += channels?.length
+    @skip += channels.length

     @listChannels err, channels

@@ -165,4 +165,4 @@
   isSameSearch : (query = "") ->
     @lastQuery or= ''

-    return true if query is @lastQuery+    return query is @lastQuery


StackTemplateListController: Update accessLevel label

--- client/app/lib/stacks/stacktemplatelistcontroller.coffee
+++ client/app/lib/stacks/stacktemplatelistcontroller.coffee
@@ -86,9 +86,18 @@
   handleStackTemplateChanged: (params) ->

     stackTemplateId = params.contents
-    [item]  = @getListItems().filter (i) -> i.getData()._id is stackTemplateId
-
-    unless item
+    hasFound        = no
+
+    for item in @getListItems()
+      item.isDefaultView.hide()
+
+      if item.getData()._id is stackTemplateId
+        item.getData().isDefault    = yes
+        item.isDefaultView.show()
+        item.updateAccessLevel 'group'
+        hasFound = yes
+
+    unless hasFound
       @fetch { _id : stackTemplateId }, (items) => @addListItems items


fix ga

fix ga

--- src/modules/top/topCtrl.coffee
+++ src/modules/top/topCtrl.coffee
@@ -87,8 +87,8 @@

     sendGAEvent = (age,sex)->
       $analytics.eventTrack 'CheckDieRate', {
-        "label": age
-        "category": sex
+        "category": sex + ''
+        "label": age + ''
       }
       return


Add comment to PaneContainer::indexOfPane

--- src/pane-container-view.coffee
+++ src/pane-container-view.coffee
@@ -144,5 +144,6 @@
     top: {x: boundingBox.left, y: boundingBox.top}
     bottom: {x: boundingBox.left, y: boundingBox.bottom}

+  # Deprecated
   getPanes: ->
     @getPaneViews()


added initializer and type setting method

--- lib/index.coffee
+++ lib/index.coffee
@@ -12,6 +12,9 @@
   out         = undefined
   fileList    = []
   fileStack   = []
+  initialize = ->
+    zip = archiver type, options
+    initialized = true

   return {
     getStream: ->
@@ -20,12 +23,18 @@
     setOptions: (opt) ->
       extend(options, opt)

+    setType: (type='zip') ->
+      type = type
+
     setOutput: (path) ->
       if out?
         out.end()
       out = fs.createWriteStream path

     add: (path, callback) ->
+      if not initialized
+        initialize()
+
       glob path, (err, files) ->
         callback err if err
         files.forEach (file, i) ->
@@ -43,6 +52,9 @@
       return fileList

     compress: (callback) ->
+      if not initialized
+        initialize()
+
       zip.pipe out

       async.parallel fileStack, (err) ->


cache bash completions

cache bash completions

--- lib/index.coffee
+++ lib/index.coffee
@@ -16,6 +16,7 @@

   # eval "$(./app.js init)"
   init: () ->
+    cacheFile = path.join(config.getDataDir(), "bash_completions")
     console.log """
     function _qdwrap() {
       local cmd newPath
@@ -50,7 +51,11 @@
       cur="${COMP_WORDS[COMP_CWORD]}"
       prev="${COMP_WORDS[COMP_CWORD-1]}"

-      opts=$(${job} _bash_complete)
+      if [[ -s "#{cacheFile}" ]]; then
+        opts=$(cat "#{cacheFile}")
+      else
+        opts=$(${job} _bash_complete)
+      fi
       COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
       return 0
     }
@@ -59,7 +64,9 @@
     programDone()

   _bash_complete: () ->
-    console.log (cmd._name for cmd in commander.commands when cmd._name isnt "*").join(" ")
+    str = (cmd._name for cmd in commander.commands when cmd._name isnt "*").join(" ")
+    console.log str
+    cacheCompletions str
     programDone()

   _cd: () ->
@@ -70,6 +77,11 @@

 cmds.extend defaultCommands

+cacheCompletions = (str) ->
+  cacheFile = path.join(config.getDataDir(), "bash_completions")
+  str ?= (cmd._name for cmd in commander.commands when cmd._name isnt "*").join(" ")
+  oldStr = (if fs.existsSync(cacheFile) then fs.readFileSync cacheFile, "utf8" else null)
+  fs.writeFileSync cacheFile, str, "utf8" if str != oldStr

 # check for private commands first (start with underscore)
 if process.argv.length == 3


restify: changed service name, added accept, gzip middlewares

--- lib/index.coffee
+++ lib/index.coffee
@@ -13,10 +13,13 @@
 pkg         = require '../package.json'

 module.exports = server = restify.createServer
-  name: 'service-starter'
+  name: 'lru-cache-service'
   version: '0.0.0'

 server.use restify.bodyParser()
+server.use (restify.acceptParser server.acceptable)
+server.use restify.gzipResponse()
+
 server.on 'after', logger

 server.get '/api/health', (req, res, next) ->