alter calls to make context the 'this', swap pipeline args order

--- lib/chain-builder.coffee
+++ lib/chain-builder.coffee
@@ -34,7 +34,7 @@
         for fn in array                       # loop over provided functions
           okay = do (fn) ->                   # run in own scope, hold return result
             try                               # grab errors in chain calls
-              return fn context               # calls function, return result
+              return fn.call context, context # calls function with context=this
             catch e
               console.log 'chain: error', e   # report to console. TODO: remove?
               context.chainError = e          # store error in context
@@ -71,19 +71,19 @@

     return (ctx={}) ->                        # return new function, pipeline
       i = 0                                   # start with first fn in array
-      caller = (context, next) ->             # new fn calls their fn
+      caller = (next, context) ->             # new fn calls their fn
         fn = array[i]                         # get next function
         try                                   # catch errors from call
-          return fn context, next             # call with provided args
+          return fn.call context, next, context # call: context=this
         catch e
           console.log 'pipeline: error', e    # report to console. TODO: remove?
           context.chainError = e              # store error in context
           return false                        # end chain with false return

-      repeater = (context) ->                 # the 'next' function
+      repeater = (context=ctx) ->             # the 'next' function
         i++                                   # advance 'i'
         if i < array.length                   # check there is another fn
-          caller context, repeater            # call it with context+next
+          caller repeater, context            # call it with context+next
         else true                             # successful pipeline, return true

-      return caller ctx, repeater             # initiate pipeline, return result
+      return caller repeater, ctx             # initiate pipeline, return result


PPU bugfixes.

PPU bugfixes.

--- src/PPU.coffee
+++ src/PPU.coffee
@@ -51,9 +51,9 @@
         @setMask 0          #  8-bit
         @setStatus 0        #  8-bit
         @oamAddress = 0     # 15-bit
+        @tempAddress = 0    # 15-bit also known as 'Loopy_T'
+        @vramAddress = 0    # 15-bit also known as 'Loopy_V'
         @vramReadBuffer = 0 #  8-bit
-        @vramAddress = 0    # 15-bit also known as 'Loopy_V'
-        @tempAddress = 0    # 15-bit also known as 'Loopy_T'
         @writeToogle = 0    #  1-bit
         @fineXScroll = 0    #  3-bit

@@ -67,7 +67,7 @@

     writeControl: (value) ->
         @setControl value
-        @tempAddress = (@vramAddress & 0xF3FF) | (value & 0x03) << 10 # T[11,10] = C[1,0]
+        @tempAddress = (@tempAddress & 0xF3FF) | (value & 0x03) << 10 # T[11,10] = C[1,0]
         value

     setControl: (value) ->
@@ -99,7 +99,7 @@

     readStatus: ->
         value = @getStatus()
-        @vblankInProgress = 0
+        @vblankStarted = 0
         @writeToogle = 0
         value

@@ -107,13 +107,13 @@
         @vramWritesIgnored     << 4 | # S[4]
         @spriteScalineOverflow << 5 | # S[5]
         @spriteZeroHit         << 6 | # S[6]
-        @vblankInProgress      << 7   # S[7]
+        @vblankStarted         << 7   # S[7]

     setStatus: (value) ->
         @vramWritesIgnored     = (value >>> 4) & 0x01 # S[4]
         @spriteScalineOverflow = (value >>> 5) & 0x01 # S[5]
         @spriteZeroHit         = (value >>> 6) & 0x01 # S[6]
-        @vblankInProgress      = (value >>> 7)        # S[7]
+        @vblankStarted         = (value >>> 7)        # S[7]

     ###########################################################
     # Object attribute memory access
@@ -142,6 +142,7 @@
         else            # 2nd write
             addressLow = address
             @tempAddress = (@tempAddress & 0xFF00) | addressLow  # Low bits  [7-0]
+            @vramAddress = @tempAddress
         address

     readData: ->
@@ -197,14 +198,14 @@

     writeScroll: (value) ->
         @writeToogle = not @writeToogle
-        if @writeToogle # 1st write
+        if @writeToogle # 1st write (x scroll)
             @fineXScroll = value & 0x07
-            coarseXScroll = value >> 3
+            coarseXScroll = value >>> 3
             @tempAddress = (@tempAddress & 0xFFE0) | coarseXScroll
-        else            # 2nd write
+        else            # 2nd write (y scroll)
             fineYScroll = (value & 0x07) << 12
-            coarseYScroll = (value & 0xF1) << 2
-            @tempAddress = (@tempAddress & 0x18C0) | coarseYScroll | fineYScroll
+            coarseYScroll = (value & 0xF8) << 2
+            @tempAddress = (@tempAddress & 0x0C1F) | coarseYScroll | fineYScroll
         value

     copyHorizontalScrollBits: ->
@@ -248,15 +249,18 @@
     ###########################################################

     tick: ->
+        @updateScrolling() if @isRenderingEnabled()
         @renderFramePixel() if @isRenderingScanlineCycle()
-        @updateScrolling() if @isRenderingEnabled()
         @incrementCycle()

     isRenderingScanlineCycle: ->
         0 <= @scanline <= 239 and 1 <= @cycle <= 256

     isRenderingInProgress: ->
-        not @vblankInProgress and @isRenderingEnabled()
+        not @isVBlank() and @isRenderingEnabled()
+
+    isVBlank: ->
+        241 <= @scanline <= 260

     isRenderingEnabled: ->
         @spritesVisible or @backgroundVisible
@@ -330,7 +334,7 @@
         @incrementFineXScroll() if 1 <= @cycle <= 256 # Increments coarse X scroll each 8th fine X scroll increment
         @incrementCoarseXScroll() if @cyle is 328 or @cycle is 336 # Extra coarse X scroll
         @copyHorizontalScrollBits() if @cycle is 257
-        @copyVerticalScrollBits() if 280 <= @cycle <= 304
+        @copyVerticalScrollBits() if @scanline is -1 and 280 <= @cycle <= 304

     incrementCycle: ->
         @cycle++
@@ -339,10 +343,13 @@
     incementScanline: ->
         @cycle = 0
         @scanline++
-        @scanline = -1 if @scanline is 262
-        @vblankInProgress = 241 <= @scanline <= 260
-        @cpu.nonMaskableInterrupt() if @scanline is 241 and @vblankGeneratesNMI
-        @frameAvailable = true if @scanline is 241
+        if @scanline is 241
+            @vblankStarted = 1
+            @frameAvailable = true
+            @cpu.nonMaskableInterrupt() if @vblankGeneratesNMI
+        else if @scanline is 262
+            @vblankStarted = 0
+            @scanline = -1

     readFrame: ->
         @frameAvailable = false


getResourcePath: Mac: calculate asarPath and return nextTick -> callback

--- src/apm.coffee
+++ src/apm.coffee
@@ -47,7 +47,8 @@
         child_process.exec 'mdfind "kMDItemCFBundleIdentifier == \'com.github.atom\'"', (error, stdout='', stderr) ->
           [appLocation] = stdout.split('\n') unless error
           appLocation = '/Applications/Atom.app' unless appLocation
-          callback("#{appLocation}/Contents/Resources/app.asar")
+          asarPath = "#{appLocation}/Contents/Resources/app.asar"
+          return process.nextTick -> callback(asarPath)
       when 'linux'
         appLocation = '/usr/local/share/atom/resources/app.asar'
         unless fs.existsSync(appLocation)


Final commit

Final commit

--- src/app.coffee
+++ src/app.coffee
@@ -9,8 +9,6 @@

 #Kimono uri
 productsUrl = 'URL'
-
-

 #Get products from kimono
 jsonHttp.getJson productsUrl, (err, response) ->


added easier integration with jQuery/Semantic.js/other

use sparingly.

--- src/aui.coffee
+++ src/aui.coffee
@@ -2,6 +2,7 @@
 unless React? then throw new Error "Aui: window.React not found."
 unless jQuery? then console.warn "Aui: window.jQuery not found, Modules and Semantic-UI will be disabled."
 unless jQuery?.site?.settings?.modules? then console.warn 'Aui: No Semantic-UI window.jQuery.site.settings.modules found, Semantic will be disabled.'
+

 ## mixin Aui.Mixin
 # Aui class is the main wrapper class
@@ -10,21 +11,27 @@
 # and merges them into the className of each element.
 AuiMixin =
   componentWillMount: ->
+    @$ = (ref) -> jQuery? React.findDOMNode @refs[ref] or ref
     render = @render
     @render = ->
       element = render?.apply? @, arguments
       if React.isValidElement element
-        Aui.classify element, @props
+        Aui.classify element
       else
-        Aui.classify (Aui.warning 'Aui.Mixin: Child is not a React.isValidElement', element), @props
+        Aui.classify Aui.warning 'Aui.Mixin: Child is not a React.isValidElement', element

-## class <Aui/>
+## (depricated) class <Aui/>
 # A tag that applies the Aui.Mixin
+# Using the Aui.Mixin in your Component is prefered over <Aui/> tag.
 Aui = React.createClass
   mixins: [AuiMixin]
-  render: -> @props.children
+  render: ->
+    Aui.warning '<Aui/> tag is depricated, use Aui.Mixin instead', @props.children
+  componentDidMount: ->
+    console.warn React.findDOMNode @
+Aui.Mixin = Aui.AuiMixin = AuiMixin
 Aui.Aui = Aui
-Aui.Mixin = Aui.AuiMixin = AuiMixin
+Aui.$ = jQuery

 ## function Aui.warning (string, element)
 # console.warn a warning message
@@ -42,6 +49,8 @@
   disableSemantic: not jQuery?.site?.settings?.modules?
   # do not recursively Aui.classify children by default.
   ignoreChildren: no
+  # automatically event.preventDefault() onSubmit of <form> tags
+  preventDefaultSubmit: yes

 ## internal class `AuiOptions({options})`
 # defaults all Aui options
@@ -63,7 +72,7 @@
 Aui.classify = (element, options) ->
   options = AuiOptions options
   unless React.isValidElement element
-    Aui.warning 'Aui.classify: element is not a React.isValidElement', element
+    element = Aui.warning 'Aui.classify: element is not a React.isValidElement', element
   classify = (element) ->
     return element unless React.isValidElement element
     classNames = {}
@@ -75,17 +84,17 @@
     for own key, value of element.props
       classNames[key] = yes if value is yes
       if (not options.disableModules) and (Array.isArray value) and (key in Aui.modules)
-        unless modules then modules = []
-        classNames[key] = true
-        modules.push key
+        classNames[key] = yes
+        modules or= {}
+        modules[key] = value
       props[key] = value
     props.className = Object.keys(classNames).join ' '
     unless props.className.length
       delete props.className
-    console.log modules, (if typeof element.type is 'string' then element.type else 'Component'), props.className or ''
+    #console.log modules, (if typeof element.type is 'string' then element.type else 'Component'), props.className or ''
     element = React.cloneElement element, props, if options.ignoreChildren then element.props.children else React.Children.map element.props.children, classify
     if modules
-      React.createElement Aui.Module, { modules }, element
+      React.createElement Aui.Module, { modules, options }, element
     else element
   classify element

@@ -98,29 +107,41 @@
 # but in theory could be used to call any `window.jQuery.fn` function
 Aui.modules = (jQuery?.site?.settings?.modules or []).slice()

-## internal cache of options passed to modules.
-moduleCache = {}
+## internal global cache of options passed to module functions.
+cache = {}
+
 ## internal class <Aui.Module/>
 # This class provides support for `Aui.modules` and handles calling jQuery.fn[module] calls
 Aui.Module = React.createClass
   render: -> React.Children.only @props.children
-  componentDidMount: -> @callModules @props.children.props
-  componentWillReceiveProps: (props) -> @callModules props.children.props
+  preventDefault: (event) ->
+    event?.preventDefault?()
+  componentDidMount: ->
+    if @props.options.preventDefaultSubmit and 'form' of @props.modules
+      jQuery(React.findDOMNode @).on 'submit', @preventDefault
+    @callModules @props.children.props
+  componentWillReceiveProps: (props) ->
+    @callModules props.children.props
   callModules: (props) ->
     $element = jQuery React.findDOMNode @
-    moduleId = $element.attr 'data-reactid'
-    if moduleId isnt @moduleId
-      console.log 'moduleId isnt @moduleId', moduleId, @moduleId
-      delete moduleCache[@moduleId]
-    @moduleId = moduleId
-    moduleCache[@moduleId] or= {}
-    for module, options in @props.modules
-      cacheOptions = JSON.stringify options
-      if moduleCache[@moduleId][module] isnt cacheOptions
-        moduleCache[@moduleId][module] = cacheOptions
-        ($element)[module] @props.children.props[module]...
+    @id = $element.attr 'data-reactid'
+    cache[@id] or= {}
+    for own module, options of @props.modules
+      stringifiedOptions = JSON.stringify options
+      if cache[@id][module] isnt stringifiedOptions
+        cache[@id][module] = stringifiedOptions
+        console.log module, stringifiedOptions, React.findDOMNode @
+        $element[module]? @props.children.props[module]...
     return
   componentWillUnmount: ->
-    delete moduleCache[@moduleId]
+    $element = jQuery React.findDOMNode @
+    if 'form' of @props.modules
+      $element.off 'submit', @preventDefault
+    unless @props.options.disableSemantic
+      for module in @props.modules
+        $element[module]? 'destroy'
+    delete cache[@id]
+    console.log 'componentWillUnmount', React.findDOMNode @
+    #throw new Error 'STOP drop and roll...'
 window?.Aui = Aui
 module?.exports = Aui


now calculate iva factura venta

now calculate iva factura venta

--- app/assets/javascripts/autocomplete.js.coffee
+++ app/assets/javascripts/autocomplete.js.coffee
@@ -50,30 +50,31 @@
           $this.closest(".fields").find("td:nth-child(5)").find(".iva").val iva_producto.toFixed(2)
           # $this.closest(".fields").find("td:nth-child(3)").find(".valor_unitario").val ui.item.precio_venta
         window.Helpers.AutocompleteHelper.calcular_total_producto($this)
+        window.Helpers.AutocompleteHelper.sumar_iva()
         window.Helpers.AutocompleteHelper.calcular_valores_factura()

     $(".cantidad").on "input", ->
       $this = $(this)
       window.Helpers.AutocompleteHelper.calcular_total_producto($this)
+      window.Helpers.AutocompleteHelper.sumar_iva()
       window.Helpers.AutocompleteHelper.calcular_valores_factura()

     $(".eliminar_item").on "click", ->
       $this = $(this)
       $this.closest(".fields").remove()
+      window.Helpers.AutocompleteHelper.sumar_iva()
       window.Helpers.AutocompleteHelper.calcular_valores_factura()

-  calcular_iva: (componente) ->
-    cantidad = componente.closest(".fields").find("td:nth-child(2)").find(".cantidad").val()
-    valor_unitario = componente.closest(".fields").find("td:nth-child(3)").find(".valor_unitario").val()
-    iva_producto = valor_unitario * 0.12
-    console.log iva_producto
-    componente.closest(".fields").find("td:nth-child(5)").find(".iva").val((cantidad * iva_producto).toFixed(2))
+  sumar_iva: ->
+    sum = 0
+    $(".iva").each ->
+      sum += parseFloat($(this).val())
+    $(".iva_factura").val(sum.toFixed(2));

   calcular_total_producto: (componente) ->
     cantidad = componente.closest(".fields").find("td:nth-child(2)").find(".cantidad").val()
     valor_unitario = componente.closest(".fields").find("td:nth-child(3)").find(".valor_unitario").val()
     hasiva = componente.closest(".fields").find("td:nth-child(8)").find(".hasiva").val()
-    console.log hasiva
     if hasiva == "true"
       iva_producto = valor_unitario * 0.12
       componente.closest(".fields").find("td:nth-child(5)").find(".iva").val((cantidad * iva_producto).toFixed(2))
@@ -87,7 +88,7 @@
     $(".subtotal_0").val sum.toFixed(2)
     $(".subtotal_12").val sum.toFixed(2)
     $(".descuento_factura").val 0
-    $(".iva_factura").val((sum*0.12).toFixed(2));
+    # $(".iva_factura").val((sum*0.12).toFixed(2));
     $(".total_factura").val((sum*0.12+sum).toFixed(2));

   init_autocompleteProveedor: ->


fix hide/reveal of alert when song is played

--- app/assets/javascripts/channels/player.coffee
+++ app/assets/javascripts/channels/player.coffee
@@ -1,4 +1,4 @@
-App.player = App.cable.subscriptions.create "PlayerChannel",
+ App.player = App.cable.subscriptions.create "PlayerChannel",
   connected: ->
     # Called when the subscription is ready for use on the server

@@ -8,8 +8,9 @@
   received: (data) ->
     if data.nextVideoId
       window.player.play(data.nextVideoId);
+      $('.alert').hide()
     else if data.playlistEnded
-      $('.playlist-ended-alert').show()
+      $('.alert').show()
     else if data.stop
       window.player.stop()


OX-704 blur position input field after hitting enter

--- app/assets/javascripts/oxalis/view/gui.coffee
+++ app/assets/javascripts/oxalis/view/gui.coffee
@@ -123,7 +123,7 @@
     $("#trace-position-input").on "change", (event) =>

       @setPosFromString(event.target.value)
-      return
+      $("#trace-position-input").blur()

     $("#trace-finish-button").click (event) =>


Solve naming problem by inlining :)

Solve naming problem by inlining :)

--- app/assets/javascripts/roofpig/Move.js.coffee
+++ app/assets/javascripts/roofpig/Move.js.coffee
@@ -33,9 +33,8 @@
     this._do(world3d.pieces, -@turns, true)

   _do: (pieces3d, do_turns, animate) ->
-    animation_pieces = pieces3d.on(@side)
     pieces3d.move(@side, do_turns)
-    new MoveExecution(animation_pieces, @side.normal, do_turns * -Math.PI/2, @turn_time, animate)
+    new MoveExecution(pieces3d.on(@side), @side.normal, do_turns * -Math.PI/2, @turn_time, animate)

   count: -> 1


Properly initialize default category names in rubric editor

--- app/assets/javascripts/rubricEditor.js.coffee
+++ app/assets/javascripts/rubricEditor.js.coffee
@@ -375,7 +375,7 @@
   initializeDefault: ->
     @gradingMode('average')
     @finalComment('')
-    @feedbackCategories([new FeedbackCategory(this,'Strengths'),new FeedbackCategory(this,'Weaknesses'),new FeedbackCategory(this,'Other comments')])
+    @feedbackCategories([new FeedbackCategory(this, {name: 'Strengths', id:0}),new FeedbackCategory(this, {name:'Weaknesses', id:1}),new FeedbackCategory(this, {name:'Other comments', id:2})])

     page = new Page(this)
     page.initializeDefault()
@@ -488,4 +488,4 @@

 jQuery ->
-  new RubricEditor
+  new RubricEditor()


Another CS style fix

Another CS style fix

--- app/assets/javascripts/users_select.js.coffee
+++ app/assets/javascripts/users_select.js.coffee
@@ -56,7 +56,7 @@
         selectable: true
         fieldName: $dropdown.data('field-name')
         toggleLabel: (selected) ->
-          if selected && selected.id?
+          if selected && 'id' of selected
             selected.name
           else
             defaultLabel


Cleaning up CLI a bit

Cleaning up CLI a bit

--- src/cli.coffee
+++ src/cli.coffee
@@ -3,9 +3,10 @@

 cmd = require "commander"
 commands = require './'
-
 cmd.version(require("../package.json").version)

+
+# Helper to parse shorthand resource names for aliasing
 parseAlias = (input) ->
     switch input
         when 'c' then 'client'
@@ -15,7 +16,7 @@
         else input

-# Alias handling
+# Alias creation
 cmd.command('alias <alias> <resource>')
     .description('Create an alias for a Harvest resource. Shortcut: a')
     .option('-t, --type <type>', 'specify the resource type for which to create an alias. Default: project')
@@ -23,29 +24,35 @@
         commands.alias cmd.args[0], cmd.args[1], parseAlias program.type

+# Alias listing
 cmd.command('list <resource>')
     .description('Show all aliases for a Harvest resource. Shortcut: la')
     .action (prog, args, program) ->
         commands.aliases parseAlias cmd.args[0]

-# Summary reporting
+# Summary reporting (range)
 cmd.command('range <fromDate> <toDate>')
     .description('Show logged time for a date range.')
     .action ->
         commands.range cmd.args[0], cmd.args[1]

+
+# Summary reporting (single day)
 cmd.command('day [date]')
     .description('Show logged time for a date (default = today). Shortcut: d')
     .action ->
         commands.day cmd.args[0]

+
+# Summary reporting (week)
 cmd.command('week [date]')
     .description('Show logged time for the week of a date (default = today). Shortcut: w')
     .action ->
         commands.week cmd.args[0]

-# Time tracking
+
+# Start time tracking
 cmd.command('start <project> [task] [time] [note]')
     .description('Start a new timer for a given task. Project and task can be passed as a single string in dot notation (i.e. project.task) or as two separate arguments. Optionally adding initial time spent to the timer and/or a note. Shortcut: s')
     .action ->
@@ -61,10 +68,13 @@

         commands.start taskString, time, note

+
+# Pause/stop time tracking
 cmd.command('pause')
     .description('Stops the last running timer. Shortcut: p')
     .action ->
         commands.pause()
+

 # The function is executed every time user runs `bin/sow`
 exports.run = ->
@@ -76,6 +86,9 @@
         when 'a' then 'alias'
         when 'd' then 'day'
         when 'w' then 'week'
+        when 's' then 'start'
+        when 'p' then 'pause'
+        when 'stop' then 'pause'
         else command

     args[2] = fullCommand if fullCommand?


Fix exception on ISEN Konnector, refs #203

--- server/konnectors/isen.coffee
+++ server/konnectors/isen.coffee
@@ -75,19 +75,24 @@

     fetchIcs: (requiredFields, callback) ->

-        baseMainUrl = 'https://web.isen-bretagne.fr/cc/PublishVCalendar'
-        [firstPart, secondPart] = requiredFields.email.split '@'
-        [firstname, lastname] = firstPart.split '.'
-
-        options =
-            method: 'GET'
-            jar: true
+        # return error if email is not supplied or wrongly formated
+        try
+            [firstPart, secondPart] = requiredFields.email.split '@'
+            [firstname, lastname] = firstPart.split '.'
+        catch error
+            firstname = ''
+            lastname  = ''

         if firstname isnt '' and lastname isnt ''
+            baseMainUrl = 'https://web.isen-bretagne.fr/cc/PublishVCalendar'
             fetchUrl = "#{baseMainUrl}/#{firstname}.#{lastname}.ics"
             log.debug "Fetching #{fetchUrl}"
-            options.uri = fetchUrl
-            options.timeout = 7000
+
+            options =
+                method: 'GET'
+                jar: true
+                uri: fetchUrl
+                timeout: 7000
             request options, (err, res, body) ->

                 if err?


Edit in Metahaven bio

Edit in Metahaven bio

Holy —> Holly

--- maps/contributors.coffee
+++ maps/contributors.coffee
@@ -201,7 +201,7 @@
     name: 'Metahaven'
     location: 'Amsterdam'
     links: ['[metahaven.net](http://metahaven.net/Site/Metahaven.html)', '[@mthvn](https://twitter.com/mthvn)']
-    bio: 'Metahaven operates on the cutting blade between aesthetics and (geo)politics. Recent books include _Black Transparency_ (2015), _Can Jokes Bring Down Governments?_ (2013), and _Uncorporate Identity_ (2010). They created products for WikiLeaks, music videos with progressive EDM composer Holy Herndon, and are currently finishing a moving image project about perception, propaganda and the internet to be released online early 2016.'
+    bio: 'Metahaven operates on the cutting blade between aesthetics and (geo)politics. Recent books include _Black Transparency_ (2015), _Can Jokes Bring Down Governments?_ (2013), and _Uncorporate Identity_ (2010). They created products for WikiLeaks, music videos with progressive EDM composer Holly Herndon, and are currently finishing a moving image project about perception, propaganda and the internet to be released online early 2016.'
     azone_username: ''
     azone_reviews: []
     azone_funds: []


Add a spec for line wrapping in the presence of legacy scrollbars

--- spec/editor-view-spec.coffee
+++ spec/editor-view-spec.coffee
@@ -1806,6 +1806,17 @@
         runs ->
           expect(editor.getSoftWrapColumn()).toBeLessThan previousSoftWrapColumn

+      it "accounts for the width of the scrollbar if there is one", ->
+        # force the scrollbar to always be visible, regardless of OS visibility setting
+        $('#jasmine-content').prepend """
+          <style>
+            ::-webkit-scrollbar { width: 15px; }
+          </style>
+        """
+        setEditorHeightInLines(editorView, 5)
+        setEditorWidthInChars(editorView, 40)
+        expect(editor.lineForScreenRow(2).text.length).toBe 34
+
   describe "gutter rendering", ->
     beforeEach ->
       editorView.attachToDom(heightInLines: 5.5)


Copy repository to temp folder

Copy repository to temp folder

--- spec/editor-view-spec.coffee
+++ spec/editor-view-spec.coffee
@@ -2413,19 +2413,20 @@
       expect(editorView.getFirstVisibleScreenRow()).toBe(0)

   describe ".checkoutHead()", ->
-    [filePath, originalPathText] = []
+    [filePath] = []

     beforeEach ->
-      filePath = atom.project.resolve('git/working-dir/file.txt')
-      originalPathText = fs.readFileSync(filePath, 'utf8')
+      workingDirPath = temp.mkdirSync('atom-working-dir')
+      fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath)
+      fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git'))
+      atom.project.setPath(workingDirPath)
+      filePath = atom.project.resolve('file.txt')
+
       waitsForPromise ->
         atom.workspace.open(filePath).then (o) -> editor = o

       runs ->
         editorView.edit(editor)
-
-    afterEach ->
-      fs.writeFileSync(filePath, originalPathText)

     it "restores the contents of the editor view to the HEAD revision", ->
       editor.setText('')
@@ -2440,7 +2441,7 @@
         fileChangeHandler.callCount > 0

       runs ->
-        expect(editor.getText()).toBe(originalPathText)
+        expect(editor.getText()).toBe('undefined')

   describe ".pixelPositionForBufferPosition(position)", ->
     describe "when the editor view is detached", ->


Return early on token error

Return early on token error

--- src/stars.coffee
+++ src/stars.coffee
@@ -38,11 +38,10 @@
     else
       requestSettings.url = "#{config.getAtomApiUrl()}/stars"
       Login.getTokenOrLogin (error, token) =>
-        if token?
-          requestSettings.headers = authorization: token
-          @requestStarredPackages(requestSettings, callback)
-        else
-          callback(error)
+        return callback(error) if error?
+
+        requestSettings.headers = authorization: token
+        @requestStarredPackages(requestSettings, callback)

   requestStarredPackages: (requestSettings, callback) ->
     request.get requestSettings, (error, response, body={}) ->


Allow colspan and rowspan attributes, refs #87

--- server/utils/jwz_tools.coffee
+++ server/utils/jwz_tools.coffee
@@ -48,7 +48,13 @@
             'head'
             'meta'
         ]
-        safeAttributes = ['style', 'class', 'background', 'itemscope', 'itemtype', 'itemprop', 'content']
+        safeAttributes = [
+            'style', 'class', 'background',
+            # tables
+            'colspan', 'rowspan',
+            #microdata
+            'itemscope', 'itemtype', 'itemprop', 'content'
+        ]
         allowedAttributes = sanitizeHtml.defaults.allowedAttributes
         allowedTags.forEach (tag) ->
             if allowedAttributes[tag]?


:bug: Render views on status change

:bug: Render views on status change

--- lib/editor-linter.coffee
+++ lib/editor-linter.coffee
@@ -32,6 +32,7 @@
     @_status = status
     if not status
       @_messages.clear()
+      @linter.views.render()

   getMessages: ->
     @_messages


Fix repo.relativize on Windows.

Fix repo.relativize on Windows.

--- src/git.coffee
+++ src/git.coffee
@@ -98,7 +98,11 @@

 Repository::relativize = (path) ->
   return path unless path?
-  return path unless path[0] is '/'
+
+  if process.platform is 'win32'
+    path = path.replace(/\\/g, '/')
+  else
+    return path unless path[0] is '/'

   workingDirectory = @getWorkingDirectory()
   if workingDirectory and path.indexOf("#{workingDirectory}/") is 0


Send JSON instead of letting jQuery pass it through $.param

--- apps/about/client/edit.coffee
+++ apps/about/client/edit.coffee
@@ -13,7 +13,8 @@
   $.ajax
     type: 'POST'
     url: '/about/edit'
-    data: data
+    data: JSON.stringify(data)
+    contentType: 'application/json'
     error: -> alert "Whoops. Something went wrong, try again. If it doesn't work ask Craig."
     success: -> location.assign '/about'


fontkit removed widthOfString method

fontkit removed widthOfString method

--- lib/font/embedded.coffee
+++ lib/font/embedded.coffee
@@ -6,46 +6,46 @@
     @subset = @font.createSubset()
     @unicode = [[0]]
     @widths = [@font.getGlyph(0).advanceWidth]
-
+
     @name = @font.postscriptName
     @scale = 1000 / @font.unitsPerEm
     @ascender = @font.ascent * @scale
     @descender = @font.descent * @scale
     @lineGap = @font.lineGap * @scale
     @bbox = @font.bbox
-
+
   encode: (text, features) ->
     {glyphs, positions} = @font.layout text, features
-
+
     res = []
     for glyph, i in glyphs
       gid = @subset.includeGlyph glyph.id
       res.push ('0000' + gid.toString(16)).slice(-4)
-
+
       @widths[gid] ?= glyph.advanceWidth * @scale
       @unicode[gid] ?= glyph.codePoints
-
+
       for key of positions[i]
         positions[i][key] *= @scale
-
+
       positions[i].advanceWidth = glyph.advanceWidth * @scale
-
+
     return [res, positions]
-
+
   widthOfString: (string, size, features) ->
-    width = @font.widthOfString string, features
+    width = @font.layout(string, features).advanceWidth
     scale = size / @font.unitsPerEm
     return width * scale
-
+
   embed: ->
     isCFF = @subset.cff?
     fontFile = @document.ref()
-
+
     if isCFF
       fontFile.data.Subtype = 'CIDFontType0C'
-
+
     @subset.encodeStream().pipe(fontFile)
-
+
     familyClass = (@font['OS/2']?.sFamilyClass or 0) >> 8
     flags = 0
     flags |= 1 << 0 if @font.post.isFixedPitch
@@ -53,11 +53,11 @@
     flags |= 1 << 2 # assume the font uses non-latin characters
     flags |= 1 << 3 if familyClass is 10
     flags |= 1 << 6 if @font.head.macStyle.italic
-
+
     # generate a random tag (6 uppercase letters. 65 is the char code for 'A')
     tag = (String.fromCharCode Math.random() * 26 + 65 for i in [0...6]).join ''
     name = tag + '+' + @font.postscriptName
-
+
     bbox = @font.bbox
     descriptor = @document.ref
       Type: 'FontDescriptor'
@@ -70,14 +70,14 @@
       CapHeight: (@font.capHeight or @font.ascent) * @scale
       XHeight: (@font.xHeight or 0) * @scale
       StemV: 0 # not sure how to calculate this
-
+
     if isCFF
       descriptor.data.FontFile3 = fontFile
     else
       descriptor.data.FontFile2 = fontFile
-
+
     descriptor.end()
-
+
     descendantFont = @document.ref
       Type: 'Font'
       Subtype: if isCFF then 'CIDFontType0' else 'CIDFontType2'
@@ -88,9 +88,9 @@
         Supplement: 0
       FontDescriptor: descriptor
       W: [0, @widths]
-
+
     descendantFont.end()
-
+
     @dictionary.data =
       Type: 'Font'
       Subtype: 'Type0'
@@ -98,36 +98,36 @@
       Encoding: 'Identity-H'
       DescendantFonts: [descendantFont]
       ToUnicode: @toUnicodeCmap()
-
+
     @dictionary.end()
-
+
   toHex = (codePoints...) ->
     codes = for code in codePoints
       ('0000' + code.toString(16)).slice(-4)
-
+
     return codes.join ''
-
+
   # Maps the glyph ids encoded in the PDF back to unicode strings
   # Because of ligature substitutions and the like, there may be one or more
   # unicode characters represented by each glyph.
   toUnicodeCmap: ->
     cmap = @document.ref()
-
+
     entries = []
     for codePoints in @unicode
       encoded = []
-
+
       # encode codePoints to utf16
       for value in codePoints
         if value > 0xffff
           value -= 0x10000
           encoded.push toHex value >>> 10 & 0x3ff | 0xd800
           value = 0xdc00 | value & 0x3ff
-
+
         encoded.push toHex value
-
+
       entries.push "<#{encoded.join ' '}>"
-
+
     cmap.end """
       /CIDInit /ProcSet findresource begin
       12 dict begin
@@ -150,7 +150,7 @@
       end
       end
     """
-
+
     return cmap
-
+
 module.exports = EmbeddedFont


改进bug

改进bug

--- lib/helper/assert.coffee
+++ lib/helper/assert.coffee
@@ -8,7 +8,7 @@
   # 检测某字段是否与指定的值是否相同，如果不同则报错
   equal: (field, value, _obj, msg) ->
     (req, res, next) ->
-      return next(Error req.i18n.t msg) if req.hooks[_obj][field] isnt value
+      return next(Error msg) if req.hooks[_obj][field] isnt value
       next()

   # 检测某个字段是否在某个数组里包含
@@ -18,14 +18,13 @@
       value2 = req.hooks[obj2][key2]
       value2 = value2.split(',') if _.isString value2
       value2 = _.map(value2, (x) -> +x) if _.isNumber value1
-      return next(Error req.i18n.t msg) unless value1 in value2
+      return next(Error msg) unless value1 in value2
       next()

   # 检测是否存在
   exists: (hook, msg = null, allowNull = no, deleteKey) ->
     (req, res, next) ->
       model = req.hooks[hook]
-      msg = msg or req.i18n.t msg
       unless model
         if allowNull
           delete req.params[deleteKey]


emit error object on process.on('uncaughtException') in renderer

Chromium added the actual `error` object to the 5th parameter in `window.onerror`. This should be emitted as opposed to the `errorMsg` string. Worse yet, since `errorMsg` wasn't an instance of `Error`, any code that assumes this in any `process.on('uncaughtException')` handler would fail. Also, correctly emitting the error provides the stack trace.

This is now an HTML5 standard.

Sources:
- https://html.spec.whatwg.org/multipage/webappapis.html#errorevent
- https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror
- https://code.google.com/p/chromium/issues/detail?id=147127

--- atom/renderer/lib/init.coffee
+++ atom/renderer/lib/init.coffee
@@ -77,7 +77,7 @@
     global.__dirname = __dirname

   # Redirect window.onerror to uncaughtException.
-  window.onerror = (error) ->
+  window.onerror = (message, filename, lineno, colno, error) ->
     if global.process.listeners('uncaughtException').length > 0
       global.process.emit 'uncaughtException', error
       true


Pass card information to view.

Pass card information to view.

--- client/app/Applications/Account.kdapplication/account/accSettingsPaymentHistory.coffee
+++ client/app/Applications/Account.kdapplication/account/accSettingsPaymentHistory.coffee
@@ -28,6 +28,8 @@
             currency   : 'USD'
             createdAt  : t.datetime
             paidVia    : t.card or ""
+            cardType   : t.cardType
+            cardNumber : t.cardNumber
             owner      : t.owner
             refundable : t.refundable
         @instantiateListItems transactions


Client/Tests: added hubspot into parallel tests

--- scripts/test-instance/parallel-sets.coffee
+++ scripts/test-instance/parallel-sets.coffee
@@ -22,6 +22,7 @@
 module.exports = [

   [
+    { name: 'hubspot' }
     { name: 'register' }
     { name: 'login' }
     { name: 'logout' }


Make dashboard not break if a primary isn't a member of the cluster anymore.

--- admin/static/coffee/dashboard.coffee
+++ admin/static/coffee/dashboard.coffee
@@ -38,14 +38,19 @@
                     shards: status('shards').map(
                         r.range(), config('shards'), (shard, pos, conf_shard) ->
                             primary_id = conf_shard('primary_replica')
-                            primary_name = server_config.get(primary_id)('name')
-                            position: pos.add(1)
-                            num_shards: status('shards').count()
-                            primary_id: primary_id
-                            primary_name: primary_name
-                            primary_state: shard('replicas').filter(
-                                server: primary_name
-                            )('state')(0)
+                            server_config.get(primary_id)('name').default(null).do((primary_name) ->
+                                position: pos.add(1)
+                                num_shards: status('shards').count()
+                                primary_id: primary_id
+                                primary_name: primary_name
+                                primary_state: r.branch(
+                                    primary_name.eq(null),
+                                    'MISSING',
+                                    shard('replicas').filter(
+                                        server: primary_name
+                                    )('state')(0)
+                                )
+                           )
                     ).filter((shard) ->
                         r.expr(['ready', 'looking_for_primary_replica'])
                             .contains(shard('primary_state')).not()


Removing weird methods scraped from mozilla docs

--- src/stubs.coffee
+++ src/stubs.coffee
@@ -282,22 +282,6 @@

-@name italics
-@methodOf String#
-###
-###*
-Non-standard
-
-
-
-@name link
-@methodOf String#
-###
-###*
-Non-standard
-
-
-
 @name small
 @methodOf String#
 ###


Use Project::getPaths() and Project::setPaths()

--- src/window-event-handler.coffee
+++ src/window-event-handler.coffee
@@ -31,9 +31,9 @@
     @subscribe $(window), 'blur', -> document.body.classList.add('is-blurred')

     @subscribeToCommand $(window), 'window:open-path', (event, {pathToOpen, initialLine, initialColumn}) ->
-      unless atom.project?.getPath()
+      unless atom.project?.getPaths().length
         if fs.existsSync(pathToOpen) or fs.existsSync(path.dirname(pathToOpen))
-          atom.project?.setPath(pathToOpen)
+          atom.project?.setPaths([pathToOpen])

       unless fs.isDirectorySync(pathToOpen)
         atom.workspace?.open(pathToOpen, {initialLine, initialColumn})


fix(beforeunload): Chrome 51 - return undefined to continue

--- src/window-event-handler.coffee
+++ src/window-event-handler.coffee
@@ -38,8 +38,10 @@
       # Don't hide the window here if we don't want the renderer process to be
       # throttled in case more work needs to be done before closing

-      # In Electron, returning false cancels the close.
-      return @runUnloadCallbacks()
+      # In Electron, returning any value other than undefined cancels the close.
+      canCloseWindow = @runUnloadCallbacks()
+      return undefined if canCloseWindow
+      return false

     window.onunload = =>
       NylasEnv.storeWindowDimensions()


use underscore's uniqueId method

use underscore's uniqueId method

--- extension/chrome/js/src/content.coffee
+++ extension/chrome/js/src/content.coffee
@@ -166,7 +166,7 @@
 										</div>
 									</div>
 									"""
-						element.attr 'id', MailButler.ID_PREFIX + (Math.random()+"").substr 2
+						element.attr 'id', _.uniqueId MailButler.ID_PREFIX

 						toggle element, selectedClass, checked, false


Steam api key is a process env

Steam api key is a process env

--- lib/js/steambuddy.coffee
+++ lib/js/steambuddy.coffee
@@ -9,7 +9,7 @@
 async = require('async')

 STEAM_API_TOKEN = process.env.STEAM_API_TOKEN
-PLAYER_SUMMARY_URL = process.env.PLAYER_SUMMARY_URL + STEAM_API_KEY + '&steamids='
+PLAYER_SUMMARY_URL = process.env.PLAYER_SUMMARY_URL + process.env.STEAM_API_KEY + '&steamids='

 integrations = []


'Updating for version 0.12.6dev7'

'Updating for version 0.12.6dev7'

--- bokehjs/src/coffee/version.coffee
+++ bokehjs/src/coffee/version.coffee
@@ -1 +1 @@
-export version = '0.12.6dev6'
+export version = '0.12.6dev7'


Highlight error lines.

Highlight error lines.

--- lib/linter-pylama.coffee
+++ lib/linter-pylama.coffee
@@ -161,7 +161,7 @@
       options: {cwd: path.dirname originFileName}

-  lintFile: (lintInfo, callback) ->
+  lintFile: (lintInfo, textEditor, callback) ->
     results = []
     stdout = (data) ->
       console.log data if do atom.inDevMode
@@ -175,13 +175,15 @@
           "Error"
         else if match.warning
           "Warning"
+        line = textEditor.buffer.lines[match.line-1]
+        colEnd = line.length if line
         messages.push {
           type: type or 'Warning'
           text: match.message
           filePath: lintInfo.fileName
           range: [
             [match.line - 1, 0]
-            [match.line - 1, 0]
+            [match.line - 1, colEnd]
           ]
         }
       callback(messages)
@@ -214,7 +216,7 @@
         fs.write tmpInfo.fd, do textEditor.getText, (err) =>
           return reject(err) if err
           lintInfo = @makeLintInfo tmpInfo.path, filePath
-          @lintFile lintInfo, (results) ->
+          @lintFile lintInfo, textEditor, (results) ->
             fs.unlink tmpInfo.path
             resolve results

@@ -222,7 +224,7 @@
   lintOnSave: (textEditor) =>
     return new Promise (resolve, reject) =>
       lintInfo = @makeLintInfo do textEditor.getPath
-      @lintFile lintInfo, (results) ->
+      @lintFile lintInfo, textEditor, (results) ->
         resolve(results)


fix verbose issue

fix verbose issue

--- salvus/misc_node.coffee
+++ salvus/misc_node.coffee
@@ -435,7 +435,7 @@

         if opts.verbose
             winston.debug("finished exec of #{opts.command}")
-        winston.debug("stdout=#{stdout},stderr=#{stderr},exit_code=#{exit_code}")
+            winston.debug("stdout=#{stdout},stderr=#{stderr},exit_code=#{exit_code}")
         if not opts.err_on_exit and ran_code
             # as long as we made it to running some code, we consider this a success (that is what err_on_exit means).
             opts.cb?(false, {stdout:stdout, stderr:stderr, exit_code:exit_code})


fix broken test suite

fix broken test suite

--- salvus/page/r_help.cjsx
+++ salvus/page/r_help.cjsx
@@ -514,10 +514,11 @@

 exports.render_help_page = () ->
     React.render(<HelpPage />, document.getElementById('salvus-help'))
+    # also setup a listener for switching to the page. (TODO: temporary until react-router...)
+    require('top_navbar').top_navbar.on "switch_to_page-salvus-help", () ->
+        window.history.pushState("", "", window.salvus_base_url + '/help')

 exports._test =
     HelpPageSupportSection : HelpPageSupportSection
     SUPPORT_LINKS : SUPPORT_LINKS

-require('top_navbar').top_navbar.on "switch_to_page-salvus-help", () ->
-    window.history.pushState("", "", window.salvus_base_url + '/help')


Fix school contests

Fix school contests

--- server/materials/data/level_sch2021.coffee
+++ server/materials/data/level_sch2021.coffee
@@ -44,6 +44,6 @@
         label("Нижегородская область относилась ко 2 группе регионов")
         sch2021_1(),
         sch2021_2(),
-        sch2021_2(),
-        sch2021_2(),
-    ])+        sch2021_3(),
+        sch2021_4(),
+    ])


ui: Also add Suite topic header

ui: Also add Suite topic header

--- ui/main.coffee
+++ ui/main.coffee
@@ -22,7 +22,7 @@
   render: () ->
     (div {className: 'suite-header'}, [
       label {}, @props.name
-      label {}, @props.component
+      label {}, @props.topic if @props.name != @props.topic
     ])
 SuiteHeader = React.createFactory SuiteHeaderClass

@@ -39,13 +39,16 @@
 { ul, li } = React.DOM
 class TestsListingClass
   render: () ->
-    createItem = (testcase) ->
-      (li {}, [TestCaseListing testcase])
+    createSuite = (suite) ->
+      items = []
+      items.push (SuiteHeader suite)
+      for testcase in suite.cases
+        items.push (li {}, [TestCaseListing testcase])
+      return items
     (ul {className: 'horizontal-list'}, [
-      @props.cases.map createItem
+      @props.suites.map createSuite
     ])
 TestsListing = React.createFactory TestsListingClass
-# TODO: take the suites instead of cases as props

 # Main
@@ -54,10 +57,8 @@

   fixture = id('fixture-microflo-toggleanimation').innerHTML
   suite = fbpspec.testsuite.loadYAML fixture
-
-#  React.render (SuiteHeader {name: 'Suite of tests', component: 'ComponentName' }), document.body
-
-  React.render (TestsListing {cases: suite.cases}), document.body
+
+  React.render (TestsListing {suites: [suite]}), document.body
   console.log 'rendered'

 main()


Added the 'Align' feature to the image fit option. now when you set an image to fit inside a container, you can make it center or right align (left is default, so needs no aligning)

--- lib/mixins/images.coffee
+++ lib/mixins/images.coffee
@@ -49,6 +49,12 @@
             else
                 h = bh
                 w = bh * ip
+
+            if options.align
+                if 'center' == options.align
+                    x = x + bw / 2 - w / 2
+                else if 'right' == options.align
+                    x = x + bw - w

         # Set the current y position to below the image if it is in the document flow
         @y += h if @y is y


Removed unnecessary code

Removed unnecessary code

--- lib/debug.coffee
+++ lib/debug.coffee
@@ -72,7 +72,6 @@
     Debug.wrap {disabledExtra: tag}
   isExtraEnabled: (tag) ->
     return false if !tag
-    @extra tag
     extraOpts.has tag
   getAllExtraOptionsAsArray: ->
     Array.from extraOpts


добавлен coffeekup плагинобновилена версия до 0.1.0

--- lib/nbuild/nbuild.coffee
+++ lib/nbuild/nbuild.coffee
@@ -15,7 +15,7 @@

 {join, existsSync} = path

-VERSION = "0.1"
+VERSION = "0.1.0"
 BANNER = 'Usage: nbuild [options] command[:step]'
 SWITCHES = [
   ['-c', '--config [FILE*]',    'path to .nproj file']


removing no longer used region extension

--- app/assets/coffee/app.coffee
+++ app/assets/coffee/app.coffee
@@ -1,4 +1,4 @@
-define ["marionette", "apps/config/marionette/regions/dialog", "apps/config/marionette/regions/modal"], (Marionette) ->
+define ["marionette", "apps/config/marionette/regions/dialog"], (Marionette) ->

   Swabcast = new Marionette.Application()
   Swabcast.debugging = true


Link to talk subjects with just ^

Link to talk subjects with just ^

...when inside a project.

Closes #882

--- app/components/markdown.cjsx
+++ app/components/markdown.cjsx
@@ -35,7 +35,13 @@
       # \b[\w-]+\b is hyphen boundary for slugs
       .replace /@(\b[\w-]+\b)\/(\b[\w-]+\b)\^([0-9]+)/g, "$1/$2 - Subject $3"

-      # user mentions : @username
+      .replace(/\^([0-9]+)/g, (_, subjectID) ->
+        if owner and name
+          "#{owner}/#{name} - Subject #{subjectID}"
+        else
+          subjectID)
+
+     # user mentions : @username
       .replace /\B@(\b[\w-]+\b)/g, "@$1"

       # hashtags #tagname


Add white background and padding to infobox

--- app/assets/javascripts/map.js.coffee
+++ app/assets/javascripts/map.js.coffee
@@ -22,6 +22,9 @@
     pixelOffset: new google.maps.Size(-140, 0)
     boxStyle:
       width: "280px"
+      'background-color': 'white'
+      'border-radius': '2px'
+      'padding': '5px'

 # handler = Gmaps.build("Google", builders: { Marker: CustomMarkerBuilder } )
 # handler.buildMap { internal: id: "custom_builder" }, ->


Small continuity tweak. Instead of asking if you are sure you want to unsubscribe it asks if you are sure you want to unfollow.

--- src/users.coffee
+++ src/users.coffee
@@ -1,3 +1,3 @@
 $(document).ready ->
   $(".unfollow").click ->
-    confirm "Are you sure you want to unsubscribe from this user?"
+    confirm "Are you sure you want to unfollow this this user?"


Fix row rendering after completion add

Fix row rendering after completion add

--- app/assets/volume/spreadsheet.coffee
+++ app/assets/volume/spreadsheet.coffee
@@ -303,7 +303,7 @@
                 td.appendChild(document.createTextNode("add " + cat.name))
               if editing
                 if cols > 1 && m != undefined
-                  generateCell(row, undefined, undefined, id+'-rec_'+i+'_'+n+'_'+m)
+                  generateCell(row, undefined, undefined, id+'-rec_'+i+'_'+(n||0)+'_'+m)
                   row.appendChild(td)
                   td.setAttribute("colspan", cols-1)
                 td.className = 'null add'
@@ -565,7 +565,7 @@
           saveRun cell, act.then (record) ->
             if record
               r = record.id
-              info.n = inc(counts[info.i], info.c) unless 'n' of info
+              info.n = inc(counts[info.i], info.c) unless info.record

               for m, rcm of records[info.c]
                 v = if m of record then record[m] else record.measures[m]


Faster way to render tasks

Faster way to render tasks

--- app/controllers/tasks.coffee
+++ app/controllers/tasks.coffee
@@ -82,11 +82,6 @@
     , 300
     console.timeEnd "release"

-    console.time "empty"
-    @tasks.html("")
-    console.timeEnd "empty"
-
-
     console.time "getTasks"
     if list.id is "filter"
       tasks = list.tasks
@@ -121,7 +116,7 @@
     console.timeEnd "sort"

     console.time "html"
-    @tasks.html html
+    @tasks[0].innerHTML = html
     console.timeEnd "html"

     console.time "sortable"


fix dropbutton to allow it to peform the same action multiple times in a row

--- app/components/eui-dropbutton.coffee
+++ app/components/eui-dropbutton.coffee
@@ -14,7 +14,8 @@
   # If the selection changes peform the action
   secondaryAction: (->
     action = @get('selection.action')
-    @triggerAction({action})
+    @triggerAction({action}) if action
+    @set('selection', null)
   ).observes 'selection'

   actions:
@@ -25,6 +26,7 @@
           isOpenBinding: 'targetObject.popupIsOpen'
           selectionBinding: 'targetObject.selection'
           options: @get('options')
+          labelPath: 'label'
           style: 'bubble'

     primaryAction: ->


color param ui

color param ui

--- src/ubret/sw_viewer.coffee
+++ src/ubret/sw_viewer.coffee
@@ -99,6 +99,7 @@
     )

   getNextSubject: =>
+    console.log 'getNextSubject', @currentPageData()
     @requestChannels()

   # Request FITS files for each channel
@@ -123,6 +124,7 @@

       for band, index in @bands
         do (band, index) =>
+          console.log @currentPageData()[0]
           path = "#{@source}#{prefix}_#{band}.fits.fz"

           new astro.FITS.File(path, (fits) =>
@@ -184,6 +186,7 @@
     max     = @opts.extent?.max or gMax

     @wfits.setScales.apply(@wfits, scales)
+    console.log scales
     @wfits.setAlpha(alpha)
     @wfits.setQ(Q)
     @wfits.setExtent(min, max)


removing prior-state

removing prior-state

Former-commit-id: 2405226a62d72561087504c18e177998fa5c99ae [formerly d123fbd31630992f80993f7188d317d87c5ca3b7] [formerly 018b891dc0913fff3b92d1e29ca119120b1419fa [formerly b5cfacab4a128a0dde5c8005c100089a84a91d21]] [formerly 8585483b7fd195f0cdc95d559de11e6a6b038288 [formerly b384ca4e53fb1edc87effc493728a0756ef21bc0] [formerly d206e08b1abe121399a7c7f809de3027ee8ec984 [formerly d206e08b1abe121399a7c7f809de3027ee8ec984 [formerly 680ed0cc923097ca3a213d671448840a641bad48]]]]
Former-commit-id: 45c9750f935f142ca95ee5e46b4df6773d227825 [formerly cf37a99c98711adb6fa5fa31807ac607d4c306c4] [formerly 38db930c29241cb9b6dc4e5c725a40ba6144f618 [formerly 1f84f23746dd69c50f792de366518cad9f7d2b19]]
Former-commit-id: 24689417ed61bec92bad0f8c51c1b1e7f4b68938 [formerly 180f386373a59697703ef0a677b71172896daaa0]
Former-commit-id: a36eca6945da78d3f9d650df4b285a59c176eed1
Former-commit-id: a59f2cf7b9485f533f20d911fe3d1689f90083b2

--- @client/franklin.coffee
+++ @client/franklin.coffee
@@ -28,9 +28,9 @@
 #
 # These are the components and their relationships:
 #                       Proposal
-#                   /              \
-#    CommunityPoints             DecisionBoard
-#               |                  /
+#                   /      |           \            \
+#    CommunityPoints   DecisionBoard   Histogram    Slider
+#               |          |
 #               |      YourPoints
 #               |    /            \
 #              Point             NewPoint
@@ -47,7 +47,6 @@

   getDefaultProps : ->
     state : 'crafting'
-    priorstate : 'results'
     data :  # would rather have each of these data items added to top level props dict
       proposal : {}
       points : {}
@@ -98,9 +97,6 @@
             $opinion.css {position: '', top: ''}
             $cons.show()
       , 200  # delay initialization to let the rest of the dom load so that the offset is calculated properly
-
-
-

   ####
   # State-dependent styling
@@ -454,7 +450,6 @@
           # your pros
           YourPoints
             state: @props.state
-            priorstate: @props.priorstate
             points : @props.points
             included_points: @props.included_points
             valence: 'pro'
@@ -463,7 +458,6 @@
           # your cons
           YourPoints
             state: @props.state
-            priorstate: @props.priorstate
             points : @props.points
             included_points: @props.included_points
             valence: 'con'
@@ -738,12 +732,11 @@
   proposal : (long_id, state = 'crafting') ->

     if !top_level_component
-      top_level_component = React.renderComponent Proposal({state : state, priorstate : null}), document.getElementById('l_content_main_wrap')
+      top_level_component = React.renderComponent Proposal({state : state}), document.getElementById('l_content_main_wrap')
       fetch {url: Routes.proposal_path long_id}, (data) => save {data}
     else
       top_level_component.setProps
         state : state
-        priorstate : top_level_component.props.state

   results : (long_id) -> @proposal long_id, 'results'


Extracted highlightFacts method and always trigger factlink.factsLoaded

--- app/js/util/highlight.coffee
+++ app/js/util/highlight.coffee
@@ -1,45 +1,40 @@
 Factlink.Facts = []
+
+highlightFacts = (facts_data) ->
+  # If there are multiple matches on the page, loop through them all
+  for fact_data in facts_data
+    # Select the ranges (results)
+    ranges = Factlink.search(fact_data.displaystring)
+    $.merge Factlink.Facts, Factlink.selectRanges(ranges, fact_data.id)
+
+  Factlink.trigger "factlink.factsLoaded", facts_data

 # Function which will collect all the facts for the current page
 # and select them.
 # Returns deferred object
 fetchFacts = (siteUrl=Factlink.siteUrl()) ->
-  # The URL to the Factlink backend
-  src = FactlinkConfig.api + "/site?url=" + encodeURIComponent(siteUrl)
-
   $.ajax
-    url: src
+    # The URL to the Factlink backend
+    url: FactlinkConfig.api + "/site?url=" + encodeURIComponent(siteUrl)
     dataType: "jsonp"
     crossDomain: true
     type: "GET"
     jsonp: "callback"
+    success: highlightFacts

 highlighting = false
-
 Factlink.startHighlighting = ->
   return if highlighting
   highlighting = true

   console.info "Factlink:", "startHighlighting"
-  fetchFacts().done (facts_data) ->
-    # If there are multiple matches on the page, loop through them all
-    for fact_data in facts_data
-      # Select the ranges (results)
-      ranges = Factlink.search(fact_data.displaystring)
-      $.merge Factlink.Facts, Factlink.selectRanges(ranges, fact_data.id)
+  fetchFacts()

-    Factlink.trigger "factlink.factsLoaded", facts_data
-
-# Don't check for highlighting here, and also don't trigger, as this is a
+# Don't check for highlighting here, as this is a
 # special hacky-patchy method for in the blog
 Factlink.highlightAdditionalFactlinks = (siteUrl) ->
   console.info "Factlink:", "highlightAdditionalFactlinks"
-  fetchFacts(siteUrl).done (facts_data) ->
-    # If there are multiple matches on the page, loop through them all
-    for fact_data in facts_data
-      # Select the ranges (results)
-      ranges = Factlink.search(fact_data.displaystring)
-      $.merge Factlink.Facts, Factlink.selectRanges(ranges, fact_data.id)
+  fetchFacts()

 Factlink.stopHighlighting = ->
   return unless highlighting


Run copy-i18n-tags

Run copy-i18n-tags

--- app/locale/rot13.coco.coffee
+++ app/locale/rot13.coco.coffee
@@ -78,7 +78,7 @@
     global_subtitle4: "Pbhagevrf"
     go_to_my_classes: "Tb gb zl pynffrf"
     go_to_my_courses: "Tb gb zl pbhefrf"
-    quotes_quote1: "Anzr nal cebtenz bayvar, V’ir gevrq vg. Abar bs gurz zngpu hc gb PbqrPbzong & Bmnevn. Nal grnpure jub jnagf gurve fghqragf gb yrnea ubj gb pbqr... fgneg urer!" # {punatr}
+    quotes_quote1: "Anzr nal cebtenz bayvar, V’ir gevrq vg. Abar bs gurz zngpu hc gb PbqrPbzong & Bmnevn. Nal grnpure jub jnagf gurve fghqragf gb yrnea ubj gb pbqr... fgneg urer!"
     quotes_quote2: " V jnf fhecevfrq nobhg ubj rnfl naq vaghvgvir PbqrPbzong znxrf yrneavat pbzchgre fpvrapr. Gur fpberf ba gur NC rknz jrer zhpu uvture guna V rkcrpgrq naq V oryvrir PbqrPbzong vf gur ernfba jul guvf jnf gur pnfr."
     quotes_quote3: "PbqrPbzong unf orra gur zbfg orarsvpvny sbe grnpuvat zl fghqragf erny-yvsr pbqvat pncnovyvgvrf. Zl uhfonaq vf n fbsgjner ratvarre naq ur unf grfgrq bhg nyy bs zl cebtenzf. Ur chg guvf nf uvf gbc pubvpr."
     quotes_quote4: "Gur srrqonpx … unf orra fb cbfvgvir gung jr ner fgehpghevat n pbzchgre fpvrapr pynff nebhaq PbqrPbzong. Gur cebtenz ernyyl ratntrf gur fghqragf jvgu n tnzvat fglyr cyngsbez gung vf ragregnvavat naq vafgehpgvbany ng gur fnzr gvzr. Xrrc hc gur tbbq jbex, PbqrPbzong!"


Goodbye for now, star button

Goodbye for now, star button

--- lib/available-package-view.coffee
+++ lib/available-package-view.coffee
@@ -22,6 +22,7 @@
           #   count = if downloads is 1 then '1 download' else "#{downloads.toLocaleString()} downloads"
           #   @span outlet: 'downloadCount', class: 'value', count
         @span class: 'stats-item', =>
+        @span class: 'stats-item hidden', =>
           @div class: 'star-box', =>
             @a outlet: 'starButton', class: 'star-button btn icon icon-star', =>
             @a outlet: 'starCount', class: 'star-count'


Add phase of a dialogue at creating a circular

--- scripts/circular.coffee
+++ scripts/circular.coffee
@@ -356,17 +356,54 @@

     # Dialogue start
     robot.emit "dialogue:start", msg.envelope.user, (message) ->
-      text = message.text
-      options =
-        user: message.user.name
-        room: message.user.room
-        content: message.text
-      circular = new Circular options
-      circular.run()
-
-      msg.send messages.pick("done_create")
-
-      @end()
+
+      done_create = =>
+        circular = new Circular @get "circular"
+        circular.run()
+        msg.send messages.pick("done_create")
+        @end()
+
+      text = "#{message.text}"
+
+      circular = @get "circular"
+
+      if circular
+        switch @get "phase"
+          when "options:start:confirm"
+            if /yes|はい|する/i.test text
+              msg.send "対象ユーザーは誰ですか？ [all/...users...]"
+              @set "phase", "options:users"
+            else
+              @set "phase", "options:end"
+              done_create()
+          when "options:users"
+            if /^all|全員$/i.test text
+              msg.send "全員ですね"
+              @set "phase", "options:end"
+              done_create()
+            else
+              tokens = text.split /\s+/
+              circular.users = _.map tokens, (token) ->
+                token.replace /@/g, ""
+              circular.users = _.intersection Circular.users(), circular.users
+              if circular.users.length
+                msg.send "#{circular.users.join ' '} の #{circular.users.length} 人ですね。"
+                @set "circular", circular
+                @set "phase", "options:end"
+                done_create()
+              else
+                msg.send "やだなぁ、有効なユーザー名じゃないですよ。"
+
+      else
+        options =
+          user: message.user.name
+          room: message.user.room
+          content: message.text
+
+        @set "circular", options
+        msg.send "オプション設定をしますか？ [yes/no]"
+        @set "phase", "options:start:confirm"
+

   robot.respond /circular:add/i, addCircular
   robot.respond /回覧板を?回して/i, addCircular


Fix reply to target

Fix reply to target

--- scripts/circular.coffee
+++ scripts/circular.coffee
@@ -231,8 +231,8 @@
         return

       Circular.onDialogue = true
-      robot.send {room: @room}, "#{@user} さん、" + messages.pick("greet") +
-        "回覧板 No.#{@id} *#{@title}*は読まれましたか？ [yes/no/what]\n"
+      robot.send {room: @room}, "@#{user} さん、" + messages.pick("greet") +
+        "回覧板 No.#{@id} *#{@title}* は読まれましたか？ [yes/no/what]\n"

       # timeout for non-response from user
       timeout_id = setTimeout =>


force parseInt on points and totalPoints in quest/completed view

--- www/src/views/quest/completed.coffee
+++ www/src/views/quest/completed.coffee
@@ -24,8 +24,10 @@
         serialize: ->
             params = super
             params.gotTwitter = Boolean(@user.get("twitter"))
+            params.points = parseInt(params.points)
             params.totalPoints = @user.get("rp")[@model.get("realm")]
             params.totalPoints ?= 0
+            params.totalPoints = parseInt(params.totalPoints)
             params.totalPoints += params.points
             params


new name who dis?

new name who dis?

--- scripts/entrance.coffee
+++ scripts/entrance.coffee
@@ -12,6 +12,6 @@
       msg.send "wb #{username}"
     else if /kin/.test(username)
       msg.send "Hello #{username}! We missed you!"
-    else if /jense/i.test(username) or /prawn/i.test(username) or /sinjen/.test(username)
+    else if /jense/i.test(username) or /prawn/i.test(username) or /sinjen/.test(username) or /sen/.test(username)
       msg.send "Hello #{username}, have you heard of our lord & savior ZNC?"


auto scroll to bottom when new log arrival

--- lib/build/build-state-view.coffee
+++ lib/build/build-state-view.coffee
@@ -40,7 +40,7 @@
           @div id: 'toggle-out', =>
             @span class: 'glyphicon glyphicon-chevron-down'
             @span 'show output:'
-          @div id: 'out', style: 'height: 300px; overflow: scroll; color: white;', =>
+          @div id: 'out', outlet: 'out', style: 'height: 300px; overflow: scroll; color: white;', =>

       @div class: 'actions', =>
         @div class: 'pull-left', =>
@@ -86,17 +86,16 @@
       @find('.task-state').text job.state
       @showState(job)

-    @socket.on 'stdout', (out) =>
-      out_element = @find('#out')
-      out_element.append("
#{out}
")
-      out_element.scrollTop = out_element.scrollHeight
+    @socket.on 'stdout', (out) => @_output(out)

-    @socket.on 'stderr', (out) =>
-      out_element = @find('#out')
-      out_element.append("
#{out}
")
-      out_element.scrollTop = out_element.scrollHeight
+    @socket.on 'stderr', (out) => @_output(out, 'text-error')
+
+  _output: (content, style = '')->
+    @out.append("
#{content}
")
+    @out[0].scrollTop = @out[0].scrollHeight

   destroy: ->
+
     @socket?.disconnect()
     if @task and @task.state != 'complete'
       puzzleClient.deleteTask @task.id


Prefix removal corrected, resolves #193

Prefix removal corrected, resolves #193

--- wzk/ui/inlineform/FieldExpert.coffee
+++ wzk/ui/inlineform/FieldExpert.coffee
@@ -26,7 +26,7 @@
     @return {string}
   ###
   process: (attr) ->
-    attr.replace('_prefix_', @start.toString())
+    attr.replace('__prefix__', @start.toString())

   ###*
     Returns a model name from a given field


Fix scroll button on Chrome

Fix scroll button on Chrome

--- assets/javascripts/app.coffee
+++ assets/javascripts/app.coffee
@@ -102,7 +102,7 @@

 scrollTo = (pos) ->
   if (window.pageYOffset || doc.scrollTop)  - (doc.clientTop || 0) < pos
-    document.documentElement.scrollTop += 20
+    window.scrollBy(0, 20)
     setTimeout ->
       scrollTo(pos)
     , 5


Wait I may have been that drunk

Wait I may have been that drunk

--- app/frontend/scripts/views.js.coffee
+++ app/frontend/scripts/views.js.coffee
@@ -21,7 +21,7 @@
   #
   # Uses hawt data-artist fields to populate the artist.
   artist_tag: () ->
-    "#{@artist}"
+    "#{@artist}"

 # A listing of Songs.


Add timestamp type

Add timestamp type

--- lib/pg-hoff-types.coffee
+++ lib/pg-hoff-types.coffee
@@ -1,6 +1,12 @@
 class PgHoffTypes
     @Type:
         1184:
+            name: 'Timestamp'
+            format: (value) ->
+                return new Date(value).toLocaleString(atom.config.get('pg-hoff.locale'))
+            compare: (left, right) ->
+                return Date.parse(left) - Date.parse(right)
+        1114:
             name: 'Timestamp'
             format: (value) ->
                 return new Date(value).toLocaleString(atom.config.get('pg-hoff.locale'))


Add redirect (#5963)

Add redirect (#5963)

--- app/router.cjsx
+++ app/router.cjsx
@@ -170,6 +170,9 @@

     <Route path="/projects/mschwamb/planet-four/authors" component={() => <ExternalRedirect newUrl='https://authors.planetfour.org/' />} />

+    <Route path="/projects/mainehistory/beyond-borders-transcribing-historic-maine-land-documents" component={() => <RELOAD newUrl='https://fe-project.zooniverse.org/projects/mainehistory/beyond-borders-transcribing-historic-maine-land-documents' />} />
+    <Route path="/projects/mainehistory/beyond-borders-transcribing-historic-maine-land-documents/classify" component={() => <RELOAD newUrl='https://fe-project.zooniverse.org/projects/mainehistory/beyond-borders-transcribing-historic-maine-land-documents/classify' />} />
+
     <Route path="projects/:owner/:name" component={require('./pages/project').default}>
       <IndexRoute component={ProjectHomePage} />
       <Route path="home" component={ONE_UP_REDIRECT} />


Add additional ozaria sound options to level editor (#136)

--- app/schemas/models/level.ozar.coffee
+++ app/schemas/models/level.ozar.coffee
@@ -147,7 +147,7 @@
     suppressSelectionSounds: {type: 'boolean', title: 'Suppress Selection Sounds', description: 'Whether to suppress selection sounds made from clicking on Thangs.'}
     music: c.object {title: 'Music', description: 'Control music playing'},
       play: {title: 'Play', type: 'boolean'}
-      file: c.shortString(title: 'File', enum: ['/music/music_level_1', '/music/music_level_2', '/music/music_level_3', '/music/music_level_4', '/music/music_level_5'])
+      file: c.shortString(title: 'File', enum: ['/music/music_level_1', '/music/music_level_2', '/music/music_level_3', '/music/music_level_4', '/music/music_level_5', '/music/OzariaLoopMusic1', '/music/OzariaLoopMusic2', '/music/OzariaLoopMusic3', '/music/OzariaLoopMusic5', '/music/SpiritLandsAmbient01'])

 ScriptSchema = c.object {
   title: 'Script'


WIP: removing extra cloneData() calls to improve perf

--- client/src/app/redWire.coffee
+++ client/src/app/redWire.coffee
@@ -26,9 +26,9 @@
   # Return new results with combination of this and other
   appendWith: (other) ->
     # Don't touch @result
-    newMemoryPatches = RW.concatenate(@memoryPatches, other.memoryPatches)
-    newIoPatches = RW.concatenate(@ioPatches, other.ioPatches)
-    newLogMessages = RW.concatenate(@logMessages, other.logMessages)
+    newMemoryPatches = @memoryPatches.concat(other.memoryPatches)
+    newIoPatches = @ioPatches.concat(other.ioPatches)
+    newLogMessages = @logMessages.concat(other.logMessages)
     return new RW.ChipVisitorResult(@result, newMemoryPatches, newIoPatches, newLogMessages)

 # Class used just to "tag" a string as being a reference rather than a JSON value
@@ -102,14 +102,14 @@
   if _.isEqual(newValue, oldValue) then return patches

   if oldValue is undefined
-    patches.push { add: prefix, value: RW.cloneData(newValue), path: path }
+    patches.push { add: prefix, value: newValue, path: path }
   else if newValue is undefined
     patches.push { remove: prefix, path: path }
   else if not _.isObject(newValue) or not _.isObject(oldValue) or typeof(oldValue) != typeof(newValue)
-    patches.push { replace: prefix, value: RW.cloneData(newValue), path: path }
+    patches.push { replace: prefix, value: newValue, path: path }
   else if _.isArray(oldValue) and oldValue.length != newValue.length
     # In the case that we modified an array, we need to replace the whole thing
-    patches.push { replace: prefix, value: RW.cloneData(newValue), path: path }
+    patches.push { replace: prefix, value: newValue, path: path }
   else
     # both elements are objects or arrays
     keys = _.union(_.keys(oldValue), _.keys(newValue))
@@ -278,8 +278,8 @@
         bindingSet.push(newBindings)
   else if _.isString(chip.splitter.from)
     inputContext =
-      memory: RW.cloneData(constants.memoryData)
-      io: RW.cloneData(constants.ioData)
+      memory: constants.memoryData
+      io: constants.ioData

     [parent, key] = RW.getParentAndKey(inputContext, RW.splitAddress(chip.splitter.from))
     boundValue = parent[key]


Fix brain initializing

Fix brain initializing

--- scripts/strength.coffee
+++ scripts/strength.coffee
@@ -36,15 +36,21 @@

 module.exports = (robot) ->

-  robot.brain.data.strength = {} unless robot.brain.data.strength?
-
   respond_user_strength = (msg) ->
     [user_name, strength_label, rank_index] = msg.data
     strength_name = robot.brain.data.strength[user_name][rank_index]
     strength_title = StrengthList.getTitle(strength_name)
+    text_pares = [
+      ["やだなぁ", "じゃないですか"],
+      ["ご存知", "ですよ！"],
+      ["しびれるぅ！", "だッッッ！！"],
+      ["ご主人様、", "でございます。"],
+      ["神よ", "であることをお許し下さい。Amen"]
+    ]
+    [prefix, suffix] = text_pares[Math.floor Math.random() * text_pares.length]

     if strength_title?
-      msg.send "やだなぁ、#{user_name}の#{rank_index+1}番の#{strength_label}は *#{strength_title}* じゃないですか"
+      msg.send "#{prefix}#{user_name}の#{rank_index+1}番の#{strength_label}は *#{strength_title}* #{suffix}"
     else
       msg.send "登録されていません！"

@@ -52,6 +58,8 @@
     [user_name, strength_label, strength_text, rank_index] = msg.data
     strength_arr = strength_text.split /\s*[,、]\s*/
     ranking = []
+    robot.brain.data.strength = {} unless robot.brain.data.strength
+    robot.brain.data.strength[user_name] = []

     for value, i in strength_arr
       strength = StrengthList.get value
@@ -110,7 +118,7 @@
   robot.respond /(皆|みんな)の(強み|資質)を教えて/i, (msg) ->
     text = ""
     for user_name, strength_ranking of robot.brain.data.strength
-      text += "*#{user_name}* の強みは\n"
+      text += "*#{user_name}* の強み：\n"
       for rank_index, strength_title of strength_ranking
         rank_index = parseInt(rank_index, 10)
         text += "#{rank_index+1}位：#{strength_title}\n"


Don't retain nodes in event listeners.

Don't retain nodes in event listeners.

--- src/view/dom/events.coffee
+++ src/view/dom/events.coffee
@@ -4,18 +4,26 @@
 # DOM directives, but are used to handle specific events by the `data-event-#{name}` helper.
 Batman.DOM.events =
   click: (node, callback, context, eventName = 'click') ->
-    Batman.DOM.addEventListener node, eventName, (event, args...) ->
+    Batman.DOM.addEventListener node, eventName, Batman.DOM.events.clickCallback(callback)
+
+    if node.nodeName.toUpperCase() is 'A' and not node.href
+        node.href = '#'
+
+    node
+
+  clickCallback: (callback) ->
+    clickHandler = (event) ->
       return if event.metaKey || event.ctrlKey

       Batman.DOM.preventDefault event
-      return if not Batman.DOM.eventIsAllowed(eventName, event)
+      return if not Batman.DOM.eventIsAllowed(event.type, event)

-      callback node, event, args..., context
+      node = event.target
+      callback node, event

-    if node.nodeName.toUpperCase() is 'A' and not node.href
-      node.href = '#'
-
-    node
+    clickHandler.name = "clickHandler"
+    clickHandler.functionName = "clickHandler"
+    clickHandler

   doubleclick: (node, callback, context) ->
     # The actual DOM event is called `dblclick`
@@ -36,30 +44,39 @@
       else ['change']

     for eventName in eventNames
-      Batman.DOM.addEventListener node, eventName, (args...) ->
-        callback node, args..., context
+      Batman.DOM.addEventListener node, eventName, @changeCallback
+
+  changeCallback: -> callback node, args..., context

   isEnter: (ev) -> (13 <= ev.keyCode <= 14) || (13 <= ev.which <= 14) || ev.keyIdentifier is 'Enter' || ev.key is 'Enter'

   submit: (node, callback, context) ->
     if Batman.DOM.nodeIsEditable(node)
-      Batman.DOM.addEventListener node, 'keydown', (args...) ->
-        if Batman.DOM.events.isEnter(args[0])
-          Batman.DOM._keyCapturingNode = node
-      Batman.DOM.addEventListener node, 'keyup', (args...) ->
-        if Batman.DOM.events.isEnter(args[0])
-          if Batman.DOM._keyCapturingNode is node
-            Batman.DOM.preventDefault args[0]
-            callback node, args..., context
-          Batman.DOM._keyCapturingNode = null
+      Batman.DOM.addEventListener node, 'keydown', @keyDownCallback
+      Batman.DOM.addEventListener node, 'keyup',   @keyUpCallback
     else
-      Batman.DOM.addEventListener node, 'submit', (args...) ->
+      Batman.DOM.addEventListener node, 'submit',  @submitCallback
+    node
+
+  keyDownCallback: ->
+    if Batman.DOM.events.isEnter(args[0])
+      Batman.DOM._keyCapturingNode = node
+
+  keyUpCallback: ->
+    if Batman.DOM.events.isEnter(args[0])
+      if Batman.DOM._keyCapturingNode is node
         Batman.DOM.preventDefault args[0]
         callback node, args..., context
+      Batman.DOM._keyCapturingNode = null

-    node
+  submitCallback: ->
+    Batman.DOM.preventDefault args[0]
+    callback node, args..., context

-  other: (node, eventName, callback, context) -> Batman.DOM.addEventListener node, eventName, (args...) -> callback node, args..., context
+  other: (node, eventName, callback, context) ->
+    Batman.DOM.addEventListener node, eventName, @otherCallback
+
+  otherCallback: -> callback node, args..., context

 Batman.DOM.eventIsAllowed = (eventName, event) ->
   if delegate = Batman.currentApp?.shouldAllowEvent?[eventName]


fixed radar ring update

fixed radar ring update

--- src/viz/types/radar.coffee
+++ src/viz/types/radar.coffee
@@ -39,10 +39,10 @@
       textwrap().container d3.select(elem)
         .height(vars.height.viz/8)
         .width(labelWidth).draw()
-  labelHeight = d3.max sizes, (d) -> d.height
+  labelHeight = d3.median sizes, (d) -> d.height

-  maxRadius -= labelHeight * 2
-  maxRadius -= vars.labels.padding * 4
+  maxRadius -= (labelHeight * 2)
+  maxRadius -= (vars.labels.padding * 2)
   maxData = (fetchValue(vars, d, vars.size.value) for d in c for c in children)
   maxData = d3.max(d3.merge(maxData))

@@ -71,22 +71,32 @@
       intervals = i
       break

-  rings = vars.group.selectAll "#d3plus_radar_rings"
-    .data buckets([maxRadius/intervals, maxRadius], intervals-1).reverse()
+  ringData = buckets([maxRadius/intervals, maxRadius], intervals-1).reverse()
+
+  rings = vars.group.selectAll ".d3plus_radar_rings"
+    .data ringData, (d) -> d

   ringStyle = (ring) ->
     ring
-      .attr "fill", (d, i) -> if i is 0 then vars.axes.background.color else "transparent"
+      .attr "fill", (d, i) ->
+        if i is 0 then vars.axes.background.color else "transparent"
       .attr "cx", vars.width.viz/2 + vars.margin.top
       .attr "cy", vars.height.viz/2 + vars.margin.left
-      .attr "stroke", if vars.x.grid.value then vars.x.grid.color else "transparent"
+      .attr "stroke",
+        if vars.x.grid.value then vars.x.grid.color else "transparent"

   rings.enter().append "circle"
+    .attr "class", "d3plus_radar_rings"
     .call ringStyle
     .attr "r", 0

   rings.transition().duration(vars.draw.timing)
+    .call ringStyle
     .attr "r", (d) -> d
+
+  rings.exit().transition().duration(vars.draw.timing)
+    .attr "opacity", 0
+    .remove()

   labelIndex = d3.scale.ordinal()
     .domain(labels)


⍷: more tests and fixed the case for empty arrays

--- src/vocabulary/find.coffee
+++ src/vocabulary/find.coffee
@@ -35,6 +35,16 @@
     #     ...                  0 1 0 0 0
     #     ...                  0 0 0 0 0
     #     ...                  0 0 0 0 0)
+    #
+    #     1⍷⍳5                  ⍝ returns 0 1 0 0 0
+    #     1 2⍷⍳5                ⍝ returns 0 1 0 0 0
+    #     ⍬⍷⍳5                  ⍝ returns 1 1 1 1 1
+    #     ⍬⍷⍬                   ⍝ returns ⍬
+    #     1⍷⍬                   ⍝ returns ⍬
+    #     1 2 3⍷⍬               ⍝ returns ⍬
+    #     (2 3 0⍴0)⍷(3 4 5⍴0)   ⍝ returns 3 4 5⍴1
+    #     (2 3 4⍴0)⍷(3 4 0⍴0)   ⍝ returns 3 4 0⍴0
+    #     (2 3 0⍴0)⍷(3 4 0⍴0)   ⍝ returns 3 4 0⍴0
     if alpha.shape.length > omega.shape.length
       return new APLArray [0], omega.shape, repeat [0], omega.shape.length
     if alpha.shape.length < omega.shape.length
@@ -44,6 +54,8 @@
         repeat([0], omega.shape.length - alpha.shape.length).concat(alpha.stride)
         alpha.offset
       )
+    if prod(alpha.shape) is 0
+      return new APLArray [1], omega.shape, repeat [0], omega.shape.length
     findShape = []
     for i in [0...omega.shape.length]
       d = omega.shape[i] - alpha.shape[i] + 1


add valueless return

add valueless return

--- lib/emit-new-server-client.coffee
+++ lib/emit-new-server-client.coffee
@@ -18,6 +18,8 @@
           enumerable : true
           configurable: true

+    return
+
 module.exports.options =
   id: 'cio/emit-new-server-client'
   after: [ 'cio/createServer']


:fire: unused code

:fire: unused code

--- lib/table-element.coffee
+++ lib/table-element.coffee
@@ -1072,9 +1072,6 @@

       @appendGutterCell(row) for row in visibleRows

-    # else if firstRow > @lastRenderedRow or lastRow < @firstRenderedRow or firstColumn > @lastRenderedColumn or lastColumn < @firstRenderedColumn
-    #   console.log 'full redraw'
-
     else if firstRow isnt @firstRenderedRow or lastRow isnt @lastRenderedRow or firstColumn isnt @firstRenderedColumn or lastColumn isnt @lastRenderedColumn
       disposed = 0
       created = 0


enh: store status of checkbox in local storage

--- coffee/src/application.coffee
+++ coffee/src/application.coffee
@@ -107,7 +107,13 @@
   getTransitionFunc: -> @transitionFunc

   getMargin: -> if @observer.isDrawingHomePtr then @margin else 0
-
+  setDrawingHomePtr: (isDrawing)->
+    @observer.isDrawingHomePtr = isDrawing
+    redraw()
+    if localStorage?
+      localStorage.setItem "observer.isDrawingHomePtr", if isDrawing then "1" else "0"
+      console.log "store #{isDrawing}"
+
   #Convert canvas X,Y coordinates to relative X,Y in (0..1) range
   canvas2relative: (x,y) ->
     s = Math.min(canvas.width, canvas.height) - 2*@getMargin()
@@ -132,7 +138,14 @@
       @cells.put unity, 1

     @observer = new @ObserverClass @tessellation, @appendRewrite, minVisibleSize, config.getViewBase(), config.getViewOffset()
-    @observer.isDrawingHomePtr = E('flag-origin-mark').checked
+    if (isDrawing=localStorage?.getItem('observer.isDrawingHomePtr'))?
+      isDrawing = isDrawing is '1'
+      E('flag-origin-mark').checked = isDrawing
+      @observer.isDrawingHomePtr = isDrawing
+      console.log "restore #{isDrawing}"
+    else
+      @setDrawingHomePtr E('flag-origin-mark').checked
+
     @observer.onFinish = -> redraw()

     @navigator = new Navigator this
@@ -196,8 +209,11 @@
       @transitionFunc = @transitionFunc.changeGrid @getGroup().n, @getGroup().m

     @observer?.shutdown()
-    @observer = new @ObserverClass @tessellation, @appendRewrite, minVisibleSize
-    @observer.isDrawingHomePtr = E('flag-origin-mark').checked
+
+    oldObserver = @observer
+    @observer = new @ObserverClass @tessellation, @appendRewrite, minVisibleSize
+    @observer.isDrawingHomePtr = oldObserver.isDrawingHomePtr
+
     @observer.onFinish = -> redraw()
     @navigator?.clear()
     doClearMemory()
@@ -810,8 +826,7 @@
   doSetFixedSize true

 E('flag-origin-mark').addEventListener 'change', (e)->
-  application.getObserver().isDrawingHomePtr = E('flag-origin-mark').checked
-  redraw()
+  application.setDrawingHomePtr E('flag-origin-mark').checked

 E('btn-mode-edit').addEventListener 'click', (e) -> doSetPanMode false
 E('btn-mode-pan').addEventListener 'click', (e) -> doSetPanMode true
@@ -869,7 +884,6 @@
 updateCanvasSize()
 updateMemoryButtons()
 updatePlayButtons()
-application.getObserver().isDrawingHomePtr = E('flag-origin-mark').checked
 redrawLoop()

 #application.saveDialog.show()


Fix Rexler Brath German translation.

Fix Rexler Brath German translation.

fixes #113

--- coffeescripts/cards-de.coffee
+++ coffeescripts/cards-de.coffee
@@ -311,7 +311,7 @@
             text: """Wenn du angreifst und der Verteidiger bereits einen roten Zielerfassungsmarker hat, darfst du ihn unmittelbar nach dem Angriffswurf in die Zielerfassung nehmen."""
         "Rexler Brath":
             ship: "TIE-Jagdbomber"
-            text: """Nachdem du angegriffen und damit dem Verteidiger mindestens 1 Schadenskarte zugeteilt hast, kannst du einen Fokusmarker ausgeben, um die soeben zugeteilte Schadenskarte aufzudecken."""
+            text: """Nachdem du angegriffen und damit dem Verteidiger mindestens 1 Schadenskarte zugeteilt hast, kannst du einen Fokusmarker ausgeben, um die soeben zugeteilten Schadenskarten aufzudecken."""
         "Knave Squadron Pilot":
             name: "Pilot der Schurken-Staffel"
         "Blackmoon Squadron Pilot":


Viktor Hell fix

Viktor Hell fix

Added ship tag as it was missing. Someday I will have all these little things fixed, I promise :P

--- coffeescripts/cards-es.coffee
+++ coffeescripts/cards-es.coffee
@@ -991,6 +991,7 @@
         '"Double Edge"':
             text: '''Once per round, after you perform a secondary weapon attack that does not hit, you may perform an attack with a different weapon.'''
         'Viktor Hel':
+            ship: "Caza Kihraxz"
             text: '''After defending, if you did not roll exactly 2 defense dice, the attacker receives 1 stress token.'''
         'Lowhhrick':
             text: '''When another friendly ship at Range 1 is defending, you may spend 1 reinforce token. If you do, the defender adds 1 %EVADE% result.'''


Wave 8 completed

Wave 8 completed

-Translated Attack Shuttle name, as I missed it in the last update, and added 'ship' tags everywhere to avoid problems
-Translated all the cards from the other Wave 8 ships (G-1A, JumpMaster, TIE Prototype)
-Translated those three ships names and added the 'ship' tags to avoid problems.
-Added 'ship' tag to Deathfire from Imperial Veterans.

--- coffeescripts/cards-es.coffee
+++ coffeescripts/cards-es.coffee
@@ -170,6 +170,7 @@
     exportObj.renameShip 'TIE Phantom', 'TIE Fantasma'
     exportObj.renameShip 'TIE Defender', 'Defensor TIE'
     exportObj.renameShip 'TIE Punisher', 'Castigador TIE'
+    exportObj.renameShip 'TIE Advanced Prototype', 'Prototipo de TIE Avanzado'
     exportObj.renameShip 'VT-49 Decimator', 'VT-49 Diezmador'
     exportObj.renameShip 'TIE/fo Fighter', 'Caza TIE/fo'
     exportObj.renameShip 'A-Wing', 'Ala-A'
@@ -179,6 +180,7 @@
     exportObj.renameShip 'Y-Wing', 'Ala-Y'
     exportObj.renameShip 'K-Wing', 'Ala-K'
     exportObj.renameShip 'Z-95 Headhunter', 'Z-95 Cazacabezas'
+    exportObj.renameShip 'Attack Shuttle', 'Lanzadera de Ataque'
     exportObj.renameShip 'CR90 Corvette (Aft)', 'Corbeta CR90 (Popa)'
     exportObj.renameShip 'CR90 Corvette (Fore)', 'Corbeta CR90 (Proa)'
     exportObj.renameShip 'GR-75 Medium Transport', 'Transporte mediano GR-75'
@@ -187,6 +189,8 @@
     exportObj.renameShip 'StarViper', 'Víbora Estelar'
     exportObj.renameShip 'Aggressor', 'Agresor'
     exportObj.renameShip 'Kihraxz Fighter', 'Caza Kihraxz'
+    exportObj.renameShip 'G-1A Starfighter', 'Caza Estelar G-1A'
+    exportObj.renameShip 'JumpMaster 5000', 'Saltador Maestro 5000'

     pilot_translations =
         "Wedge Antilles":
@@ -624,11 +628,21 @@
             ship: "Caza TIE"
             text: """Cuando ataques a un defensor que tiene 1 o más cartas de Daño, tira 1 dado de ataque adicional."""
         "The Inquisitor":
-            text: """When attacking with your primary weapon at Range 2-3, treat the range of the attack as Range 1."""
+            name: "El Inquisidor"
+            ship: "Prototipo de TIE Avanzado"
+            text: """Cuando ataques con tu armamento principal a alcance 2-3, el alcance del ataque se considera 1."""
         "Zuckuss":
-            text: """When attacking, you may roll 1 additional attack die.  If you do, the defender rolls 1 additional defense die."""
+            ship: "Caza Estelar G-1A"
+            text: """Cuando ataques, puedes tirar 1 dado de ataque adicional. Si decides hacerlo, el defensor tira 1 dado de defensa adicional."""
+        "Ruthless Freelancer":
+            name: "Mercenario Despiadado"
+            ship: "Caza Estelar G-1A"
+        "Gand Findsman":
+            name: "Buscador Gandiano"
+            ship: "Caza Estelar G-1A"
         "Dengar":
-            text: """Once per round after defending, if the attacker is inside your firing arc, you may perform an attack against the that ship."""
+            ship: "Saltador Maestro 5000"
+            text: """Una vez por ronda, después de que te defiendas, si el atacante está dentro de tu arco de fuego, puedes efectuar un ataque contra esa nave."""
         "Talonbane Cobra":
             ship: "Caza Kihraxz"
             text: """Cuando ataques o te defiendas, duplica el efecto de tus bonificaciones al combate por alcance."""
@@ -706,6 +720,7 @@
             ship: "Caza TIE"
             text: """Cuando otra nave aliada que tengas a alcance 1 gaste una ficha de Concentración, asigna 1 ficha de Concentración a tu nave."""
         'Ezra Bridger':
+            ship: "Lanzadera de Ataque"
             text: """Cuando te defiendas, si estás bajo tensión, puedes cambiar hasta 2 de tus resultados %FOCUS% por resultados %EVADE%."""
         '"Zeta Leader"':
             name: "Jefe Zeta"
@@ -721,10 +736,13 @@
             text: """Al comienzo de la fase de Combate, toda nave enemiga con la que estés en contacto recibe 1 ficha de Tensión."""
         'Hera Syndulla (Attack Shuttle)':
             name: "Hera Syndulla (Lanzadera de Ataque)"
+            ship: "Lanzadera de Ataque"
             text: """Cuando reveles una maniobra verde o roja, puedes girar tu selector de maniobras para escoger otra maniobra del mismo color."""
         'Sabine Wren':
+            ship: "Lanzadera de Ataque"
             text: """Inmediatamente antes de revelar tu maniobra, puedes realizar una acción gratuita de impulso o tonel volado."""
         '"Zeb" Orrelios':
+            ship: "Lanzadera de Ataque"
             text: '''Cuando te defiendas, puedes anular resultados %CRIT% antes de anular resultados %HIT%.'''
         "Lothal Rebel":
             name: "Rebelde de Lothal"
@@ -736,15 +754,29 @@
             text: '''Mientras no estés bajo tensión, puedes ejecutar tus maniobras %TROLLLEFT% y %TROLLRIGHT% como maniobras blancas.'''
             ship: "T-70 Ala-X"
         "Valen Rudor":
-            text: """After defending, you may perform a free action."""
+            text: """Después de que te defiendas, puedes ralizar una acción gratuita."""
+            ship: "Prototipo de TIE Avanzado"
         "4-LOM":
-            text: """At the start of the End phase, you may assign 1 of your stress tokens to another ship at Range 1."""
+            ship: "Caza Estelar G-1A"
+            text: """Al comienzo de la fase Final, puedes asignar 1 de tus fichas de Tensión a otra nave que tengas a alcance 1."""
         "Tel Trevura":
-            text: """The first time you would be destroyed, instead cancel any remaining damage, discard all Damage cards, and deal 4 facedown Damage cards to this ship."""
+            ship: "Saltador Maestro 5000"
+            text: """La primrea vez que seas destruido, en vez de eso anula todo el daño restante, descarta todas tus cartas de Daño e inflinge 4 cartas de Daño boca abajo a esta nave."""
         "Manaroo":
-            text: """At the start of the Combat phase, you may assign all focus, evade, and target lock tokens assigned to you to another friendly ship."""
+            ship: "Saltador Maestro 5000"
+            text: """Al comienzo de la fase de Combate, puedes asignar a otra nave aliada todas las fichas de Concentración, Evasión y Blanco Fijado que tengas asignadas."""
+        "Contracted Scout":
+            name: "Explorador Contratado"
+            ship: "Saltador Maestro 5000"
         '"Deathfire"':
             text: '''When you reveal your maneuver dial or after you perform an action, you may perform a %BOMB% Upgrade card action as a free action.'''
+            ship: "TIE Bombardero"
+        "Sienar Test Pilot":
+            name: "Piloto de pruebas de Sienar"
+            ship: "Prototipo de TIE Avanzado"
+        "Baron of the Empire":
+            name: "Barón del Imperio"
+            ship: "Prototipo de TIE Avanzado"

     upgrade_translations =
         "Ion Cannon Turret":
@@ -1146,11 +1178,14 @@
             name: "Agente Kallus"
             text: '''%IMPERIALONLY%%LINEBREAK%Al comienzo de la primera ronda, elige 1 nave enemiga pequeña o grande. Cuando ataques a esa nave o te defiendas de esa nave, puedes cambiar 1 de tus resultados %FOCUS% por un resultado %HIT% o %EVADE%.'''
         'XX-23 S-Thread Tracers':
-            text: """Attack (focus): Discard this card to perform this attack.  If this attack hits, each friendly ship at Range 1-2 of you may acquire a target lock on the defender.  Then cancel all dice results."""
+            name: "Hiperrastreadores XX-23"
+            text: """Ataque (Concentración): Descarta esta carta para efectuar este ataque. Si este ataque impacta, toda nave aliada que tengas a alcance 1-2 puede fijar al defensor como blanco. Después se anulan todos los resultados de los dados."""
         "Tractor Beam":
-            text: """Attack: Attack 1 ship.%LINEBREAK%If this attack hits, the defender receives 1 tractor beam token.  Then cancel all dice results."""
+            name: "Proyector de Campo de Tracción"
+            text: """Ataque: Ataca a 1 nave.%LINEBREAK%Si este ataque impacta, el defensor recibe 1 ficha de Campo de Tracción. Después se anulan todos los resultados de los dados."""
         "Cloaking Device":
-            text: """%SMALLSHIPONLY%%LINEBREAK%Action: Perform a free cloak action.%LINEBREAK%At the end of each round, if you are cloaked, roll 1 attack die.  On a %FOCUS% result, discard this card, then decloak or discard your cloak token."""
+            name: "Dispositivo de Camuflaje"
+            text: """%SMALLSHIPONLY%%LINEBREAK%Acción: Realiza una acción gratuita de camuflaje.%LINEBREAK%Al final de cada ronda, si estás camuflado, tira 1 dado de ataque. Si sacas %FOCUS%, descarta esta carta y luego elige entre desactivar el camuflaje o retirar tu ficha de Camuflaje."""
         "Shield Technician":
             name: "Técnico de Escudos"
             text: """%HUGESHIPONLY%%LINEBREAK%Cuando lleves a cabo una acción de recuperación, en vez de retirar todas tus fichas de Energía, puedes elegir qué cantidad de fichas de Energía deseas retirar."""
@@ -1223,30 +1258,36 @@
         'Cluster Bombs':
             text: '''After defending, you may discard this card.  If you do, each other ship at Range 1 of the defending section rolls 2 attack dice, suffering all damage (%HIT%) and critical damage (%CRIT%) rolled.'''
         "Adaptability":
-            text: """Dual card.%LINEBREAK%Side A:%LINEBREAK%Increase your pilot skill value by 1.%LINEBREAK%Side B:%LINEBREAK%Decrease your pilot skill value by 1."""
+            name: "Adaptabilidad"
+            text: """Carta dual.%LINEBREAK%Cara A:%LINEBREAK%La Habilidad de tu piloto se incrementa en 1.%LINEBREAK%Cara B:%LINEBREAK%La habilidad de tu piloto se reduce en 1."""
         "Electronic Baffle":
-            text: """When you receive a stress token or an ion token, you may suffer 1 damage to discard that token."""
+            name: "Regulador Electrónico"
+            text: """Cuando recibas una ficha de Tensión o una ficha de Iones, puedes sufrir 1 de daño para descartar esa ficha."""
         "4-LOM":
-            text: """%SCUMONLY%%LINEBREAK%When attacking, during the "Modify Attack Dice" step, you may receive 1 ion token to choose 1 of the defender's focus or evade tokens.  That token cannot be spent during this attack."""
+            text: """%SCUMONLY%%LINEBREAK%Cuando ataques, durante el paso "Modificar la tirada de ataque" puedes recibir 1 ficha de Iones para elegir 1 de las fichas de Concentración o Evasión del defensor. Esa ficha no se puede gastar durante este ataque."""
         "Zuckuss":
-            text: """%SCUMONLY%%LINEBREAK%When attacking, you may receive any number of stress tokens to choose an equal number of defense dice.  The defender must reroll those dice."""
+            text: """%SCUMONLY%%LINEBREAK%Cuando ataques, puedes recibir tantas fichas de Tensión como quieras para elegir una cantidad igual de dados de defensa. El defensor debe volver a tirar esos dados."""
         'Rage':
-            text: """Action: Assign 1 focus token to your ship and receive 2 stress tokens.  Until the end of the round, when attacking, you may reroll up to 3 attack dice."""
+            name: "Furia"
+            text: """Acción: Asigna 1 ficha de Concentración a tu nave y recibe 2 fichas de Tensión. Hasta el final de la ronda, cuando ataques puedes volver a tirar hasta 3 dados de ataque."""
         "Attanni Mindlink":
-            text: """%SCUMONLY%%LINEBREAK%Each time you are assigned a focus or stress token, each other friendly ship with Attanni Mindlink must also be assigned the same type of token if it does not already have one."""
+            name: "Enlace Mental Attani"
+            text: """%SCUMONLY%%LINEBREAK%Cada vez que se te asigne una ficha de Concentración o de Tensión, a todas las demás naves aliadas equipadas con "Enlace Mental Attani" se les debe asignar también una ficha de ese mismo tipo si es que no tienen ya una."""
         "Boba Fett":
-            text: """%SCUMONLY%%LINEBREAK%After performing an attack, if the defender was dealt a faceup Damage card, you may discard this card to choose and discard 1 of the defender's Upgrade cards."""
+            text: """%SCUMONLY%%LINEBREAK%Después de que efectúes un ataque, si al defensor se le infligió una carta de Daño boca arriba, puedes descartar esta carta para elegir y descartar 1 de las cartas de Mejora del defensor."""
         "Dengar":
-            text: """%SCUMONLY%%LINEBREAK%When attacking, you may reroll 1 attack die.  If the defender is a unique pilot, you may instead reroll up to 2 attack dice."""
+            text: """%SCUMONLY%%LINEBREAK%Cuando ataques, puedes volver a tirar 1 dado de ataque. Si el defensor es un piloto único, en vez de eso puedes volver a tirar hasta 2 dados de ataque."""
         '"Gonk"':
-            text: """%SCUMONLY%%LINEBREAK%Action: Place 1 shield token on this card.%LINEBREAK%Action: Remove 1 shield token from this card to recover 1 shield (up to your shield value)."""
+            name: '"Gonk"'
+            text: """%SCUMONLY%%LINEBREAK%Acción: Coloca 1 ficha de Escudos sobre esta carta.%LINEBREAK%Acción: Quita 1 ficha de Escudos de esta carta para recupera 1 de Escudos (hast aun máximo igual a tu valor de Escudos)."""
         "R5-P8":
-            text: """Once per round, after defending, you may roll 1 attack die.  On a %HIT% result, the attacker suffers 1 damage.  On a %CRIT% result, you and the attacker each suffer 1 damage."""
+            text: """Una vez por ronda, después de que te defiendas puedes volver a tirar 1 dado de ataque. Si sacas %HIT%, el atacante sufre 1 de daño. Si sacas %CRIT%, tanto tú como el atacante sufrís 1 de daño."""
         'Thermal Detonators':
             name: "Detonadores Térmicos"
             text: """Cuando reveles tu selector de maniobras, puedes descartar esta carta para soltar 1 ficha de Detonador térmico.%LINEBREAK%Esta ficha se detona al final de la fase de Activación.%LINEBREAK%Ficha de Detonador Térmico: Cuando esta bomba detona, cada nave a Alcance 1 de la ficha sufre 1 de daño y recibe 1 ficha de tensión.  Después, descarta esta ficha."""
         "Overclocked R4":
-            text: """During the Combat phase, when you spend a focus token, you may receive 1 stress token to assign 1 focus token to your ship."""
+            name: "Droide R4 trucado"
+            text: """Durante la fase de Combate, cuando gastes una ficha de Concentración puedes recibir 1 ficha de Tensión para asignar 1 ficha de Concentración a tu nave."""
         'Systems Officer':
             text: '''%IMPERIALONLY%%LINEBREAK%After you execute a green maneuver, choose another friendly ship at Range 1.  That ship may acquire a target lock.'''

@@ -1322,7 +1363,8 @@
         'Long-Range Scanners':
             text: '''You can acquire target locks on ships at Range 3 and beyond.  You cannot acquire target locks on ships at Range 1-2.  You can equip this card only if you have %TORPEDO% and %MISSILE% in your upgrade bar.'''
         "Guidance Chips":
-            text: """Once per round, when attacking with a %TORPEDO% or %MISSILE% secondary weapon, you may change 1 die result to a %HIT% result (or a %CRIT% result if your primary weapon value is "3" or higher)."""
+            name: "Chips de Guiado"
+            text: """Una vez por ronda, cuando ataques con un sistema de armamento secundario %TORPEDO% o %MISSILE%, puedes cambiar 1 de tus resultados de dado por un resultado %HIT% (o por un resultad %CRIT% si tu valor de Armamento principal es de 3 o más)."""

     title_translations =
         "Slave I":
@@ -1411,12 +1453,18 @@
         "Phantom":
             name: "Fantasma"
             text: """Mientras estás acoplado, el Espíritu puede efectuar ataques de armamento principal desde su arco de fuego especial y, al final de la fase de Combate, puede efectuar un ataque adicional con una %TURRET% equipada. Si efectúa este ataque, no puede volver a atacar durante esta ronda."""
+            ship: 'Lanzadera de Ataque'
         "TIE/v1":
-            text: """TIE Advanced Prototype only.%LINEBREAK%After you acquire a target lock, you may perform a free evade action."""
+            text: """Solo Prototipo de TIE Av.%LINEBREAK%Después de que fijes a un blanco, puedes realizar una acción gratuita de evasión."""
+            ship: 'Prototipo de TIE Avanzado'
         "Mist Hunter":
-            text: """G-1A starfighter only.%LINEBREAK%Your upgrade bar gains the %BARRELROLL% Upgrade icon.%LINEBREAK%You must equip 1 "Tractor Beam" Upgrade card (paying its squad point cost as normal)."""
+            name: "Cazador de la Niebla"
+            text: """Solo Caza Estelar G-1A.%LINEBREAK%Tu barra de acción gana el icono %BARRELROLL%.%LINEBREAK%Debes equiparte con 1 carta de Mejora "Proyector de Campo de Tracción" (pagando su coste normal en puntos de escuadrón)."""
+            ship: 'Caza Estelar G-1A'
         "Punishing One":
-            text: """JumpMaster 5000 only.%LINEBREAK%Increase your primary weapon value by 1."""
+            name: "Castigadora"
+            text: """Solo Saltador Maestro 5000.%LINEBREAK%Tu valor de Armamento principal se incrementa en 1."""
+            ship: 'Saltador Maestro 5000'
         "Hound's Tooth":
             name: "Diente de Perro"
             text: """Solo YV-666.%LINEBREAK%Después de que seas destruido, y antes de retirarte de la zona de juego, puedes desplegar al Piloto del Cachorro de Nashtah.%LINEBREAK%El Cachorro de Nashtah no puede atacar en esta ronda."""


Remove unused Mustache

Remove unused Mustache

--- lib/test-runner.coffee
+++ lib/test-runner.coffee
@@ -1,5 +1,4 @@
 Mocha = require 'mocha'
-Mustache = require 'mustache'
 async = require 'async'
 path = require 'path'
 _ = require 'underscore'