DE Wave XIV

DE Wave XIV

--- coffeescripts/cards-de.coffee
+++ coffeescripts/cards-de.coffee
@@ -96,6 +96,7 @@
         "Resistance Bomber Expansion Pack": "Bomber des Widerstands Erweiterung"
         "TIE Silencer Expansion Pack": "TIE-Dämpfer Erweiterung"
         "Alpha-class Star Wing Expansion Pack": "Sternflügler der Alpha-Klasse Erweiterung"
+        "Saw's Renegades Expansion Pack" : "Saws Rebellenmiliz Erweiterung"
     ui:
         shipSelectorPlaceholder: "Wähle ein Schiff"
         pilotSelectorPlaceholder: "Wähle einen Piloten"
@@ -243,6 +244,7 @@
     exportObj.renameShip 'Sheathipede-class Shuttle', 'Raumfähre der Sheathipede-Klasse'
     exportObj.renameShip 'B/SF-17 Bomber', 'B/SF-17-Bomber'
     exportObj.renameShip 'TIE Silencer', 'TIE-Dämpfer'
+    exportObj.renameShip 'TIE Reaper', 'TIE-Schnitter'
     pilot_translations =
         "Wedge Antilles":
             ship: "X-Flügler"
@@ -1147,25 +1149,43 @@
         'Sienar-Jaemus Analyst':
             name: "Analyst von Sienar-Jaemus"
             ship: "TIE-Dämpfer"
+        'Cavern Angels Zealot':
+            name: "Fanatiker der Sturmengel"
+            ship: "X-Flügler"
+        'Partisan Renegade':
+            name: "Überzeugter Partisan"
+            ship: "U-Flügler"
+        'Scarif Base Pilot':
+            name: "Pilot der Scarif-Basis"
+            ship: "TIE-Schnitter"
         'Kullbee Sperado':
             ship: "X-Flügler"
-            text: '''After you perform a boost or barrel roll action, you may flip your equipped "Servomotor S-foils" upgrade card.'''
+            text: '''Nachdem du die Aktion Schub oder Fassrolle durchgeführt hast, darfst du deine ausgerüstete Aufwertungskarte "Servomotorische S-Flügel" umdrehen.'''
         'Major Vermeil':
-            text: '''When attacking, if the defender does not have a focus or evade token, you may change one of your blank or %FOCUS% results to a %HIT% result.'''
+            ship: "TIE-Schnitter"
+            text: '''Sobald du angreifst, darfst du, falls der Verteidiger keine Fokus- oder Ausweichmarker hat, 1 deiner Leerseiten oder %FOCUS% in ein %HIT% ändern.'''
         'Leevan Tenza':
-            text: '''After you perform a boost action, you may receive 1 stress token to receive 1 evade token.'''
+            ship: "X-Flügler"
+            text: '''Nachdem du die Aktion Schub durchgeführt hast, darfst du 1 Stressmarker erhalten, um 1 Ausweichmarker zu erhalten.'''
         'Saw Gerrera':
-            text: '''When a friendly ship at Range 1-2 attacks, if it is stressed or has at least 1 Damage card, it may reroll 1 attack die.'''
+            ship: "U-Flügler"
+            text: '''Sobald ein freundliches Schiff in Reichweite 1-2 angreift, darf es, falls es gestresst ist oder mindestens 1 Schadenskarte hat, 1 Angriffswürfel neu würfeln.'''
         'Benthic Two-Tubes':
-            text: '''After you perform a focus action, you may remove 1 of your focus tokens to assign it to a friendly ship at Range 1-2.'''
+            ship: "U-Flügler"
+            text: '''Nachdem du die Aktion Fokussierung durchgeführt hast, darfst du 1 deiner Fokusmarker entfernen, um ihn einem freundlichen Schiff in Reichweite 1-2 zuzuordnen.'''
         'Captain Feroph':
-            text: '''When defending, if the attacker is jammed, add 1 %EVADE% result to your roll.'''
+            ship: "TIE-Schnitter"
+            text: '''Sobald du verteidigst, falls der Angreifer gestört ist, fügst du 1 %EVADE% zu deinem Wurf hinzu.'''
         '"Vizier"':
-            text: '''After a friendly ship executes a 1-speed maneuver, if it is at Range 1 and did not overlap a ship, you may assign 1 of your focus or evade tokens to it.'''
+            name: "Wesir"
+            ship: "TIE-Schnitter"
+            text: '''Nachdem ein freundliches Schiff ein Manöver mit Geschwindigkeit 1 ausgeführt hat, darfst du ihm, falls es in Reichweite 1 ist und sich nicht mit einem Schiff überschnitten hat, 1 deiner Fokus- oder Ausweichmarker zuordnen.'''
         'Magva Yarro':
-            text: '''When another friendly ship at Range 1-2 is defending, the attacker cannot reroll more than 1 attack die.'''
+            ship: "U-Flügler"
+            text: '''Sobald ein anderes freundliches Schiff in Reichweite 1-2 verteidigt kann der Angreifer nicht mehr als 1 Angriffswürfel neu würfeln.'''
         'Edrio Two-Tubes':
-            text: '''When you become the active ship during the Activation phase, if you have 1 or more focus tokens, you may perform a free action.'''
+            ship: "X-Flügler"
+            text: '''Sobald du während der Aktivierungsphase zum aktiven Schiff wirst, darfst du, falls du 1 oder mehrere Fokusmarker hast, eine freie Aktion durchführen.'''

     upgrade_translations =
         "Ion Cannon Turret":
@@ -1909,23 +1929,31 @@
             name: "Trümmer-Gambit"
             text: '''%SMALLSHIPONLY%%LINEBREAK%Aktion: Ordne deinem Schiff 1 Ausweichmarker für jedes Hindernis in Reichweite 1 bis zu einem Maximum von 2 Ausweichmarkern zu.'''
         'Targeting Scrambler':
-            text: '''At the start of the Planning phase, you may receive a weapons disabled token to choose a ship at Range 1-3 and assign it the "Scrambled" Condition.'''
+            name: "Zielcomputer-Sperrsystem"
+            text: '''Zu Beginn der Planungsphase darfst du einen Waffen-deaktiviert-Marker erhalten, um ein Schiff in Reichweite 1-3 zu wählen und ihm den Zustand "Gesperrt" zuzuordnen.'''
         'Death Troopers':
-            text: '''After another friendly ship at range 1 becomes the defender, if you are inside the attacker's firing arc at range 1-3, the attacker receives 1 stress token.'''
+            name: "Todestruppen"
+            text: '''%IMPERIALONLY%%LINEBREAK%Nachdem ein anderes freundliches Schiff in Reichweite 1 zum Verteidiger geworden ist, erhält der Angreifer 1 Stressmarker, falls du innnerhalb des Feuerwinkels des Angreifers und in Reichweite 1-3 zu ihm bist.'''
         'Saw Gerrera':
-            text: '''%REBELONLY%%LINEBREAK%When attacking, you may suffer 1 damage to change all of your %FOCUS% results to %CRIT% results.'''
+            text: '''%REBELONLY%%LINEBREAK%Sobald du angreifst, darfst du 1 Schaden nehmen, um alle deine %FOCUS% in %CRIT% zu ändern.'''
         'Director Krennic':
-            text: '''During setup, before the "Place Forces" step, assign the "Optimized Prototype" condition to a friendly Galactic Empire ship with 3 or fewer shields.'''
+            name: "Direktor Krennic"
+            text: '''%IMPERIALONLY%%LINEBREAK%Ordne den Zustand "Optimierter Prototyp" während des Spielaufbaus vor dem Schritt "Streitkräfte platzieren", einem freundlichen Schiff des Galaktischen Imperiums zu, das 3 oder weniger Schilde hat.'''
         'Magva Yarro':
-            text: '''%REBELONLY%%LINEBREAK%After defending, you may acquire a target lock on the attacker.'''
+            text: '''%REBELONLY%%LINEBREAK%Nachdem du verteidigt hast, darfst du den Angreifer in die Zielerfassung nehmen.'''
         'Renegade Refit':
-            text: '''T-65 X-Wing and U-Wing only.%LINEBREAK%You can equip up to two different Modification upgrades.%LINEBREAK%The squad point cost of each of your equipped %ELITE% upgrades is reduced by 1 (to a minimum of 0).'''
+            name: "Umrüstung der Miliz"
+            ship: "X-Flügler"
+            text: '''Nur für T-65 X-Flügler und U-Flügler.%LINEBREAK%Du darfst bis zu 2 verschiedene Modifikation-Aufwertungen ausrüsten.%LINEBREAK%Die Kommandopunktekosten jeder %ELITE%-Aufwertung, die du ausgerüstet hast, sinken um 1 (bis zu einem Minimum von 0).'''
         'Tactical Officer':
-            text: '''%IMPERIALONLY%%LINEBREAK%Your action bar gains %COORDINATE%.'''
+            name: "Taktikoffizier"
+            text: '''%IMPERIALONLY%%LINEBREAK%Deine Aktionsleite erhält %COORDINATE%.'''
         'ISB Slicer':
-            text: '''After you perform a jam action against an enemy ship, you may choose a ship at Range 1 of that ship that is not jammed and assign it 1 jam token.'''
+            name: "ISB-Hacker"
+            text: '''%IMPERIALONLY%%LINEBREAK%Nachdem du die Aktion Störsignal gegen ein feindliches Schiff durchgeführt hast, darfst du ein nicht gestörtes Schiff in Reichweite 1 zu jenem Schiff wählen und ihm 1 Störsignal-Marker zuordnen.'''
         'Thrust Corrector':
-            text: '''When defending, if you have 3 or fewer stress tokens, you may receive 1 stress token to cancel all of your dice results.  If you do, add 1 %EVADE% result to your roll.  Your dice cannot be modified again during this attack.%LINEBREAK%You can equip this Upgrade only if your hull value is "4" or lower.'''
+            name: "Schubregler"
+            text: '''Sobald du verteidigst, darfst du, falls du 3 oder weniger Stressmarker hast, 1 Stressmarker erhalten, um alle deine Würfelergebnisse zu negieren. Falls du das tust, füge deinem Wurf 1 %EVADE% hinzu. Während diees Angriffs können deine Würfel nicht erneut modifiziert werden.%LINEBREAK%Du kannst die Aufwertung nur dann ausrüsten, falls dein Hüllenwert 4 oder niedriger ist.'''

     modification_translations =
         "Stealth Device":
@@ -2035,9 +2063,12 @@
             ship: "B/SF-17-Bomber"
             text: '''Nur für B/SF-17-Bomber.%LINEBREAK%Sobald eine freundliche Bombe detoniert, darfst du wählen ihre Effekte nicht zu erleiden. Falls du das tust, wirf einen Angriffswürfel. Lege diese Karte bei einem %HIT% an.'''
         'Servomotor S-Foils':
-            text: '''T-65 X-Wing only. %DUALCARD%%LINEBREAK%Side A (Attack):Your action bar gains %BARRELROLL%.  If you are not stressed, when you reveal a (%TURNLEFT% 3) or (3 %TURNRIGHT%) maneuver, you may treat it as a red (%TROLLLEFT% 3) or (%TROLLRIGHT% 3) in the same direction.%LINEBREAK%At the start of the Activation phase, you may flip this card.%LINEBREAK%Side B (Closed):Reduce your primary attack value by 1.  Your action bar gains %BOOST%.  Treat your (%BANKLEFT% 2) and (%BANKRIGHT% 2 ) as green.%LINEBREAK%At the start of the Activation phase, you may flip this card.'''
+            name: "Servomotorische S-Flügel"
+            ship: "X-Flügler"
+            text: '''Nur für T-65 X-Flügler. %DUALCARD%%LINEBREAK%Seite A (Angriffsposition):Deine Aktionsleiste erhält %BARRELROLL%.  Falls du nicht gestresst bist, darfst du, sobald du ein (%TURNLEFT% 3)- oder (3 %TURNRIGHT%)-Manöver aufdeckst, es wie ein rotes (%TROLLLEFT% 3)- oder (%TROLLRIGHT% 3)-Manöver mit gleicher Flugrichtung behandeln.%LINEBREAK%Zu Beginn der Aktivierungsphase darfst du diese Karte umdrehen.%LINEBREAK%Seite B (Geschlossen):Senke deinen Primärangriffswert um 1. Deine Aktionsleiste erhält %BOOST%. Behandle deine Manöver (%BANKLEFT% 2) und (%BANKRIGHT% 2) wie grüne Manöver.%LINEBREAK%Zu Beginn der Aktivierungsphase darfst du diese Karte umdrehen.'''
         'Multi-spectral Camouflage':
-            text: '''%SMALLSHIPONLY%%LINEBREAK%After you receive a red target lock token, if you have only 1 red target lock token, roll 1 defense die.  On an %EVADE% result, remove 1 red target lock token.'''
+            name: "Multispektrale Tarnung"
+            text: '''%SMALLSHIPONLY%%LINEBREAK%Nachdem du einen roten Zielerfassungsmarker erhalten hast, wirf 1 Verteidigungswürfel, falls du nur 1 roten Zielerfassungsmarker hast. Bei einem %EVADE% entferne 1 roten Zielerfassungsmarker.'''

     title_translations =
         "Slave I":
@@ -2252,7 +2283,9 @@
             name: "Kreuzfeuer-Formation"
             text: '''Nur für B/SF-17-Bomber.%LINEBREAK%Sobald du verteidigst, darfst du, falls sich minestens 1 anderes freunldiches Schiff des Widerstands in Reichweite 1-2 zum Angreifer befindet, 1 %FOCUS% hinzufügen.'''
         'Advanced Ailerons':
-            text: '''TIE Reaper only.%LINEBREAK%Treat your (%BANKLEFT% 3) and (%BANKRIGHT% 3) maneuvers as white.%LINEBREAK%Immediately before you reveal your dial, if you are not stressed, you must execute a white (%BANKLEFT% 1), (%STRAIGHT% 1), or (%BANKRIGHT% 1) maneuver.'''
+            ship: "TIE-Schnitter"
+            name: "Verbesserte Querruder"
+            text: '''Nur für TIE-Schnitter%LINEBREAK%Behandle deine Manöver (%BANKLEFT% 3) und (%BANKRIGHT% 3) wie weiße Manöver. Unmittelbar bevor du dein Rad aufdeckst, musst du, falls du nicht gestresst bist, ein weißes (%BANKLEFT% 1)-, (%STRAIGHT% 1)- oder (%BANKRIGHT% 1)-Manöver ausführen.'''

     condition_translations =
         '''I'll Show You the Dark Side''':
@@ -2281,8 +2314,10 @@
             name: "Aus der Fassung"
             text: '''Sobald du Schaden oder kritischen Schaden durch eine Bombe nimmst, nimmst du 1 zusätzlichen kritischen Schaden. Entferne dann diese Karte.%LINEBREAK%Aktion: Wirf 1 Angriffswürfel. Bei einem %FOCUS% oder %HIT% entferne diese Karte.'''
         'Scrambled':
-            text: '''When attacking a ship at Range 1 that is equipped with the "Targeting Scrambler" upgrade, you cannot modify attack dice.%LINEBREAK%At the end of the combat phase, remove this card.'''
+            name: "Gesperrt"
+            text: '''Sobald du ein mit der Aufwertung "Zielcomputer-Sperrsystem" ausgerüstetes Schiff in Reichweite 1 angreifst, kannst du deine Angriffswürfel nicht modifizieren.%LINEBREAK%Entferne diese Karte am Ende der Kampfhase.'''
         'Optimized Prototype':
-            text: '''Increase your shield value by 1.%LINEBREAK%Once per round, when performing a primary weapon attack, you may spend 1 die result to remove 1 shield from the defender.%LINEBREAK%After you perform a primary weapon attack, a friendly ship at Range 1-2 equipped with the "Director Krennic" Upgrade card may acquire a target lock on the defender.'''
+            name: "Optimierter Prototyp"
+            text: '''Erhöhe deinen Schildwert um 1.%LINEBREAK%Ein Mal pro Runde darfst du, sobald du einen Primärwaffenangriff durchführst 1 Würfelergebnis ausgeben, um 1 Schild vom Verteidiger zu entfernen.%LINEBREAK%Nachdem du einen Primärwaffenangriff durchgeführt hast, darf ein mit der Aufwertung "Direktor Krennic" ausgerüstetes, befreundetes Schiff in Reichweite 1-2 den Verteidiger in die Zielerfassung nehmen.'''

     exportObj.setupCardData basic_cards, pilot_translations, upgrade_translations, modification_translations, title_translations, condition_translations


Added maneuvers for the rest of the released ships plus Z95.

Had to comment out the lambda shuttle because I forgot to implement 0
speed maneuvers.


Imp Vets

Imp Vets

-Added Glaive Squadron Pilot with the 'ship' tag, as it was missing in the Spanish version
-Translated Maarek Stele as it's the same text as the TIE Advanced version

--- coffeescripts/cards-es.coffee
+++ coffeescripts/cards-es.coffee
@@ -777,11 +777,13 @@
         "Baron of the Empire":
             name: "Barón del Imperio"
             ship: "Prototipo de TIE Avanzado"
-        "Maarek Stele (TIE Defender)":
-            text: """When your attack deals a faceup Damage card to the defender, instead draw 3 Damage cards, choose 1 to deal, and discard the others."""
+        "Maarek Stele (Defensor TIE)":
+            text: """Cuando tu ataque inflija una carta de Daño boca arriba al defensor, en vez de eso roba 3 cartas de Daño, elige 1 de ellas a tu elección y luego descarta las otras."""
             ship: "Defensor TIE"
         "Countess Ryad":
             text: """When you reveal a %STRAIGHT% maneuver, you may treat it as a %KTURN% maneuver."""
+            ship: "Defensor TIE"
+        "Glaive Squadron Pilot":
             ship: "Defensor TIE"

     upgrade_translations =


Update cards-fr.coffee

Update cards-fr.coffee

Correction ship ligne for 4 tie, correction Rey text and add Advanced slam in fr

--- coffeescripts/cards-fr.coffee
+++ coffeescripts/cards-fr.coffee
@@ -534,6 +534,7 @@
         'Gozanti-class Cruiser':
             text: """Après avoir exécuté une manoeuvre, vous pouvez déployer jusqu'à 2 vaisseaux arrimés."""
         '"Scourge"':
+            ship: "Chasseur TIE"
             text: """Quand vous attaquez un défenseur qui a au moins 1 carte de dégâts, lancez 1 dé d'attaque supplémentaire."""
         "The Inquisitor":
             text: """Quand vous attaquez avec votre arme principale à portée 2-3, considérez que votre attaque se fait à portée 1."""
@@ -565,10 +566,13 @@
         'Hera Syndulla':
             text: '''Quand vous révélez une manoeuvre verte ou rouge, vous pouvez tourner votre cadran sur une autre manoeuvre de même difficulté.'''
         '"Youngster"':
+            ship: "Chasseur TIE"
             text: """Les chasseurs TIE alliés situés à portée 1-3 peuvent effectuer l'action de votre carte d'amélioration %ELITE% dont vous êtes équipé."""
         '"Wampa"':
+            ship: "Chasseur TIE"
             text: """Quand vous attaquez, vous pouvez annuler le résultat de tous les dés. Si vous annulez au moins un résultat, assignez 1 carte dégâts face cachée au défenseur."""
         '"Chaser"':
+            ship: "Chasseur TIE"
             text: """Quand un autre vaisseau allié situé à portée 1 dépense un marqueur de concentration, assignez un marqueur de concentration à votre vaisseau."""
         'Ezra Bridger':
             text: """Quand vous défendez, si vous êtes sous l'effet du stress, vous pouvez changer jusqu'à 2 de vos résultats %FOCUS% en résultats %EVADE% ."""
@@ -618,7 +622,7 @@
         "Poe Dameron (PS9)":
             text: """Tant que vous défendez ou que vous attaquez, si vous avez un marqueur de concentration, vous pouvez changer un de vos résutats %FOCUS% pour un résultat %HIT% ou %EVADE% ."""
         "Rey":
-            text: """When attacking or defending, if the enemy ship is inside of your firing arc, you may reroll up to 2 of your blank results."""
+            text: """Quand vous attquez ou défendez, si le vaisseau ennemi est dans votre arc de tir, Vous pouvez relancer jusqu'a 2 de vos resultats blanc."""
         'Han Solo (TFA)':
             text: ''' Quand vous vous positionnez pendant la phase de préparation, vous pouvez vous placer n'importe où dans la zone de jeu au-delà de portée 3 des vaisseaux ennemis.'''
         'Chewbacca (TFA)':
@@ -1479,6 +1483,9 @@
             text: '''TIE only.%LINEBREAK%Quand vous défendez, après avoir lancé les dés de défense, s'il y a plus de dés d'attaque que de défense, lancez 1 dé de défense supplémentaire.%LINEBREAK%Vous ne pouvez pas vous équiper de cette carte si votre valeur d'agilité est de "3" ou plus.'''
         'Pulsed Ray Shield':
             text: '''Rebel and Scum only.%LINEBREAK%Durant la phase de dénouement, vous pouvez recevoir 1 marqueur ionique pour récupérer 1 bouclier (à hauteur de votre valeur de boucliers). Vous ne pouvez vous equiper de cette carte que si votre valeur de boucliers est de "1". '''
+        'Advanced SLAM' :
+            name: "MASL avancé"
+            text: '''Après avoir effectué une action MASL, si vous n'avez pas chevauché un obstacle ou un autre vaisseau, vous pouvez effecteur une action gratuite.'''

     title_translations =
         "Slave I":


Fix missing ship translations to PL.

Fix missing ship translations to PL.

fixes #381

--- coffeescripts/cards-pl.coffee
+++ coffeescripts/cards-pl.coffee
@@ -613,8 +613,10 @@
         'Sabine Wren (Scum)':
             text: '''When defending against an enemy ship inside your mobile firing arc at Range 1-2, you may add 1 %FOCUS% result to your roll.'''
         'Sabine Wren (TIE Fighter)':
+            ship: 'Myśliwiec TIE'
             text: '''Immediately before you reveal your maneuver, you may perform a free boost or barrel roll action.'''
         '"Zeb" Orrelios (TIE Fighter)':
+            ship: 'Myśliwiec TIE'
             text: '''When defending, you may cancel %CRIT% results before %HIT% results.'''
         'Kylo Ren':
             text: '''The first time you are hit by an attack each round, deal the "I'll Show You the Dark Side" Condition card to the attacker.'''
@@ -639,8 +641,10 @@
         'Jess Pava':
             text: '''When attacking or defending, you may reroll 1 of your dice for each other friendly ship at Range 1.'''
         'Ahsoka Tano':
+            ship: 'Myśliwiec TIE'
             text: '''At the start of the Combat phase, you may spend 1 focus token to choose a friendly ship at Range 1.  It may perform 1 free action.'''
         'Captain Rex':
+            ship: 'Myśliwiec TIE'
             text: '''After you perform an attack, assign the "Suppressive Fire" Condition card to the defender.'''
         'Major Stridan':
             text: '''For the purpose of your actions and Upgrade cards, you may treat friendly ships at Range 2-3 as being at Range 1.'''
@@ -1285,7 +1289,7 @@
         "Moldy Crow":
             text: """Tylko HWK-290.%LINEBREAK%Podczas fazy końcowej nie usuwaj ze swojego statku niewykorzystanych żetonów skupienia."""
         "ST-321":
-            ship: "Navette de classe Lambda"
+            ship: "Prom typu Lambda"
             text: """Navette de classe Lambda uniquement.%LINEBREAK%Quand vous verrouillez une cible, vous pouvez verrouiller n'importe quel vaisseau ennemi situé dans la zone de jeu."""
         "Royal Guard TIE":
             ship: "TIE Interceptor"
@@ -1316,7 +1320,7 @@
         "Outrider":
             text: """Tylko YT-2400.%LINEBREAK%Dopóki jesteś wyposażony w kartę rozwinięcia [cannon], nie możesz wykonywać ataków bronią podstawową. Przy pomocy dodatkowej broni [cannon] możesz wykonywać ataki skierowane przeciwko statkom znajdujacym się poza twoim polem rażenia. """
         "Dauntless":
-            ship: "Décimateur VT-49"
+            ship: 'Decimator VT-49'
             text: """Tylko Decimator VT-49.%LINEBREAK%Po tym jak wykonasz manewr, który sprawi że będziesz nachodzić na inny statek, możesz wykonać 1 darmową akcję. Następnie otrzymujesz 1 żeton stresu."""
         "Virago":
             text: """Tylko StarViper.%LINEBREAK%Twój pasek rozwinięć zyskuje symbole rozwinięć %SYSTEM% i %ILLICIT%. Nie możesz wyposażyć swojego statku w tę kartę jeśli wartość umiejętności twojego pilota wynosi „3” lub mniej."""
@@ -1358,6 +1362,7 @@
         'TIE/D':
             text: '''TIE Defender only.%LINEBREAK%Once per round, after you perform an attack with a %CANNON% secondary weapon that costs 3 or fewer squad points, you may perform a primary weapon attack.'''
         'TIE Shuttle':
+            ship: 'Bombowiec TIE'
             text: '''TIE Bomber only.%LINEBREAK%Your upgrade bar loses all %TORPEDO%, %MISSILE%, and %BOMB% upgrade icons and gains 2 %CREW% upgrade icons.  You cannot equip a %CREW% Upgrade card that costs more than 4 squad points.'''
         'Requiem':
             text: '''%GOZANTIONLY%%LINEBREAK%When you deploy a ship, treat its pilot skill value as "8" until the end of the round.'''
@@ -1379,6 +1384,7 @@
             text: '''Lancer-class Pursuit Craft only.%LINEBREAK%After you perform an attack that hits, if the defender is inside your mobile firing arc and at Range 1-2, you may assign the defender 1 tractor beam token.'''
         # Wave X
         '''Sabine's Masterpiece''':
+            ship: 'Myśliwiec TIE'
             text: '''TIE Fighter only.%REBELONLY%%LINEBREAK%Your upgrade bar gains the %CREW% and %ILLICIT% upgrade icons.'''
         '''Kylo Ren's Shuttle''':
             text: '''Upsilon-class Shuttle only.%LINEBREAK%At the end of the Combat phase, choose an unstressed enemy ship at Range 1-2.  Its owner must assign a stress token to it or assign a stress token to another ship at Range 1-2 of you that that player controls.'''


:sweat::statue_of_liberty: Updated in browser at strd6.github.io/editor

--- pixie.cson
+++ pixie.cson
@@ -1,4 +1,4 @@
-version: "0.1.2"
+version: "0.1.3"
 entryPoint: "main"
 remoteDependencies: [
   "https://code.jquery.com/jquery-1.10.1.min.js"


:pig2::cactus: Updated in browser at strd6.github.io/editor

--- pixie.cson
+++ pixie.cson
@@ -1,4 +1,4 @@
-version: "0.1.2"
+version: "0.1.3-alpha.1"
 remoteDependencies: [
   "//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.5.2/underscore-min.js"
   "//code.jquery.com/jquery-1.10.1.min.js"


Bump the version to 1.0.1

Bump the version to 1.0.1

--- src/coffeescripts/transition-callbacks.js.coffee
+++ src/coffeescripts/transition-callbacks.js.coffee
@@ -9,7 +9,7 @@
   return o

 class window.TransitionCallbacks
-  @VERSION: '1.0'
+  @VERSION: '1.0.1'
   @timeout: 3000
   @clearAll: false


href: don't mangle case in links

href: don't mangle case in links

--- src/smc-hub/share/process-internal-links.coffee
+++ src/smc-hub/share/process-internal-links.coffee
@@ -11,14 +11,15 @@
         a = e.find('a')
         for x in a
             y = $(x)
-            href = y.attr('href')?.toLowerCase()
+            href = y.attr('href')
             if href?
                 if href[0] == '#'
                     # CASE: internal link on same document - do not touch (e.g., sections in jupyter/sagews)
                     continue
-                if misc.startswith(href, 'mailto:')
+                href_lower = href.toLowerCase()
+                if misc.startswith(href_lower, 'mailto:')
                     continue
-                if misc.startswith(href, 'http://') or misc.startswith(href, 'https://')
+                if misc.startswith(href_lower, 'http://') or misc.startswith(href_lower, 'https://')
                     # for now at least, just leave all such links alone, except make them
                     # open in a new tab (rather than replacing this)
                     y.attr("target","_blank")


add default period

add default period

--- src/components/widgets/accounts-cash-balance/accounts-cash-balance.directive.coffee
+++ src/components/widgets/accounts-cash-balance/accounts-cash-balance.directive.coffee
@@ -61,7 +61,7 @@
   # ----

   getPeriod = ->
-    w.metadata? && w.metadata.hist_parameters? && w.metadata.hist_parameters.period
+    w.metadata? && w.metadata.hist_parameters? && w.metadata.hist_parameters.period || 'MONTHLY'

   getSerieByAccount = (series, account)->
     _.find(series, (serie)-> (serie.uid || serie.options && serie.options.uid) == account.uid)


only request messages that we haven't seen

--- zechat/static/peers.coffee
+++ zechat/static/peers.coffee
@@ -36,6 +36,7 @@
     @transport = @options.transport
     @transport.on('open', @on_open)
     @transport.on('packet', @on_packet)
+    @seen = {}

   on_open: (open) =>
     @identity.authenticate(@transport)
@@ -44,10 +45,14 @@
       @transport.send(type: 'list', identity: @identity.pubkey())

     .then (resp) =>
+      messages = (h for h in resp.messages when not @seen[h])
+      unless messages.length
+        return {messages: []}
+
       @transport.send(
         type: 'get'
         identity: @identity.pubkey()
-        messages: resp.messages
+        messages: messages
       )

     .done (resp) =>
@@ -71,6 +76,7 @@

     message = JSON.parse(zc.b64decode(packed_message))
     message.hash = zc.message_hash(packet.data)
+    @seen[message.hash] = true
     peer = @app.request('peer', sender)
     peer.message_col.add(message)


index: Fix several index bugs.

index: Fix several index bugs.

--- client/index.coffee
+++ client/index.coffee
@@ -53,7 +53,7 @@
         this["each_#{k}"] = (f = (x) -> x) ->
           f(v) for v of @_.indices[k]
         this["with_#{k}"] = (value) ->
-          Heron.Util.keys(@_.indices[k][value])
+          @_.indices[k][value]

   # Add an object to the index.
   #
@@ -64,7 +64,7 @@
   add: (obj) ->
     for k, e of @_.extractors
       v = e(obj)
-      (@_.indices[k][v] ?= {})[obj] = true
+      (@_.indices[k][v] ?= []).push(obj)
     this

   # Remove an object from the index.
@@ -77,7 +77,11 @@
     for k, e of @_.extractors
       v = e(obj)
       if @_.indices[k][v]?
-        delete @_.indices[k][v][obj]
+        new_index = []
+        for other in @_.indices[k][v]
+          if obj != other
+            new_index.push(other)
+        @_.indices[k][v] = new_index
         if Heron.Util.empty(@_.indices[k][v])
           delete @_.indices[k][v]
     this
@@ -94,7 +98,7 @@
     r = []
     index = Heron.Util.any(@_.indices)[1]
     for v, objs of index
-      for obj of obj
+      for obj in objs
         r.push(f(obj))
     r


Increase autocompile rollout to 10%

Increase autocompile rollout to 10%

--- services/web/app/coffee/Features/Project/ProjectController.coffee
+++ services/web/app/coffee/Features/Project/ProjectController.coffee
@@ -289,7 +289,7 @@
 				# Extract data from user's ObjectId
 				timestamp = parseInt(user_id.toString().substring(0, 8), 16)

-				rolloutPercentage = 5 # Percentage of users to roll out to
+				rolloutPercentage = 10 # Percentage of users to roll out to
 				if !ProjectController._isInPercentageRollout('autocompile', user_id, rolloutPercentage)
 					# Don't show if user is not part of roll out
 					return cb(null, { enabled: false, showOnboarding: false })


Adding show events to fabricated show

Adding show events to fabricated show

--- node/fabricate.coffee
+++ node/fabricate.coffee
@@ -349,6 +349,24 @@
       status: 'closed'
       updated_at: '2013-09-26T15:03:29Z'
       coordinates: null
+      events: [
+        {
+          _id: "556c77227261694a5fef0600"
+          title: "Inez and Vinoodh Opening"
+          start_at: "2015-01-07T20:00:00+00:00"
+          end_at: "2015-01-07T21:00:00+00:00"
+          description: "Inez and Vinoodh present their new works."
+          event_type: "Opening Reception"
+        }
+        {
+          _id: "556c77227261694a5fef0600",
+          title: "Inez and Vinoodh Gallery Walkthrough",
+          start_at: "2015-01-08T19:15:00+00:00",
+          end_at: "2015-01-08T21:00:00+00:00",
+          description: "Artists Inez and Vinoodh visit the gallery.",
+          event_type: "Other"
+        }
+      ]

     when 'page'
       content: 'This is an awesome page'


Make graphite.coffee work and add `graphite list`

--- src/scripts/graphite.coffee
+++ src/scripts/graphite.coffee
@@ -10,17 +10,25 @@
 #   GRAPHITE_AUTH (e.g. user:password for Basic Auth)
 #
 # Commands:
-#   hubot graphite search <string> - search for graph by name
-#   hubot graphite show <graph.name> - output graph
+#   graphite list - list all available graphs
+#   graphite search <string> - search for graph by name
+#   graphite show <graph.name> - output graph
 #
-# Author:
+# Authors:
 #   obfuscurity
+#   spajus

 module.exports = (robot) ->
+
+  robot.hear /graphite list/i, (msg) ->
+    treeversal msg, (data) ->
+      output = ""
+      output += "#{human_id(metric)}\n" for metric in data
+      msg.send output
   robot.hear /graphite search (\w+)/i, (msg) ->
     treeversal msg, (data) ->
       output = ""
-      output += metric.id + "\n" for metric in data
+      output += "#{human_id(metric)}\n" for metric in data
       msg.send output
   robot.hear /graphite show (\S+)/i, (msg) ->
     treeversal msg, (data) ->
@@ -28,7 +36,6 @@
         msg.send url

 construct_url = (msg, graphUrl, cb) ->
-  cb(graphUrl) unless process.env.GRAPHITE_AUTH
   graphRegex = /(\bhttps?:\/\/)(\S+)(\/render\/\S+)$/
   serverRegex = /(\bhttps?:\/\/)(\S+)$/
   uri = graphUrl.match(graphRegex)[3]
@@ -37,7 +44,10 @@
   port = construct_port()
   timestamp = '#' + new Date().getTime()
   suffix = '&png'
-  newUrl = proto + process.env.GRAPHITE_AUTH + '@' + server + port + uri + timestamp + suffix
+  if process.env.GRAPHITE_AUTH
+    newUrl = proto + process.env.GRAPHITE_AUTH + '@' + server + port + uri + timestamp + suffix
+  else
+    newUrl = proto + server + port + uri + timestamp + suffix
   cb(newUrl)

 treeversal = (msg, cb, node="") ->
@@ -45,13 +55,18 @@
   if node == ""
     prefix = "*"
   else
-    prefix = node + ".*"
+    if node.slice(-1) == '.'
+      prefix = node + "*"
+    else
+      prefix = node + ".*"
   port = construct_port()
-  uri = "/browser/usergraph/?query=#{prefix}&format=treejson&contexts=1&path=#{node}&user=#{node}&node=#{node}"
+  uri = "/browser/usergraph/?query=#{prefix}&format=treejson&contexts=1&path=#{node}&node=#{node}"
   auth = 'Basic ' + new Buffer(process.env.GRAPHITE_AUTH).toString('base64') if process.env.GRAPHITE_AUTH
+  headers = { Accept: "application/json", 'Content-type': 'application/json' }
+  headers['Authorization'] = auth if auth
   msg
     .http(process.env.GRAPHITE_URL + port + uri)
-    .headers(Authorization: auth if auth, Accept: "application/json", 'Content-type': 'application/json')
+    .headers(headers)
     .get() (err, res, body) ->
       unless res.statusCode is 200
         console.log(res)
@@ -62,11 +77,14 @@
           treeversal(msg, cb, nodes[i].id)
         else
           regex = new RegExp(msg.match[1], "gi")
-          unless nodes[i].id.search(regex) == -1
+          unless human_id(nodes[i]).search(regex) == -1
             unless nodes[i].id == "no-click"
               data[data.length] = nodes[i]
         i++
       cb(data) if data.length > 0
+
+human_id = (node) ->
+  node.id.replace /\.[a-z0-9]+$/, ".#{node.text}"

 construct_port = () ->
   port = ':'


Altered wording of no results message

Altered wording of no results message

--- src/scripts/rubygems.coffee
+++ src/scripts/rubygems.coffee
@@ -13,4 +13,4 @@
         gems=[]
         for result in results[0..4]
           gems.push "#{result.name}: https://rubygems.org/gems/#{result.name}"
-        if gems.length>0 then msg.send gems.join('\n') else msg.send "Actually there aren't any gems for that"
+        if gems.length>0 then msg.send gems.join('\n') else msg.send "Actually, there isn't a gem for that!"


show title when editing file

show title when editing file

--- source/javascripts/all.coffee
+++ source/javascripts/all.coffee
@@ -597,6 +597,7 @@
     .hide()
   $ '#editor'
     .show()
+  @document.title = @editor_filename
   @editor = @ace.edit $('#editor .editor')[0]
   $('#btnReloadEditor').click()


Disable autocomplete

Disable autocomplete

--- atom/symlink.config.cson
+++ atom/symlink.config.cson
@@ -53,3 +53,5 @@
       "PULLREQ_EDITMSG$"
       "MERGE_MSG$"
     ]
+  "autocomplete-plus":
+    enableAutoActivation: false


fix error in billing error display I randomly hit

--- src/smc-webapp/billing.cjsx
+++ src/smc-webapp/billing.cjsx
@@ -1879,7 +1879,7 @@
         billing :
             customer        : rtypes.object
             invoices        : rtypes.object
-            error           : rtypes.string
+            error           : rtypes.oneOfType([rtypes.string, rtypes.object])
             action          : rtypes.string
             loaded          : rtypes.bool
             no_stripe       : rtypes.bool     # if true, stripe definitely isn't configured on the server


swapping to a doc

swapping to a doc

--- lib/cli.coffee
+++ lib/cli.coffee
@@ -665,9 +665,9 @@
                 delete data._id
                 data.seo = {}
                 data.site_id = dst_site.get('_id').val()
-                db.get('objects').insert data, (err, new_data) ->
-                  id_mapping[old_id] = new_data.val()._id
-                  next null, new_data
+                db.get('objects').insert data, (err, new_doc) ->
+                  id_mapping[old_id] = new_doc.val()._id
+                  next null, new_doc
                   exit err if err
               ), (err, objects_cloned) ->


removed --no-auto-launch flag

removed --no-auto-launch flag

--- lib/cli.coffee
+++ lib/cli.coffee
@@ -18,7 +18,6 @@
   env:         "sets environment variables. separate multiple values with a comma. overrides any value in cypress.json or cypress.env.json"
   config:      "sets configuration values. separate multiple values with a comma. overrides any value in cypress.json."
   version:     "installs a specific version of Cypress"
-  noLaunch:      "disable automatically launching your project and opening Chrome"
 }

 text = (d) ->
@@ -80,11 +79,10 @@
   program
     .command("open [project]")
     .usage("[project] [options]")
-    .description("Opens Cypress as a regular application. If your current pwd matches an existing Cypress project, that project will automatically open and Chrome will be launched.")
+    .description("Opens Cypress normally, as a desktop application.")
     .option("-p, --port <port>",         text("port"))
     .option("-e, --env <env>",           text("env"))
     .option("-c, --config <config>",     text("config"))
-    .option("--no-auto-launch",          text("noLaunch"))
     .action (project, opts) ->
       require("./commands/open")(project, parseOpts(opts))


Skip getAvailableDictionaries on hunspell

--- spec/spellchecker-spec.coffee
+++ spec/spellchecker-spec.coffee
@@ -158,13 +158,14 @@
       @fixture.setDictionary defaultLanguage, dictionaryDirectory

     it "returns an array of string dictionary names", ->
+      # NB: getAvailableDictionaries is nop'ped in hunspell and it also doesn't
+      # work inside Appveyor's CI environment
+      return if process.platform is 'linux' or process.env.CI or process.env.SPELLCHECKER_PREFER_HUNSPELL
+
       dictionaries = @fixture.getAvailableDictionaries()
       expect(Array.isArray(dictionaries)).toBe true

-      # Dictionaries do not appear to be available on AppVeyor
-      unless process.platform is 'win32' and (process.env.CI or process.env.SPELLCHECKER_PREFER_HUNSPELL)
-        expect(dictionaries.length).toBeGreaterThan 0
-
+      expect(dictionaries.length).toBeGreaterThan 0
       for dictionary in dictionaries.length
         expect(typeof dictionary).toBe 'string'
         expect(diction.length).toBeGreaterThan 0


Remove comments with regex.

Remove comments with regex.

--- lib/css.coffee
+++ lib/css.coffee
@@ -1,20 +1,23 @@
 parser = require('./parser').parser

-_comment = /^\/\*.*\*\//
-_blank_line = /\n+/g
+_comment = /^\/\*.*\*\//g
+_new_line = /\n+/g
+_blank_line = /^[ \t\n]*$/
 _multi_spaces = /\ +/g

 smash = (raw) ->

-  lines = raw.replace(_blank_line, "\n")
+  lines = raw.replace(_comment, "")
+  lines = lines.replace(_new_line, "\n")
   lines = lines.replace(_multi_spaces, " ")
   lines = lines.split("\n")
   results = []
   for line in lines
-    if _comment.test(line)
+    if _blank_line.test(line)
       continue
     results.push(line)
-  results.join("\n")
+  results = results.join("\n")
+  results
 exports.smash = smash

 class Stylesheet


size threshold now defaults to false

size threshold now defaults to false

--- src/viz/methods/size.coffee
+++ src/viz/methods/size.coffee
@@ -19,5 +19,5 @@
   solo:       filter(true)
   threshold:
     accepted: [Boolean, Function, Number]
-    value:    true
+    value:    false
   value:      false


update to pull from id

update to pull from id

--- lib/lt3.coffee
+++ lib/lt3.coffee
@@ -172,7 +172,7 @@
           db.get('objects').find {site_id:site.get('_id').val()}, (err, objects) =>
             for object in objects
               log "_id: #{object.data._id}, collection:#{object.data.collection},
-              type:#{object.data.type}"
+              type:#{object.data.type} slug:#{object.data.slug}"
             rl.prompt();

     when 'get:site_ids'


Better trace error

Better trace error

--- lib/zen.coffee
+++ lib/zen.coffee
@@ -122,9 +122,9 @@
       process.on "uncaughtException", (error) =>
         stack1 = error.stack.indexOf " at "
         stack2 = error.stack.indexOf " at ", stack1 + 1
+        error_trace = "uncaughtException: #{__date(new Date())}"
         file = error.stack.slice stack1, stack2
-        console.log(" ⚑  ERR".red, "uncaughtException:", __date(new Date()),
-          error.message.grey, file)
+        console.log(" ⚑  ERR".red, error_trace, error.message.grey, file)
         process.exit 1
       process.on "SIGTERM", =>
         @server.close -> process.exit 1


Clean empty value detection

Clean empty value detection

--- framer/InputLayer.coffee
+++ framer/InputLayer.coffee
@@ -68,7 +68,7 @@
 			lastCharacter = @input.value.substr(@input.value.length - 1)

 			# Exclude enter, shift, caps lock, control, alt, command etc.
-			unless e.which in [13, 16, 17, 18, 20, 91, 93] or @value is ""
+			unless e.which in [13, 16, 17, 18, 20, 91, 93]
 				@emit("change:value", @value)
 				@emit(Events.InputValueChange, @value)

@@ -79,6 +79,7 @@
 			# If backspace key
 			if e.which is 8
 				@emit(Events.BackSpaceKey, event)
+

 	_setPlaceholder: =>
 		@input.value =


export class from validators.coffee instead of instance

--- workers/social/lib/social/models/user/validators.coffee
+++ workers/social/lib/social/models/user/validators.coffee
@@ -1,6 +1,6 @@
 isEmailValid = require './emailchecker'

-module.exports = new class
+module.exports = class Validators

   @validateTeamDomain: (name) ->


Ensure event is not attached to local annotations

--- client/publication.coffee
+++ client/publication.coffee
@@ -624,7 +624,7 @@

 Template.publicationAnnotationsItem.rendered = ->
   # Run for the first time only
-  unless @rendered
+  unless @rendered or @data.local
     $saved = $(@findAll '.saved')

     fadeOutSavedNotification = _.debounce ->


remove console.log

remove console.log

--- coffee/_l10n.coffee
+++ coffee/_l10n.coffee
@@ -63,17 +63,13 @@
       return translatedString

   changeLang: (lang) ->
-    console.log 'changed lang' + lang
     @currentLang = lang
     @fetchIniData(() =>
-      console.log('xxxx')
       $elements = $('[data-l10n-id]')
       $elements.each((index, ele) =>
         $ele = $(ele)
         l10nId = $ele.data('l10n-id')
         params = $ele.data('l10n-params')
-        console.log($ele)
-        console.log(l10nId)
         $ele.text(@get(l10nId, params))
       )
     )


Implement `isFirstSlide()` and `isLastSlide()`.

--- coffee/ruban.coffee
+++ coffee/ruban.coffee
@@ -117,8 +117,13 @@
     @firstSlide()

   firstSlide: ->
-    $first = @$current.prevAll('section:first-child')
-    @go($first, direction: 'backward')
+    @go(@getFirstSlide(), direction: 'backward')
+
+  getFirstSlide: ->
+    @$sections.first()
+
+  isFirstSlide: ->
+    @$current.is(@getFirstSlide())

   prev: =>
     if @hasSteps()
@@ -145,8 +150,13 @@
     @lastSlide()

   lastSlide: ->
-    $last = @$current.nextAll('section:last-child')
-    @go($last, direction: 'forward')
+    @go(@getLastSlide(), direction: 'forward')
+
+  getLastSlide: ->
+    @$sections.last()
+
+  isLastSlide: ->
+    @$current.is(@getLastSlide())

   next: =>
     if @hasSteps()


Add the `title` option to display a title

Add the `title` option to display a title in every slides, in front of the pagination (or in place of it if not requested).

--- coffee/ruban.coffee
+++ coffee/ruban.coffee
@@ -20,6 +20,7 @@
     @options.minPadding         ?= '0.4em'
     @options.transitionDuration ?= '1s'
     @options.pagination         ?= false
+    @options.title              ?= null
     @options.stripHtmlInToc     ?= false
     @options.bindClicks         ?= false
     @options.bindMouseWheel     ?= false
@@ -216,13 +217,16 @@
     @$current = $section

   pagination: ->
-    if @options.pagination
+    @paginationText = []
+    @paginationText.push @options.title if @options.title
+    if @options.pagination or @options.title
       unless @$pagination
         @$ruban.parent().append('<footer class="pagination"></footer>')
         @$pagination = $('.pagination')
         @total = @$sections.length
-
-      @$pagination.html("#{@$current.index() + 1}/#{@total}")
+      if @options.pagination
+        @paginationText.push("#{@$current.index() + 1}/#{@total}")
+      @$pagination.html(@paginationText.join(' - '))

   toc: ->
     $toc = $('.toc').first()


Don't log every trace to console.

Don't log every trace to console.

--- coffee/tutor.coffee
+++ coffee/tutor.coffee
@@ -213,8 +213,6 @@
     null

 render_traces = ->
-  console.log traces
-
   if typeof traces isnt 'undefined'
     html = ''
     for trace, i in traces


add a "html code" option when editing wysiwyg text (but just for super admins)

Former-commit-id: aba5a76f8f6d154c98ed9dd08f2275c7e3936eed [formerly d6e9267c7d88076de713924f86623c312445251c] [formerly a8e798fc8563bb3a83c00d84665e65560d7bc891 [formerly c8a8e7dd6a08327a6de6ee2f211649f0c6202cf2]] [formerly d72c446d8d790674c8aeca1f7589d78219550349 [formerly 7156bfb8a05d8fb27aa558fa0f4d65a6c8e1fca0] [formerly 070d740025d4e7816385d7b8e70c02d0d0fd21b3 [formerly 070d740025d4e7816385d7b8e70c02d0d0fd21b3 [formerly e8dc85dbbf5b8a0d057796ef2444c182ded0cbe6]]]]
Former-commit-id: 089524826da8bcb43d47d15a2212e7bc3132a0d6 [formerly 13b4fa4872109a0a89208c140a61ed484a73da15] [formerly 39c7b0620d5e9fef3e02025976d1a42799bde7cc [formerly 287b69278fa5f2df92fdad56d9bfda62be45ffcf]]
Former-commit-id: 88dbdafab93f1e9de8ea703a1b2fedd7d72e824d [formerly e4bac8403d5c05bf7339d31e5a77c2aca029e8c3]
Former-commit-id: ecad7b93f5daaf09811835bfb9c2ad4265f53dfd
Former-commit-id: 3133ca56f59867e92bc3cb7afa8963ebdab5f44e

--- @client/shared.coffee
+++ @client/shared.coffee
@@ -179,55 +179,76 @@
         # show_wysiwyg_toolbar state
         ev.stopPropagation()

-      # Toolbar
-      DIV
-        id: 'toolbar'
-        style:
-          position: 'fixed'
-          top: 0
-          backgroundColor: '#e7e7e7'
-          boxShadow: '0 1px 2px RGBA(0,0,0,.2)'
-          zIndex: 999
-          padding: '0 12px'
-          display: if @root.show_wyswyg_toolbar == @props.key then 'block' else 'none'
-
-        I
-          className: "ql-bullet fa fa-list-ul"
-          style: toolbar_button_style
-          title: 'Bulleted list'
-
-        I
-          className: "ql-list fa fa-list-ol"
-          style: toolbar_button_style
-          title: 'Numbered list'
-
-        I
-          className: "ql-bold fa fa-bold"
-          style: toolbar_button_style
-          title: 'Bold'
-
-        I
-          className: "ql-link fa fa-link"
-          style: toolbar_button_style
-          title: 'Link'
-
-        I
-          className: "ql-image fa fa-image"
-          style: toolbar_button_style
-          title: 'Insert image'
-
-      DIV
-        id: 'editor'
-        dangerouslySetInnerHTML:{__html: @props.html}
-        'data-placeholder': if show_placeholder then @props.placeholder else ''
-        onFocus: =>
-          # Show the toolbar on focus
-          # show_wyswyg_toolbar is global state for the toolbar to be
-          # shown. It gets set to null when someone clicks outside the
-          # editor area. This is handled at the root level
-          # in the same way that clicking outside a point closes it.
-          # See Root.resetSelection.
-          @root.show_wyswyg_toolbar = @props.key; save(@root)
+      if @local.edit_code
+        AutoGrowTextArea
+          style:
+            width: '100%'
+            fontSize: 18
+          defaultValue: fetch(@props.key).html
+          onChange: (e) =>
+            my_data = fetch(@props.key)
+            my_data.html = e.target.value
+            save my_data
+
+      else
+
+        # Toolbar
+        [DIV
+          id: 'toolbar'
+          style:
+            position: 'fixed'
+            top: 0
+            backgroundColor: '#e7e7e7'
+            boxShadow: '0 1px 2px RGBA(0,0,0,.2)'
+            zIndex: 999
+            padding: '0 12px'
+            display: if @root.show_wyswyg_toolbar == @props.key then 'block' else 'none'
+
+          I
+            className: "ql-bullet fa fa-list-ul"
+            style: toolbar_button_style
+            title: 'Bulleted list'
+
+          I
+            className: "ql-list fa fa-list-ol"
+            style: toolbar_button_style
+            title: 'Numbered list'
+
+          I
+            className: "ql-bold fa fa-bold"
+            style: toolbar_button_style
+            title: 'Bold'
+
+          I
+            className: "ql-link fa fa-link"
+            style: toolbar_button_style
+            title: 'Link'
+
+          # I
+          #   className: "ql-image fa fa-image"
+          #   style: toolbar_button_style
+          #   title: 'Insert image'
+
+          if fetch('/current_user').is_super_admin
+            I
+              className: 'fa fa-code'
+              style: toolbar_button_style
+              onClick: => @local.edit_code = true; save @local
+
+
+        DIV
+          id: 'editor'
+          dangerouslySetInnerHTML:{__html: @props.html}
+          'data-placeholder': if show_placeholder then @props.placeholder else ''
+          onFocus: =>
+            # Show the toolbar on focus
+            # show_wyswyg_toolbar is global state for the toolbar to be
+            # shown. It gets set to null when someone clicks outside the
+            # editor area. This is handled at the root level
+            # in the same way that clicking outside a point closes it.
+            # See Root.resetSelection.
+            @root.show_wyswyg_toolbar = @props.key; save(@root)
+        ]

   componentDidMount : ->
     # Attach the Quill wysiwyg editor


test helper no longer extends from test helper, sets base element selector on el.self instead. remove instantiating new object on each test helper method call, wrap item helper into function just once, amendment to #983

--- test-integration/helpers/ui/test-element.coffee
+++ test-integration/helpers/ui/test-element.coffee
@@ -4,7 +4,7 @@
 S = require '../../../src/helpers/string'

 class TestItemHelper
-  constructor: (test, testElementLocator) ->
+  constructor: (test, testElementLocator, options = {}) ->
     throw new Error('BUG: Missing the current test!') unless test
     throw new Error('BUG: Missing locator') unless testElementLocator

@@ -18,6 +18,10 @@
       if _.isFunction(value) and not /^_locator/.test(key) # Exclude the _locator() function because that is used in waitUntil loops
         @[key] = (args...) =>
           @test.utils.verboseWrap("HELPER: #{key}", => value.apply(@, args))
+
+    @fn = wrapHelperToFunction(@, options, _.keys(TestItemHelper::))
+
+    @

   getLocator: (args...) =>
     locator = if _.isFunction(@locator)
@@ -86,9 +90,8 @@
     @test.utils.dom.getParent(locator)

-class TestHelper extends TestItemHelper
+class TestHelper
   constructor: (test, testElementLocator, commonElements, options) ->
-    super(test, testElementLocator)
     commonElements ||= _.result(@, 'elementRefs', {})
     defaultOptions =
       loadingLocator:
@@ -96,10 +99,12 @@
       defaultWaitTime: 40 * 1000 # TODO: Letting tests define their own wait time is dangerous. tutor-dev takes > 10sec to delete a task-plan
       # defaultWaitTime: 20 * 1000 # 20sec seems to be enough for deployed code but not local

+    @test = test
     @_options = _.assign {}, defaultOptions, options
     @_el = {}

     commonElements.loadingState = @options.loadingLocator
+    commonElements.self = testElementLocator

     _.each commonElements, @setCommonElement
     @
@@ -108,27 +113,40 @@
     # Adjust the test timeout *and* tell selenium to wait up to the same amount of time. Maybe this is redundant?
     @test.utils.wait.giveTime @options.defaultWaitTime, =>
       @test.utils.verboseWrap 'Waiting until Loadable .is-loading is gone', => @test.driver.wait(=>
-        @el.loadingState.isPresent().then (isPresent) -> not isPresent
+        @el.loadingState().isPresent().then (isPresent) -> not isPresent
       , @options.defaultWaitTime)

   setCommonHelper: (name, helper) =>
-    @el[name] = helper
+    @el[name] = helper.fn

   setCommonElement: (locator, name) =>
-    if _.isFunction(locator)
-      fn = (args...) => new TestItemHelper(@test, locator(args...))
-      # START: BACKWARDS_COMPATIBILITY Area
-      # For backwards compatibility, stick all the testItemHelper Methods onto the fn  (so it "looks like" a TestItemHelper)
-      oldItemHelper = new TestItemHelper(@test, locator)
-      _.each ['getLocator', 'get', 'getAll', 'findElement', 'findElements', 'forEach', 'isPresent', 'isDisplayed', 'click', 'waitClick'], (fnName) ->
-        fn[fnName] = (args...) ->
-          console.log "Deprecated call to el.#{name}.#{fnName}(...). Use el.#{name}(...).#{fnName}() instead"
-          oldItemHelper[fnName].apply(oldItemHelper, args)
-      # END: BACKWARDS_COMPATIBILITY Area
-      @setCommonHelper(name, fn)
-    else
-      @setCommonHelper(name, new TestItemHelper(@test, locator))
+    @setCommonHelper(name, new TestItemHelper(@test, locator,
+      onBeforeMethodCall: (methodName, args...) ->
+        console.log "Deprecated call to el.#{name}.#{methodName}(...). Use el.#{name}(...).#{methodName}() instead"
+    ))

+
+wrapHelperToFunction = (helper, options, methodNames) ->
+
+  helperFunction = (args...) ->
+    wrappedMethods = {}
+
+    # For each method, pass on args from helper function to method
+    _.each methodNames, (methodName) ->
+      if _.isFunction helper[methodName]
+        wrappedMethods[methodName] = _.partial helper[methodName], args...
+
+    # The helper function returns a fresh copy of
+    # the helper extended with the wrapped methods.
+    _.extend({}, helper, wrappedMethods)
+
+  # expose each method on the function as well
+  _.each methodNames, (methodName) ->
+    helperFunction[methodName] = (args...) ->
+      options.onBeforeMethodCall?(methodName, args...)
+      helper[methodName](args...)
+
+  helperFunction

 # Using defined properties for access eliminates the possibility
 # of accidental assignment


sales-growth: fix product name display when several companies

--- src/components/widgets/sales-growth/sales-growth.directive.coffee
+++ src/components/widgets/sales-growth/sales-growth.directive.coffee
@@ -66,8 +66,8 @@
     return _.last(w.content.dates) if $scope.isDataFound

   $scope.getDisplayName = (product, threshold) ->
-    fullName = if w.content.organizations.length = 1 then product.name else product.company + ' - ' + product.name
-    codeName = if w.content.organizations.length = 1 then product.code else product.company + ' - ' + product.code
+    fullName = if w.content.organizations.length == 1 then product.name else product.company + ' - ' + product.name
+    codeName = if w.content.organizations.length == 1 then product.code else product.company + ' - ' + product.code
     return if fullName.length > threshold then codeName else fullName

   # Chart formating function


cut a few minutes out of hodu on YomTob

cut a few minutes out of hodu on YomTob

--- source/javascripts/site/vatikin.coffee
+++ source/javascripts/site/vatikin.coffee
@@ -19,8 +19,8 @@

 (->
   @NULL = { }
-  @YOM_KIPPUR = { korbanot: 15, hodu: 36, yishtabach: 17 }
-  @YOM_TOB = { korbanot: 15, hodu: 26, yishtabach: 17 }
+  @YOM_KIPPUR = { korbanot: 15, hodu: 33, yishtabach: 17 }
+  @YOM_TOB = { korbanot: 15, hodu: 23, yishtabach: 17 }
   @HOL_HAMOED = { korbanot: 16, hodu: 13, yishtabach: 8.5 }
   @ASERET_YEMEI_TESHUBA = { selihot: 56, korbanot: 16, hodu: 11.5, yishtabach: 8.5 }
   @ELUL = { selihot: 50, korbanot: 16, hodu: 11.5, yishtabach: 8.5 }


Ace: fix no tabView exists issue with StackEditor

--- client/ace/lib/ace.coffee
+++ client/ace/lib/ace.coffee
@@ -682,8 +682,9 @@
       return

     { tabView }     = aceView.delegate
+    return  unless tabView
+
     activeTabHandle = tabView.getActivePane().getHandle()
-    return  unless tabView

     { handles }  = tabView
     targetHandle = null


Updated atom.io configuration

Updated atom.io configuration

--- config.cson
+++ config.cson
@@ -23,6 +23,8 @@
       beautify_on_save: true
     ruby:
       beautify_on_save: true
+  "atom-maven":
+    classpathFileName: ".atom-classpath"
   "autocomplete-java":
     classpathFilePath: ".atom-classpath"
     refreshClassOnSave: false


Switch to config

Switch to config

--- config.cson
+++ config.cson
@@ -11,7 +11,7 @@
     invisibles: {}
     fontFamily: "Monaco"
     scrollPastEnd: true
-    fontSize: 13
+    fontSize: 12
   core:
     ignoredNames: [
       ".git"
@@ -34,3 +34,4 @@
   "one-dark-ui": {}
   minimap:
     autoToggle: false
+  "crystal-tools": {}


Add DisplayBuffer.foldsStartingAtBufferRow

--- src/app/display-buffer.coffee
+++ src/app/display-buffer.coffee
@@ -115,16 +115,22 @@
     for marker in @findMarkers(class: 'fold', containsBufferRow: bufferRow)
       marker.destroy()

-  # Public: Given a buffer row, this returns the largest fold that starts there.
-  #
-  # Largest is defined as the fold whose difference between its start and end points
-  # are the greatest.
+  # Largest is defined as the fold whose difference between its start and end rows
+  # is the greatest.
   #
   # bufferRow - A {Number} indicating the buffer row
   #
-  # Returns a {Fold}.
+  # Returns a {Fold} or null if none exists.
   largestFoldStartingAtBufferRow: (bufferRow) ->
-    if marker = @findMarker(class: 'fold', startBufferRow: bufferRow)
+    @foldsStartingAtBufferRow(bufferRow)[0]
+
+  # Public: Given a buffer row, this returns all folds that start there.
+  #
+  # bufferRow - A {Number} indicating the buffer row
+  #
+  # Returns an {Array} of {Fold}s.
+  foldsStartingAtBufferRow: (bufferRow) ->
+    for marker in @findMarkers(class: 'fold', startBufferRow: bufferRow)
       @foldForMarker(marker)

   # Public: Given a screen row, this returns the largest fold that starts there.


Titles and Subtitles #messy

Titles and Subtitles #messy

--- docs/components/docs/DocsBody.cjsx
+++ docs/components/docs/DocsBody.cjsx
@@ -33,6 +33,20 @@
       inner:
         padding: '16px'

+      file:
+        paddingBottom: '60px'
+
+      title:
+        fontSize: '32px'
+        fontWeight: '200'
+        color: 'rgba(0,0,0,.67)'
+        paddingBottom: '20px'
+
+      subtitle:
+        fontSize: '24px'
+        fontWeight: '400'
+        color: 'rgba(0,0,0,.67)'
+
   render: ->

       <Container>
@@ -49,7 +63,16 @@

             { for fileName, file of docs
-                <Markdown key={ fileName }>{ /---[\s\S]*?---([\s\S]*)/.exec(file)[1] }</Markdown> }
+                regex = /---[\s\S]*?title: (.+)[\s\S]*?---([\s\S]*)/.exec(file)
+                title = regex[1]
+                body = regex[2]
+                if body.trim()
+

+
{ title }

+                    <Markdown>{ body }</Markdown>
+

+                else
+
{ title }
 }

         </Grid>


fixed the new count in tab handle, which would not show up on first time

--- client/landing/app/Applications/Groups.kdapplication/Views/grouptabhandleview.coffee
+++ client/landing/app/Applications/Groups.kdapplication/Views/grouptabhandleview.coffee
@@ -20,7 +20,7 @@

   markDirty:(@isDirty=yes)->
     if @isDirty
-      @setClass 'dirty'  unless @currentCount++
+      @setClass 'dirty'  unless ++@currentCount
       @newCount.updatePartial @currentCount
       @newCount.show()
     else


chunked storage: setup functions for sync.

--- salvus/cassandra.coffee
+++ salvus/cassandra.coffee
@@ -2882,6 +2882,35 @@
                     cb    : cb
         ], (err) => opts.cb?(err))

+    # Sync back and forth between a path and the database:
+    #
+    # Files with the same name and size are considered equal (for our application
+    # even the same name is equal).
+
+    # Copy any files in path not in database *to* the database.
+    sync_put: (opts) =>
+        opts = defaults opts,
+            path   : required
+            delete : false          # if true, deletes anything in the database not in the path
+            cb     : undefined
+        #async.series(
+
+    # Copy any files not in path *from* the database to the local directory.
+    sync_get: (opts) =>
+        opts = defaults opts,
+            path   : required
+            delete : false
+            cb     : undefined
+
+    # First copy any files from the database to path, and any from path (not in db) back to the database,
+    # so that the same files (the union) are in both.
+    sync: (opts) =>
+        opts = defaults opts,
+            path   : required
+            cb     : undefined
+
+
+

 quote_if_not_uuid = (s) ->
     if misc.is_valid_uuid_string(s)


get rid of reference to "demo3"

get rid of reference to "demo3"

--- salvus/page/head.coffee
+++ salvus/page/head.coffee
@@ -20,4 +20,4 @@
     else
         show_page(page_ids[0])

-connect_links_and_pages(["#about", "#demo1", "#demo2", "#demo3"], "#demo1")
+connect_links_and_pages(["#about", "#demo1", "#demo2"], "#demo1")


Added test for https://github.com/kmalakoff/knockback/issues/122

--- test/spec/ecosystem/backbone-relational.tests.coffee
+++ test/spec/ecosystem/backbone-relational.tests.coffee
@@ -966,4 +966,27 @@
   #   assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null
   #   done()

+  # https://github.com/kmalakoff/knockback/issues/122
+  it '13. Issue 113', (done) ->
+    kb.statistics = new kb.Statistics() # turn on stats
+
+    assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null
+    done()
+
+    Model2 = Backbone.RelationalModel.extend({})
+    Model1 = Backbone.RelationalModel.extend({relations: [{type: Backbone.HasOne, key: 'nested', relatedModel: Model2}]})
+
+    class ViewModel extends kb.ViewModel
+      constructor: (model, options) ->
+        super model, {requires: ['nested'], options: options}
+        @removeNested = => model.get('nested').destroy()
+        @addNested = => model.set({nested: new Model2()})
+        @hasNested = ko.computed => !!@nested()
+
+    view_model = new ViewModel(model = new Model1())
+    view_model.addNested()
+    assert.ok !!model.get('nested'), 'added a nested'
+    view_model.removeNested()
+    assert.ok !model.get('nested'), 'removed a nested'
+
   it 'CLEANUP', -> kb.configure({orm: 'default'})


:city_sunset::currency_exchange: Updated in browser at strd6.github.io/editor

--- pixie.cson
+++ pixie.cson
@@ -1,4 +1,4 @@
-version: "0.2.3-pre.2"
+version: "0.2.3-pre.3"
 entryPoint: "main"
 remoteDependencies: [
   "https://code.jquery.com/jquery-1.10.1.min.js"


Fix some tests

Fix some tests

--- test/integration/login.coffee
+++ test/integration/login.coffee
@@ -26,12 +26,13 @@
 createProfile = (options, done) ->
   login 'teststaff', process.env.CU_TEST_STAFF_PASSWORD, (err, res, body) ->
     form =
+      shortName: options.shortName
       displayName: options.displayName
       email: options.email
     form.logoUrl = options.logoUrl if options.logoUrl?

     request.post
-      uri: "#{BASE_URL}/api/#{options.shortName}"
+      uri: "#{BASE_URL}/api/user"
       form: form
     , (err, resp, body) ->
       obj = JSON.parse body
@@ -44,7 +45,6 @@
 describe 'Login', ->
   before (done) ->
     wd40.init ->
-      console.log browser
       browser.get LOGIN_URL, done

   before (done) ->
@@ -64,20 +64,23 @@
         wd40.fill '#username', 'ickletest', ->
           wd40.fill '#password', 'toottoot', ->
             wd40.click '#login', ->
-              browser.wait done
+              setTimeout ->
+                done()
+              , 500

       it 'shows my name', (done) ->
-        wd40.text '#subnav-path', (err, text) ->
-          text.should.include 'Mr Ickle Test'
+        # change "does not show my name" below as well if you change this
+        wd40.getText '#subnav-path .btn', (err, text) ->
+          text.should.include 'Ickle Test'
           done()

       context 'when I logout', ->
         before (done) ->
           wd40.click '#header .logout a', done

-        it 'redirects me to the login page', (done) ->
+        it 'redirects me to the home page', (done) ->
           wd40.trueURL (err, url) ->
-            url.should.equal LOGIN_URL
+            url.should.equal BASE_URL + "/"
             done()

      xcontext 'when I try to login with my email address as my username', ->
@@ -89,10 +92,11 @@
     before (done) ->
       login 'teststaff', process.env.CU_TEST_STAFF_PASSWORD, (err, res, body) =>
         form =
+          shortName: newUser
           displayName: newUser
           email: "pass@example.com"
         request.post
-          uri: "#{BASE_URL}/api/#{newUser}"
+          uri: "#{BASE_URL}/api/user"
           form: form
         , (err, resp, body) =>
           obj = JSON.parse body
@@ -158,12 +162,12 @@
         done()

     it 'shows me datasets of the profile into which I have switched', (done) ->
-      wd40.text '.dataset-list', (err, text) ->
+      wd40.getText '.dataset-list', (err, text) ->
         text.should.include dataset_name
         done()

     it "has the switched to profile's name", (done) ->
-      wd40.text 'h1', (err, text) ->
+      wd40.getText 'h1', (err, text) ->
         text.should.include 'Mr Ickle Test'
         done()

@@ -188,14 +192,14 @@
                 wd40.get BASE_URL, done

     it "hasn't changed who I am", (done) ->
-      wd40.text 'h1', (err, text) ->
+      wd40.getText 'h1', (err, text) ->
         text.should.include 'Mr Ickle Test'
-        wd40.text 'h1', (err, text) ->
+        wd40.getText 'h1', (err, text) ->
           text.should.not.include 'Staff Test'
           done()

     it 'still shows me my datasets', (done) ->
-      wd40.text '.dataset-list', (err, text) ->
+      wd40.getText '.dataset-list', (err, text) ->
         text.should.include dataset_name
         done()


Making data the mergable attribute for Amoeba sync

--- vendor/assets/javascripts/amoeba/sync.js.coffee
+++ vendor/assets/javascripts/amoeba/sync.js.coffee
@@ -7,15 +7,14 @@

 originalSync = Backbone.sync
 Backbone.sync = (method, model, options = {}) ->
-  if not options.data and model and (method is 'create' or method is 'update' or method is 'patch')
-    options.attrs ?= {}
+  if model and (method is 'create' or method is 'update' or method is 'patch')
+    options.data ?= {}
     data = {}
     if model.paramRoot
       data[model.paramRoot] = model.toJSON(options)
     else
       data = model.toJSON(options)
-
     options.contentType = 'application/json'
-    options.data = JSON.stringify(_.extend(options.attrs, data))
+    options.data = JSON.stringify(_.extend(options.data, data))

   originalSync(method, model, options)


Delete sniperino.coffee

Delete sniperino.coffee

--- scripts/sniperino.coffee
+++ scripts/sniperino.coffee
@@ -1,64 +0,0 @@
-###
-module.exports = (robot) ->
-  dongerOutcome = ['alive','dead']
-  gameOutcome = ['winning against', 'losing to']
-
-  # Respond to sniperino with a sniperino challenge, or get sassy if the user is already playing
-  robot.respond /sniperino( me)?/i, (res) ->
-    # Is user currently playing?
-    user = res.message.user.name
-    userChallenge = robot.brain.get(user)
-    # User isn't currenly playing
-    if userChallenge <= 0
-      robot.brain.set user, rng()
-      res.send "#{user}, roll higher than a #{robot.brain.get(user)} or the donger gets it!"
-    else
-      res.send "#{user} is already playing Sniperino, you doofus! I oughtta sniperino YOU!"
-
-  # Respond to roll with a roll, whether against a sniperino game or for the lolz
-  robot.respond /roll( me)?/i, (res) ->
-    roll = rng()
-    user = res.message.user.name
-    challenge = robot.brain.get(user).challenge
-
-    # Is user currently playing Sniperino?
-    if challenge > 0
-      # 0 is a loss, 1 is a win
-      if roll > challenge then gameStatus = 1 else gameStatus = 0
-      res.send "#{user} rolled a #{roll}, #{gameOutcome[gameStatus]} the challenge of #{challenge}! #{dongerOutcome[gameStatus]}"
-      robot.brain.set user, 0
-      updateStats(user, gameStatus)
-    else
-      res.send "#{user} rolled a #{roll}!"
-
-  # Send a list of users with their win percentages
-  robot.respond /stats( me)?/i, (res) ->
-    getStats(res)
-      .then (data) ->
-        stats = data
-          .sort (a, b) ->
-            return -1 if a.winPct > b.winPct
-            return 1 if a.winPct < b.winPct
-            return 0
-          .map (stats) ->
-            "#{stats.name}: #{stats.winPct}%"
-        res.send stats...
-      .done()
-
-  # Update number of wins and number of games played for user
-  updateStats = (user, outcome) ->
-
-    # For the first timers
-    if !games then robot.brain.set "#{user} games", 1 else robot.brain.set "#{user} games", (ParseInt robot.brain.get "#{user} games") + 1
-    if !wins then robot.brain.set "#{user} wins", outcome else robot.brain.set "#{user} wins", (ParseInt robot.brain.get "#{user} wins") + 1
-
-  getStats = (res) ->
-    gamesArray = robot.brain.get "/(.*)( games)"
-    winsArray  = robot.brain.get "/(.*)( wins)"
-    res.send userArray
-    return userArray
-
-  # Generate a random number between 1 and 100 inclusive
-  rng = () ->
-    return (Math.floor(Math.random() * 100) + 1)
-###


Make constructors for all HTML5 elements.

--- client/html.coffee
+++ client/html.coffee
@@ -2,8 +2,38 @@
   e = $ "<#{name}>"
   e.append v

-for n in ["label", "div", "span", "button", "img", "figure", "figcaption",
-          "h1", "h2", "h3",
-          "table", "thead", "tfoot", "tbody", "tr", "th", "td",
-          "form", "select", "option"]
+# Make HTML element constructors of the same names as the elements.
+for n in [# Root element
+          "html",
+          # Document metadata
+          "head", "title", "base", "link", "meta", "style",
+          # Scripting
+          "script", "noscript", "template",
+          # Sections
+          "body", "section", "nav", "article", "aside",
+          "h1", "h2", "h3", "h4", "h5", "h6",
+          "header", "footer", "address", "main",
+          # Grouping content
+          "p", "hr", "pre", "blockquote", "ol", "ul", "li", "dl",
+          "dt", "dd", "figure", "figcaption", "div",
+          # Text-level semantics
+          "a", "em", "strong", "small", "s", "cite", "q", "dfn",
+          "abbr", "data", "time", "code", #"var",
+          "samp", "kdb", "sub", "sup", "i", "b", "u", "mark",
+          "ruby", "rt", "rp", "bdi", "bdo", "span", "br", "wbr",
+          # Edit
+          "ins", "del",
+          # Embedded content
+          "img", "iframe", "embed", "object", "param", "video",
+          "audio", "source", "track", "canvas", "map", "area",
+          "svg", "math",
+          # Tabular data
+          "table", "caption", "colgroup", "col", "tbody", "thead",
+          "tfoot", "tr", "td", "th",
+          # Forms
+          "form", "fieldset", "legend", "label", "input", "button",
+          "select", "datalist", "optgroup", "option", "textarea",
+          "keygen", "output", "progress", "meter",
+          # Interactive elements.
+          "details", "summary", "meuitem", "menu"]
   @[n] = do (n) -> (v) -> make_element n, v


Remove lint warnings.

Remove lint warnings.

--- src/spec/integration-inventory.spec.coffee
+++ src/spec/integration-inventory.spec.coffee
@@ -14,7 +14,7 @@
     @sync = new InventorySync config: Config.staging
     del = (id) =>
       deferred = Q.defer()
-      @sync._rest.DELETE "/inventory/#{id}", (error, response, body) =>
+      @sync._rest.DELETE "/inventory/#{id}", (error, response, body) ->
         if error
           deferred.reject error
         else
@@ -24,7 +24,7 @@
             deferred.reject body
       deferred.promise

-    @sync._rest.GET "/inventory?limit=0", (error, response, body) =>
+    @sync._rest.GET "/inventory?limit=0", (error, response, body) ->
       stocks = JSON.parse(body).results
       if stocks.length is 0
         done()
@@ -32,9 +32,9 @@
       for s in stocks
         dels.push del(s.id)

-      Q.all(dels).then (v) =>
+      Q.all(dels).then (v) ->
         done()
-      .fail (err) =>
+      .fail (err) ->
         console.log err
         expect(false).toBe true

@@ -49,7 +49,7 @@
       expect(error).toBeNull()
       expect(response.statusCode).toBe 201
       e = JSON.parse(body)
-      @sync.buildActions(ieChanged, e).update (error, response, body) =>
+      @sync.buildActions(ieChanged, e).update (error, response, body) ->
         expect(error).toBeNull()
         expect(response.statusCode).toBe 200
         expect(JSON.parse(body).quantityOnStock).toBe 7


payment: fix nodejs linting problem

payment: fix nodejs linting problem

--- workers/social/lib/social/models/payment.coffee
+++ workers/social/lib/social/models/payment.coffee
@@ -51,47 +51,47 @@

   @subscribe = secure (client, data, callback) ->
-    return callback new Error "subscriptions are closed via this endpoint"
+    return callback new Error 'subscriptions are closed via this endpoint'

   @subscribeGroup = secure (client, data, callback) ->
-    return callback new Error "subscriptions are closed via this endpoint"
+    return callback new Error 'subscriptions are closed via this endpoint'

   @fetchGroupPlan = secure (client, callback) ->
-    return callback new Error "subscriptions are closed via this endpoint"
+    return callback new Error 'subscriptions are closed via this endpoint'

   @cancelGroupPlan = secure (client, callback) ->
-    return callback new Error "subscriptions are closed via this endpoint"
+    return callback new Error 'subscriptions are closed via this endpoint'

   @subscriptions = secure (client, data, callback) ->
-    return callback new Error "subscriptions are closed via this endpoint"
+    return callback new Error 'subscriptions are closed via this endpoint'

   @fetchGroupInvoices = secure (client, callback) ->
-    return callback new Error "subscriptions are closed via this endpoint"
+    return callback new Error 'subscriptions are closed via this endpoint'

   @invoices = secure (client, data, callback) ->
-    return callback new Error "subscriptions are closed via this endpoint"
+    return callback new Error 'subscriptions are closed via this endpoint'

   @creditCard = secure (client, data, callback) ->
-    return callback new Error "subscriptions are closed via this endpoint"
+    return callback new Error 'subscriptions are closed via this endpoint'

   @fetchGroupCreditCard$ = secure (client, callback) ->
-    return callback new Error "subscriptions are closed via this endpoint"
+    return callback new Error 'subscriptions are closed via this endpoint'

   @updateCreditCard = secure (client, data, callback) ->
-    return callback new Error "subscriptions are closed via this endpoint"
+    return callback new Error 'subscriptions are closed via this endpoint'

   @updateGroupCreditCard = secure (client, data, callback) ->
-    return callback new Error "subscriptions are closed via this endpoint"
+    return callback new Error 'subscriptions are closed via this endpoint'

   @canChangePlan = secure (client, data, callback) ->
-    return callback new Error "subscriptions are closed via this endpoint"
+    return callback new Error 'subscriptions are closed via this endpoint'

   @logOrder = secure (client, raw, callback) ->
     SiftScience = require './siftscience'
     SiftScience.createOrder client, raw, callback

   @canUserPurchase = secure (client, callback) ->
-    return callback new Error "subscriptions are closed via this endpoint"
+    return callback new Error 'subscriptions are closed via this endpoint'

   @deleteAccount = (client, callback) ->
-    return callback new Error "subscriptions are closed via this endpoint"
+    return callback new Error 'subscriptions are closed via this endpoint'


Social: do not wrap session in an object

--- workers/social/lib/social/models/session.coffee
+++ workers/social/lib/social/models/session.coffee
@@ -116,7 +116,7 @@
   @fetchSessionByData = (data, callback) ->
     @one data, (err, session) =>
       return callback err  if err
-      return callback null, { session }  if session?
+      return callback null, session  if session?
       @createNewSession data, callback


Added forgotten whitespace.

Added forgotten whitespace.

--- tests-src/test-factory.coffee
+++ tests-src/test-factory.coffee
@@ -4,11 +4,11 @@
         a = messages.slice(0)
         r = new SockJS(sockjs_url + '/echo', [protocol])
         r.onopen = (e) ->
-            log('onopen' + e)
+            log('onopen ' + e)
             ok(true)
             r.send(a[0])
         r.onmessage = (e) ->
-            #log('onmessage');
+            #log('onmessage ' + e);
             deepEqual(e.data, a[0])
             a.shift()
             if typeof a[0] is 'undefined'
@@ -19,7 +19,7 @@
             if a.length
                 ok(false, "Transport closed prematurely.")
             else
-                log('onclose' + e)
+                log('onclose ' + e)
                 ok(true)
             start()


coralEDA: add support for rounded pads

coralEDA: add support for rounded pads

--- src/coraleda-generator.coffee
+++ src/coraleda-generator.coffee
@@ -14,9 +14,6 @@
 # board layout editor: pcb-rnd http://repo.hu/projects/pcb-rnd/
 # lihata subcirctuit pattern format: http://repo.hu/projects/pcb-rnd/developer/lihata_format/
 #
-# rounded pads are not implemented yet
-# the subcircuit format does not support rounded rectangles
-# rounded corner need to be approximated using polygon lines

 class CoraledaGenerator
   #
@@ -174,16 +171,50 @@
             if layer.endsWith("Mask")
               width += 2 * (padstack.mask || pattern.settings.clearance.padToMask || 0)
               height += 2 * (padstack.mask || pattern.settings.clearance.padToMask || 0)
-            fs.writeSync(fd, "       li:ps_poly {\n")
-            fs.writeSync(fd, sprintf("        #{@f}mm\n", width / -2))
-            fs.writeSync(fd, sprintf("        #{@f}mm\n", height / -2))
-            fs.writeSync(fd, sprintf("        #{@f}mm\n", width / 2))
-            fs.writeSync(fd, sprintf("        #{@f}mm\n", height / -2))
-            fs.writeSync(fd, sprintf("        #{@f}mm\n", width / 2))
-            fs.writeSync(fd, sprintf("        #{@f}mm\n", height / 2))
-            fs.writeSync(fd, sprintf("        #{@f}mm\n", width / -2))
-            fs.writeSync(fd, sprintf("        #{@f}mm\n", height / 2))
-            fs.writeSync(fd, "       }\n") # end ps_poly
+            if pattern.settings.smoothPadCorners
+              corner = if width < height then width else height
+              corner *= pattern.settings.ratio.cornerToWidth
+              if corner > pattern.settings.maximum.cornerRadius
+                corner = pattern.settings.maximum.cornerRadius
+              angle_step = 10
+              # draw rounded rectangle using line approximation (arcs are not possible in polygons)
+              fs.writeSync(fd, "       li:ps_poly {\n")
+              # top right corner
+              for angle in [0..90] by angle_step
+                angle_x = Math.cos(Math.PI * 2 / 360 * angle) * corner
+                angle_y = Math.sin(Math.PI * 2 / 360 * angle) * corner
+                fs.writeSync(fd, sprintf("        #{@f}mm\n", width / 2 - corner + angle_x))
+                fs.writeSync(fd, sprintf("        #{@f}mm\n", height / -2 + corner - angle_y))
+              # top left corner
+              for angle in [90..180] by angle_step
+                angle_x = Math.cos(Math.PI * 2 / 360 * angle) * corner
+                angle_y = Math.sin(Math.PI * 2 / 360 * angle) * corner
+                fs.writeSync(fd, sprintf("        #{@f}mm\n", width / -2 + corner + angle_x))
+                fs.writeSync(fd, sprintf("        #{@f}mm\n", height / -2 + corner - angle_y))
+              # bottom left corner
+              for angle in [180..270] by angle_step
+                angle_x = Math.cos(Math.PI * 2 / 360 * angle) * corner
+                angle_y = Math.sin(Math.PI * 2 / 360 * angle) * corner
+                fs.writeSync(fd, sprintf("        #{@f}mm\n", width / -2 + corner + angle_x))
+                fs.writeSync(fd, sprintf("        #{@f}mm\n", height / 2 - corner - angle_y))
+              # bottom right corner
+              for angle in [270..360] by angle_step
+                angle_x = Math.cos(Math.PI * 2 / 360 * angle) * corner
+                angle_y = Math.sin(Math.PI * 2 / 360 * angle) * corner
+                fs.writeSync(fd, sprintf("        #{@f}mm\n", width / 2 - corner + angle_x))
+                fs.writeSync(fd, sprintf("        #{@f}mm\n", height / 2 - corner - angle_y))
+              fs.writeSync(fd, "       }\n") # end ps_poly
+            else
+              fs.writeSync(fd, "       li:ps_poly {\n")
+              fs.writeSync(fd, sprintf("        #{@f}mm\n", width / -2))
+              fs.writeSync(fd, sprintf("        #{@f}mm\n", height / -2))
+              fs.writeSync(fd, sprintf("        #{@f}mm\n", width / 2))
+              fs.writeSync(fd, sprintf("        #{@f}mm\n", height / -2))
+              fs.writeSync(fd, sprintf("        #{@f}mm\n", width / 2))
+              fs.writeSync(fd, sprintf("        #{@f}mm\n", height / 2))
+              fs.writeSync(fd, sprintf("        #{@f}mm\n", width / -2))
+              fs.writeSync(fd, sprintf("        #{@f}mm\n", height / 2))
+              fs.writeSync(fd, "       }\n") # end ps_poly
         fs.writeSync(fd, "       ha:layer_mask {\n")
         if layer.startsWith("top")
           fs.writeSync(fd, "        top = 1\n")


Fix broken sql statement

Fix broken sql statement

--- storage/lib/geocache.coffee
+++ storage/lib/geocache.coffee
@@ -115,13 +115,13 @@
                 @db.insert numberedParameters: true
                     .into 'geocaches'
                     .set 'id', id
-                    .set 'updated', updated or 'CURRENT_TIMESTAMP', dontQuote: not updated?
+                    .set 'updated', updated or 'now', dontQuote: not updated?
                     .set 'data', JSON.stringify data
                     .toParam()
             else
                 @db.update numberedParameters: true
                     .table 'geocaches'
-                    .set 'updated', updated or 'CURRENT_TIMESTAMP', dontQuote: not updated?
+                    .set 'updated', updated or 'now', dontQuote: not updated?
                     .set 'data', JSON.stringify data
                     .where 'id = ?', id
                     .toParam()


added forceInterval flag to druid driver

--- src/driver/druidDriver.coffee
+++ src/driver/druidDriver.coffee
@@ -137,9 +137,10 @@
       else
         throw new Error("unknown filter type '#{filter.type}'")

-  addFilter: (filter) ->
+  addFilter: (filter, forceInterval) ->
     [@filter, @intervals] = @filterToDruid(filter)
     if not @intervals
+      throw new Error("must have an interval") if forceInterval
       @intervals = DruidQueryBuilder.allTimeInterval
     return this

@@ -365,7 +366,7 @@

 druidQueryFns = {
-  all: ({requester, dataSource, timeAttribute, filter, condensedQuery}, callback) ->
+  all: ({requester, dataSource, timeAttribute, filter, forceInterval, condensedQuery}, callback) ->
     if condensedQuery.applies.length is 0
       # Nothing to do as we are not calculating anything (not true, ToDo: fix this)
       callback(null, [{
@@ -379,7 +380,7 @@
     try
       # filter
       if filter
-        druidQuery.addFilter(filter)
+        druidQuery.addFilter(filter, forceInterval)

       # apply
       for apply in condensedQuery.applies
@@ -415,7 +416,7 @@
       return
     return

-  timeseries: ({requester, dataSource, timeAttribute, filter, condensedQuery}, callback) ->
+  timeseries: ({requester, dataSource, timeAttribute, filter, forceInterval, condensedQuery}, callback) ->
     if condensedQuery.applies.length is 0
       # Nothing to do as we are not calculating anything (not true, ToDo: fix this)
       callback(null, [{
@@ -432,7 +433,7 @@

       # filter
       if filter
-        druidQuery.addFilter(filter)
+        druidQuery.addFilter(filter, forceInterval)

       # apply
       for apply in condensedQuery.applies
@@ -478,7 +479,7 @@
       return
     return

-  topN: ({requester, dataSource, timeAttribute, filter, condensedQuery}, callback) ->
+  topN: ({requester, dataSource, timeAttribute, filter, forceInterval, condensedQuery}, callback) ->
     if condensedQuery.applies.length is 0
       # Nothing to do as we are not calculating anything (not true, ToDo: fix this)
       callback(null, [{
@@ -495,7 +496,7 @@

       # filter
       if filter
-        druidQuery.addFilter(filter)
+        druidQuery.addFilter(filter, forceInterval)

       # apply
       for apply in condensedQuery.applies
@@ -539,7 +540,7 @@
       return
     return

-  histogram: ({requester, dataSource, timeAttribute, filter, condensedQuery}, callback) ->
+  histogram: ({requester, dataSource, timeAttribute, filter, forceInterval, condensedQuery}, callback) ->
     callback("not implemented yet"); return # ToDo
     # data.queryType = "timeseries"
     # data.postAggregations = null
@@ -556,7 +557,7 @@
 }

-module.exports = ({requester, dataSource, timeAttribute, approximate, filter}) ->
+module.exports = ({requester, dataSource, timeAttribute, approximate, filter, forceInterval}) ->
   timeAttribute or= 'time'
   approximate ?= true
   return (query, callback) ->
@@ -588,6 +589,7 @@
           dataSource
           timeAttribute
           filter: if parentSegment then parentSegment._filter else filter
+          forceInterval
           condensedQuery
         }, (err, splits) ->
           if err


automatic date handling

automatic date handling

--- src/driver/druidDriver.coffee
+++ src/driver/druidDriver.coffee
@@ -90,10 +90,12 @@
         }]

       when 'within'
-        r0 = filter.range[0]
-        r1 = filter.range[1]
+        [r0, r1] = filter.range
         if filter.attribute is @timeAttribute
+          r0 = new Date(r0) if typeof r0 is 'string'
+          r1 = new Date(r1) if typeof r1 is 'string'
           throw new Error("start and end must be dates") unless r0 instanceof Date and r1 instanceof Date
+          throw new Error("invalid dates") if isNaN(r0) or isNaN(r1)
           [
             null,
             ["#{@dateToIntervalPart(r0)}/#{@dateToIntervalPart(r1)}"]


KD: Add KD.tell helper for appManager.tell

--- client/Main/KD.extend.coffee
+++ client/Main/KD.extend.coffee
@@ -154,6 +154,8 @@
   getReferralUrl: (username) ->
     "#{location.origin}/R/#{username}"

+  tell: (rest...)-> KD.getSingleton('appManager').tell rest...
+
 Object.defineProperty KD, "defaultSlug",
   get:->
     if KD.isGuest() then 'guests' else 'koding'


fix for group settings not showing

fix for group settings not showing

--- jsapp/xlform_model_view/view.rowDetail.coffee
+++ jsapp/xlform_model_view/view.rowDetail.coffee
@@ -97,8 +97,15 @@
         changeModelValue($elVal)
       return

-    insertInDOM: (rowView)->
-      rowView.rowExtras.find('.card__settings__fields--question-options').append(@el)
+    get_correct_element: (rowView, selector) ->
+      el = rowView.rowExtras.find(selector)
+      if el.length == 0
+        el = rowView.rowExtras
+      return el
+
+    insertInDOM: (rowView)->
+      el = @get_correct_element rowView, '.card__settings__fields--question-options'
+      el.append(@el)

     renderInRowView: (rowView)->
       @render()
@@ -141,7 +148,7 @@
     afterRender: ->
       @listenForInputChange()

-  viewRowDetail.DetailViewMixins.constraint_message =
+  viewRowDetail.DetailViewMixins.constraint_message =
     html: ->
       @$el.addClass("card__settings__fields--active")
       """
@@ -168,7 +175,7 @@
       @skipLogicEditor.builder = @model.builder
       @skipLogicEditor.render()
     insertInDOM: (rowView) ->
-      rowView.rowExtras.find('.card__settings__fields--skip-logic').append(@el)
+      @get_correct_element(rowView, '.card__settings__fields--skip-logic').append(@el)

   viewRowDetail.DetailViewMixins.constraint =
     html: ->
@@ -187,7 +194,7 @@
     afterRender: ->
       @listenForInputChange()
     insertInDOM: (rowView) ->
-      rowView.rowExtras.find('.card__settings__fields--validation-criteria').append(@el)
+      @get_correct_element(rowView, '.card__settings__fields--validation-criteria').append(@el)

   viewRowDetail.DetailViewMixins.name =
     html: ->


#957 - Form settings box doesn't close when user clicks elsewhere

--- jsapp/xlform_model_view/view.surveyApp.coffee
+++ jsapp/xlform_model_view/view.surveyApp.coffee
@@ -195,7 +195,12 @@
       $et = $(evt.currentTarget)
       $et.toggleClass('active__settings')
       if @features.surveySettings
-        @$(".form__settings").toggle()
+        $settings = @$(".form__settings")
+        $settings.toggle()
+        $('body').click (e) ->
+          if e.target not in [$et[0], $settings[0]] && $settings.find(e.target).length == 0
+            $settings.hide()
+            $et.removeClass('active__settings')

     toggleGroupSettings: (evt)->
       $et = $(evt.currentTarget)


add test for max rings

add test for max rings

--- test/macro-tool_test.coffee
+++ test/macro-tool_test.coffee
@@ -187,6 +187,11 @@
           { line: { transform: 'rotate(45)' } }
           { line: { transform: 'rotate(45)' } }
         ]
+      it 'should have no more than maxrings (arg 6)', ->
+        m = new Macro ['AMNAME']
+        m.primitive [6, 0, 0, 20, 1, 1, 2, 1, 22, 0]
+        # shapes should have two rings and two lines
+        m.shapes.length.should.equal 4
       it 'should throw an error if rotation given when center is not 0,0', ->
         m = new Macro ['AMNAME']
         (-> m.primitive [6, 1, 1, 20, 2, 2, 3, 2, 22, 45])


Add exponential back off to task page auto-refresh (Resolves #104)

--- SingularityUI/app/views/task.coffee
+++ SingularityUI/app/views/task.coffee
@@ -12,6 +12,7 @@
     killTaskTemplate: require './templates/vex/killTask'

     initialize: ->
+        @sandboxTries = 0
         @taskFiles = {}
         @taskHistory = new TaskHistory {}, taskId: @options.taskId

@@ -29,6 +30,7 @@
             @taskFiles = new TaskFiles {}, { taskId: @options.taskId, offerHostname: @taskHistory.attributes.task.offer.hostname, directory: @taskHistory.attributes.directory }
             @taskFiles.testSandbox()
                 .done(=>
+                    @sandboxTries = 0
                     @taskFiles.fetch().done =>
                         @taskFilesFetchDone = true
                         @taskFilesSandboxUnavailable = false
@@ -39,6 +41,7 @@
                     @taskFilesSandboxUnavailable = true
                     deferred.resolve()
                 )
+            @sandboxTries += 1

         deferred

@@ -53,7 +56,7 @@

         if @taskHistory.attributes.taskUpdates?.length is 0
             @taskHistory.attributes.hasNoTaskUpdates = true
-            setTimeout (=> @refresh()), 3 * 1000
+            setTimeout (=> @refresh()), (1 + Math.pow(1.5, @sandboxTries)) * 1000

         context =
             request: @taskHistory.attributes.task.taskRequest.request


GameObject.Effect

GameObject.Effect

--- src/game_object_effect.coffee
+++ src/game_object_effect.coffee
@@ -9,7 +9,7 @@
       player.fadeOut 2

       # Fade out and then destroy
-      player.fadeOut, 0.25
+      player.fadeOut, 0.25, "destroy"

   @name fadeOut
   @methodOf GameObject#


do manual call to renderer.clear()

do manual call to renderer.clear()

--- src/webapp/_scripts/r7/Stage4Render.coffee
+++ src/webapp/_scripts/r7/Stage4Render.coffee
@@ -177,6 +177,7 @@
           #
           #_cameraControls.update()
           nLookAt(_camera, obj.position) if obj? and _camera.gameDriven
+          _renderer.clear()
           _renderer.render(_scene, _camera)

       addLights = (scene, camera) ->


commented the parser

commented the parser

--- server/src/Parser.coffee
+++ server/src/Parser.coffee
@@ -25,93 +25,104 @@
     @idx  = 0              # start at token 0
     this.handleAddMinus()  # Start with minus because it has lowest precedence

+  # Handler for '+' and '-' (only the binary versions).
   handleAddMinus: () ->
-    child1 = @handleMultiplyDivide()
+    child1 = @handleMultiplyDivide() # parses the left hand expression
     node = child1
     c = @expr[@idx]
-    while c == DICEFACESYMBOLS.plus or c == DICEFACESYMBOLS.minus
+    while c == DICEFACESYMBOLS.plus or c == DICEFACESYMBOLS.minus # checks if the current operator is '+' or '-'
       ++@idx
-      if @expr[@idx] == DICEFACESYMBOLS.bracketR
+      if @expr[@idx] == DICEFACESYMBOLS.bracketR # detects if a right bracket ')' is wrongly placed.
         ErrorManager.throw(ERRORCODES.parseError, {token: @idx, diceface:c}, "Invalid value before bracket")
-      child2 = @handleMultiplyDivide()
+      child2 = @handleMultiplyDivide()  # parses the right hand expression
       node = new Node(type: "binop", token: [c], children: [child1, child2])
       c = @expr[@idx]
       child1 = node
     node

+  # Handler for '*' and '/'.
   handleMultiplyDivide: () ->
-    child1 = @handlePower()
+    child1 = @handlePower() # parses the left hand expression
     node = child1
     c = @expr[@idx]
-    while c == DICEFACESYMBOLS.multiply or c == DICEFACESYMBOLS.divide
+    while c == DICEFACESYMBOLS.multiply or c == DICEFACESYMBOLS.divide # checks if the current operator is '*' or '/'
       ++@idx
-      child2 = @handlePower()
+      child2 = @handlePower() # parses the right hand expression
       if(c == DICEFACESYMBOLS.divide)
         e = new Evaluator
-        if e.evaluate(child2) == 0
+        if e.evaluate(child2) == 0 #detects division by 0
           ErrorManager.throw(ERRORCODES.parserDivByZero, {token: @idx, diceface:c}, "You can't divide by zero ")
       node = new Node(type : "binop", token: [c], children: [ child1, child2 ]);
       c = @expr[@idx]
       child1 = node
     node
+
+  # Handler for 'power'.
   handlePower: () ->
-    child1 = @handleUnaryOps()
+    child1 = @handleUnaryOps() # parses the left hand expression
     node = child1
     c = @expr[@idx]
-    while c == DICEFACESYMBOLS.power
+    while c == DICEFACESYMBOLS.power # checks if the current operator is a power
       ++@idx
-      child2 = @handleUnaryOps()
+      child2 = @handleUnaryOps() # parses the right hand expression
       node = new Node(type : "binop", token: [c], children: [ child1, child2 ]);
       c = @expr[@idx]
       child1 = node
     node
+
+  # Handler for '+' and '-' (only the unary versions).
   handleUnaryOps: () ->
     c = @expr[@idx]
     node = {}
-    if c == DICEFACESYMBOLS.minus or c == DICEFACESYMBOLS.plus or c == DICEFACESYMBOLS.sqrt
+    if c == DICEFACESYMBOLS.minus or c == DICEFACESYMBOLS.plus or c == DICEFACESYMBOLS.sqrt # checks if ithe current operator is a unary operator
       ++@idx
-      if @expr[@idx] == DICEFACESYMBOLS.bracketR
+      if @expr[@idx] == DICEFACESYMBOLS.bracketR ## detects if a right bracket ')' is wrongly placed.
         ErrorManager.throw(ERRORCODES.parseError, {token: @idx, diceface:c}, "Invalid value before bracket")
       if c == DICEFACESYMBOLS.sqrt
         temp = @idx
         e = new Evaluator()
-        if e.evaluate(@handleUnaryOps())<0
+        if e.evaluate(@handleUnaryOps())<0 # detects if a right bracket ')' is wrongly placed.
           ErrorManager.throw(ERRORCODES.parserSqrtNeg, {token: @idx, diceface:c}, "You can't square root a negative")
         @idx = temp
       node = new Node(type: "unaryop", token: [c], children: [@handleUnaryOps()])
-    else if c == DICEFACESYMBOLS.multiply or c == DICEFACESYMBOLS.divide or c == DICEFACESYMBOLS.power
+    else if c == DICEFACESYMBOLS.multiply or c == DICEFACESYMBOLS.divide or c == DICEFACESYMBOLS.power # detects wrongly placed binary operators
       ErrorManager.throw(ERRORCODES.parseError, {token: @idx, diceface:c}, "This operator isn't allowed to go here")
     else
-      node = @handleParen()
+      node = @handleParen() # parses the expression without the operator
     node
+
+  # Handler for '(' and ')'.
   handleParen: () ->
     c = @expr[@idx]
-    if c == DICEFACESYMBOLS.bracketL
+    if c == DICEFACESYMBOLS.bracketL # checks if the current symbol is a left bracket
       ++@idx
+      # detects wrongly placed operators after left bracket
       if @expr[@idx] == DICEFACESYMBOLS.multiply || @expr[@idx] == DICEFACESYMBOLS.divide || @expr[@idx] == DICEFACESYMBOLS.power || @expr[@idx] == DICEFACESYMBOLS.bracketR
         ErrorManager.throw(ERRORCODES.parseError, {token: @idx, diceface:c}, "Invalid value before bracket")
-      node = @handleAddMinus()
-      if @expr[@idx] != DICEFACESYMBOLS.bracketR
+      node = @handleAddMinus() # parses the expression inside the brackets.
+      if @expr[@idx] != DICEFACESYMBOLS.bracketR # detects unbalanced bracketing
         ErrorManager.throw(ERRORCODES.parserUnbalancedBrack, {token: @idx, diceface:c}, "Error Unbalanced Parenthesis")
-      ++@idx # move past the '('
+      ++@idx
     else
-      node = @atom()
+      node = @atom() # reaches here only if expression is an atom. Parses this atom.
     node
+
   # Handle atomimic bits, numbers and variables
   atom:() ->
     c = @expr[@idx]
-    if @expr[@idx+1]? && @expr[@idx+1] == DICEFACESYMBOLS.bracketL
+    if @expr[@idx+1]? && @expr[@idx+1] == DICEFACESYMBOLS.bracketL # detects if you have a number before brackets (for multiplication), which isn't allowed in this game
       ErrorManager.throw(ERRORCODES.parserMultBrackWithoutTimes, {token: @idx, diceface:c}, "Invalid syntax. Must use a cross to multiply")
-    if this.isNumber(c)
+    if this.isNumber(c) # checks if the current token is a number
       node = new Node(type: "number", token: this.matchNumber())
-    else if !c?
+    else if !c? # detects if you finish an equation with an operator
       ErrorManager.throw(ERRORCODES.parseError, {token: @idx-1
         , diceface:c}, "You can't finish with an operator")
-    else
+    else # for all other errors
       ErrorManager.throw(ERRORCODES.parseError, {token: @idx, diceface:c}, "UNEXPECTED TOKEN: #{c}")
     return node

-  isNumber : (c) -> c >= DICEFACESYMBOLS.zero
+  # checks if the given value is a number
+  isNumber : (c) -> c >= DICEFACESYMBOLS.zero
   matchNumber:()-> this.match(this.isNumber)

   atEnd: () -> @idx == @expr.length
@@ -121,17 +132,11 @@
     numMatched = 0
     c = @expr[@idx]
     result.push @expr[@idx++] while !this.atEnd() and matchFn(@expr[@idx]) and ++numMatched <=2
-    if(numMatched >2 && @isGoal)
+    if(numMatched >2 && @isGoal) # prevents using numbers with more than 2 digits when setting the goal
       ErrorManager.throw(ERRORCODES.parserTooManyDigits, {token: @idx, diceface:c, maxdigits:2}, "Can't have more than two digits")
-    else if (numMatched >1)
+    else if (numMatched >1) # prevents using numbers with more than 1 digit when giving a solution
       ErrorManager.throw(ERRORCODES.parserTooManyDigits, {token: @idx, diceface:c, maxdigits:1}, "Can't have more than one digit")
     result
-    #if(numMatched >2 && @isGoal)
-      #throw "Can't have more than two digits, maytey"
-    #else if (numMatched >1)
-      #console.log @isGoal
-      #throw "Can't have more than one digit, maytey"
-    #result

   precedence: (node) ->
     if node.type == 'unaryop'