<commit_before>//Translated from hunter.rb using babyruby2js
'use strict';

var main = require('../../main');

var Grid = require('../../Grid');
var Heuristic = require('./Heuristic');
var inherits = require('util').inherits;

var ALWAYS = main.ALWAYS;

/** @class Hunters find threats to struggling enemy groups.
 *  Ladder attack fits in here. */
function Hunter(player) {
    Heuristic.call(this, player);

    this.takeLifeCoeff = this.getGene('takeLife', 1, 0.01, 2);

    this.snapbacks = null;
}
inherits(Hunter, Heuristic);
module.exports = Hunter;

/** Returns true if group g could get at least minLives by killing one of the
 *  enemy groups around in atari
 */
Hunter.prototype._gotLivesFromKillingAround = function (g, minLives) {
    var enemies = g.allEnemies();
    for (var n = enemies.length - 1; n >= 0; n--) {
        var eg = enemies[n];
        if (eg.lives > 1) continue;
        // found 1 enemy group in atari; killing it would give us its size in new lives
        var addedLives = eg.stones.length;
        if (addedLives >= minLives) return true;
        // this is not enough, so count lives we would get by connecting with our groups around eg
        var allies = eg.allEnemies();
        for (var a = allies.length - 1; a >= 0; a--) {
            addedLives += allies[a].lives - 1;
            if (addedLives >= minLives) return true;
        }
    }
    return false;
};

var KO_KILL_SCORE = 1;

/** Returns a score to measure what kind of kill a move is.
 *  Any number < 1.01 is sign of a "not so good" kill.
 *  E.g. in this board, black c5 has a bad "kill score" of 1.0001
 *  5 O@+OO
 *  4 O@O@+
 *  3 OO@@+
 *  2 ++@++
 *  1 ++@++
 *    abcde
 *  NB: kill score of a KO is 1 (KO_KILL_SCORE)
 */
Hunter.prototype._killScore = function (empty, color) {
    var numAllies = 0, numKill = 0, life = 0;
    for (var i = empty.neighbors.length - 1; i >= 0; i--) {
        var n = empty.neighbors[i];
        switch (n.color) {
        case main.EMPTY:
            life += 0.01;
            break;
        case color: // ally
            life += (n.group.lives - 1) * 0.01;
            if (n.group.isAlive === ALWAYS) life += 2;
            numAllies += 0.0001;
            break;
        default: // enemy
            if (n.group.lives > 1) break; // not a kill
            numKill += n.group.stones.length;
        }
    }
    return numKill + life + numAllies;
};

Hunter.prototype._countAtariThreat = function (enemies, level) {
    var atariThreat = 0, eg;
    for (var egNdx = enemies.length - 1; egNdx >= 0; egNdx--) {
        eg = enemies[egNdx];
        if (eg.lives !== 1) continue;
        // if we can take eg anytime later, no need to take it now
        //TODO also verify no group in "enemies" is strong
        if (!level && this._isAtariGroupCaught(eg, level) && !this._gotLivesFromKillingAround(eg, 1)) {
            continue;
        }
        atariThreat += this.groupThreat(eg);
    }
    return atariThreat;
};

Hunter.prototype._countPreAtariThreat = function (stone, enemies, empties, color, level, egroups) {
    var isSnapback = false, eg;
    var allies = stone.uniqueAllies(color);
    // now look for groups with 2 lives
    for (var egNdx = enemies.length - 1; egNdx >= 0; egNdx--) {
        eg = enemies[egNdx];
        if (eg.lives !== 2) continue;
        // no good if enemy can escape by killing one of our weak groups around
        if (this._gotLivesFromKillingAround(eg, 2)) continue; // >=2 because killing 1 stone is not enough to escape
        // same but for the group of our new stone; if should not become atari either
        if (empties.length === 0 && allies.length === 1 && allies[0].lives === 2) continue;

        if (empties.length === 1 && allies.length === 0) {
            // unless this is a snapback, this is a dumb move
            // it is a snapback if the last empty point (where the enemy will have to play)
            // would not make the enemy group connect to another enemy group
            // (equivalent to: the empty point has no other enemy group as neighbor)
            var enemiesAroundEmpty = empties[0].uniqueAllies(eg.color);
            if (enemiesAroundEmpty.length !== 1 || enemiesAroundEmpty[0] !== eg) {
                continue;
            }
            // here we know this is a snapback
            isSnapback = true;
            if (main.debug) main.log.debug('Hunter ' + Grid.colorName(color) + ' sees a snapback in ' + stone);
        }
        if (main.debug) main.log.debug('Hunter ' + Grid.colorName(color) + '(level ' + level + ') looking at threat ' + stone + ' on ' + eg);
        egroups.push(eg);
    }
    return isSnapback;
};

Hunter.prototype._countSimplePressure = function (enemies) {
    var threat = 0;
    for (var egNdx = enemies.length - 1; egNdx >= 0; egNdx--) {
        var egl = enemies[egNdx].lives;
        if (egl >= 2) threat += 1 / (egl + 1);
    }
    return threat * this.takeLifeCoeff;
};

Hunter.prototype._beforeEvalBoard = function () {
    this.snapbacks = [];
};

Hunter.prototype._evalMove = function (i, j, color, level) {
    level = level || 0;
    var stone = this.goban.stoneAt(i, j);
    var empties = stone.empties();
    var enemies = stone.uniqueEnemies(color);

    // count groups already in atari
    var threat1 = this._countAtariThreat(enemies, level);

    // count some profit in removing enemy lives
    if(level === 0) threat1 += this._countSimplePressure(enemies);

    // now look for groups with 2 lives
    var egroups = [];
    var snapback = this._countPreAtariThreat(stone, enemies, empties, color, level, egroups);
    if (level === 0 && snapback) {
        this.snapbacks.push(stone); // for other heuristics to look at...
    }
    // unless snapback, make sure our new stone's group can survive
    if (!snapback && empties.length <= 1) {
        var killScore = this._killScore(stone, color); //TODO: make this easier!
        if (killScore !== KO_KILL_SCORE &&
            (killScore < 0.02 || (killScore > 1 && killScore < 1.01))) {
            return 0; // REVIEW ME: return threat1 does not penalize snapback victim enough
        }
    }
    if (!egroups.length) return threat1;

    this.goban.tryAt(i, j, color); // our attack takes one of the 2 last lives (the one in i,j)

    // see attacks that fail
    var canEscape = [false, false, false];
    for (var g = egroups.length - 1; g >= 0; g--) {
        if (this._isAtariGroupCaught(egroups[g], level)) continue;
        if (egroups.length === 1) { egroups.pop(); break; }
        canEscape[g] = true;
    }

    this.goban.untry(); // important to undo before, so we compute threat right

    var threat2 = this._getMultipleChaseThreat(egroups, canEscape);

    if (main.debug && (threat1 || threat2)) main.log.debug('Hunter ' + Grid.colorName(color) +
        ' found a threat of ' + threat1 + ' + ' + threat2 + ' at ' + Grid.xy2move(i, j));
    return threat1 + threat2;
};

function basicSort(a, b) { return a - b; }

/** Returns the maximum threat we can hope for when several groups can be chased.
 *  Some of these chases might fail, but even so, the enemy can only defend one.
 *  Rule of thumb:
 *  - if 0 can escape => we capture the bigger one
 *  - if　1 or more can escape => we capture nothing if only 1, or the 2nd bigger if the 1st can escape
 */
Hunter.prototype._getMultipleChaseThreat = function (egroups, canEscape) {
    switch (egroups.length) {
    case 0: return 0;
    case 1: return canEscape[0] ? 0 : this.groupThreat(egroups[0]);
    case 2:
        if (!canEscape[0] && !canEscape[1]) return Math.max(this.groupThreat(egroups[0]), this.groupThreat(egroups[1]));
        if ( canEscape[0] &&  canEscape[1]) return Math.min(this.groupThreat(egroups[0]), this.groupThreat(egroups[1]));
        return canEscape[0] ? this.groupThreat(egroups[1]) : this.groupThreat(egroups[0]);
    case 3:
        var threats = [this.groupThreat(egroups[0]), this.groupThreat(egroups[1]), this.groupThreat(egroups[2])];
        if (!canEscape[0] && !canEscape[1] && !canEscape[2]) return Math.max(threats[0], threats[1], threats[2]);
        var sortedThreats = threats.concat().sort(basicSort);
        var bigger = threats.indexOf(sortedThreats[0]);
        if (!canEscape[bigger]) return threats[bigger];
        var secondBigger = threats.indexOf(sortedThreats[1]);
        return threats[secondBigger];
    default: throw new Error('Unexpected in Hunter#getMultipleChaseThreat');
    }
};

/** Evaluates if group g in atari (1 last escape move) can escape */
Hunter.prototype._isAtariGroupCaught = function (g, level) {
    var allLives = g.allLives();
    if (allLives.length !== 1) throw new Error('Unexpected: hunter #1: ' + allLives.length);

    var lastLife = allLives[0];
    var stone = this.goban.tryAt(lastLife.i, lastLife.j, g.color); // enemy's escape move
    var isCaught = this._escapingAtariThreat(stone, level) > 0;
    this.goban.untry();
    if (main.debug) main.log.debug('Hunter: group with last life ' + lastLife + ' would ' + (isCaught ? 'be caught: ' : 'escape: ') + g);
    return isCaught;
};

/** Returns true if played stone has put a nearby enemy group in atari */
Hunter.prototype._isStoneCreatingAtari = function (stone) {
    var enemyColor = 1 - stone.color;
    var neighbors = stone.neighbors;
    for (var n = neighbors.length - 1; n >= 0; n--) {
        if (neighbors[n].color !== enemyColor) continue;
        if (neighbors[n].group.lives === 1) {
            return true;
        }
    }
    return false;
};

/** @param stone is the enemy group's escape move (just been played)
 *  @param [level] - just to keep track for logging purposes
 *  @return the best score possible by chasing stone's group (could be the killing of a bystander though)
 */
Hunter.prototype._escapingAtariThreat = function (stone, level) {
    var g = stone.group;
    if (g.lives <= 1) return this.groupThreat(g); // caught
    if (g.lives > 2) {
        return 0; //TODO look better
    }
    // g.lives is 2

    // if escape move just put one of our groups in atari the chase fails
    if (this._isStoneCreatingAtari(stone)) return 0;

    // get 2 possible escape moves
    var empties = stone.empties();
    if (empties.length !== 2) {
        empties = g.allLives();
    }
    if (empties.length !== 2) throw new Error('Unexpected: hunter #2');
    var e1 = empties[0];
    var e2 = empties[1];
    if (main.debug) main.log.debug('Hunter: group has 2 lives left: ' + e1 + ' and ' + e2);

    // try blocking the 2 moves (recursive descent)
    var color = 1 - g.color;
    level = (level || 0) + 1;
    return Math.max(this._evalMove(e1.i, e1.j, color, level), this._evalMove(e2.i, e2.j, color, level));
};

/** @param stone is the enemy group's escape move (just been played)
 *  @return true if the group gets captured
 */
Hunter.prototype.isEscapingAtariCaught = function (stone) {
    return this._escapingAtariThreat(stone, 1);
};

Hunter.prototype.catchThreat = function (i, j, color) {
    return this._evalMove(i, j, color, 1);
};

/** Called by other heuristics to know if a stone is a snapback for current move */
Hunter.prototype.isSnapback = function (stone) {
    return this.snapbacks.indexOf(stone) !== -1;
};
<commit_msg>Hunter now cares about races<commit_after>//Translated from hunter.rb using babyruby2js
'use strict';

var main = require('../../main');

var Grid = require('../../Grid');
var Heuristic = require('./Heuristic');
var inherits = require('util').inherits;

var ALWAYS = main.ALWAYS;

/** @class Hunters find threats to struggling enemy groups.
 *  Ladder attack fits in here. */
function Hunter(player) {
    Heuristic.call(this, player);

    this.pressureCoeff = this.getGene('pressure', 1, 0.01, 2);

    this.snapbacks = null;
    this.noEasyPrisonerYx = player.getHeuristic('NoEasyPrisoner').scoreGrid.yx;
}
inherits(Hunter, Heuristic);
module.exports = Hunter;

/** Returns true if group g could get at least minLives by killing one of the
 *  enemy groups around in atari
 */
Hunter.prototype._gotLivesFromKillingAround = function (g, minLives) {
    var enemies = g.allEnemies();
    for (var n = enemies.length - 1; n >= 0; n--) {
        var eg = enemies[n];
        if (eg.lives > 1) continue;
        // found 1 enemy group in atari; killing it would give us its size in new lives
        var addedLives = eg.stones.length;
        if (addedLives >= minLives) return true;
        // this is not enough, so count lives we would get by connecting with our groups around eg
        var allies = eg.allEnemies();
        for (var a = allies.length - 1; a >= 0; a--) {
            addedLives += allies[a].lives - 1;
            if (addedLives >= minLives) return true;
        }
    }
    return false;
};

var KO_KILL_SCORE = 1;
var RACE_KILL_SCORE = 1.1; // just need to be enough to let the move happen (score comes from pressure eval)

/** Returns a score to measure what kind of kill a move is.
 *  Any number < 1.01 is sign of a "not so good" kill.
 *  E.g. in this board, black c5 has a bad "kill score" of 1.0001
 *  5 O@+OO
 *  4 O@O@+
 *  3 OO@@+
 *  2 ++@++
 *  1 ++@++
 *    abcde
 *  NB: kill score of a KO is 1 (KO_KILL_SCORE)
 */
Hunter.prototype._killScore = function (empty, color) {
    var numAllies = 0, numKill = 0, life = 0;
    for (var i = empty.neighbors.length - 1; i >= 0; i--) {
        var n = empty.neighbors[i];
        switch (n.color) {
        case main.EMPTY:
            life += 0.01;
            break;
        case color: // ally
            life += (n.group.lives - 1) * 0.01;
            if (n.group.isAlive === ALWAYS) life += 2;
            numAllies += 0.0001;
            break;
        default: // enemy
            if (n.group.inRaceWith) return RACE_KILL_SCORE;
            if (n.group.lives > 1) break; // not a kill
            numKill += n.group.stones.length;
        }
    }
    return numKill + life + numAllies;
};

Hunter.prototype._countAtariThreat = function (enemies, level) {
    var atariThreat = 0, eg;
    for (var egNdx = enemies.length - 1; egNdx >= 0; egNdx--) {
        eg = enemies[egNdx];
        if (eg.lives !== 1) continue;
        // if we can take eg anytime later, no need to take it now
        //TODO also verify no group in "enemies" is strong
        if (!level && this._isAtariGroupCaught(eg, level) && !this._gotLivesFromKillingAround(eg, 1)) {
            continue;
        }
        atariThreat += this.groupThreat(eg);
    }
    return atariThreat;
};

Hunter.prototype._countPreAtariThreat = function (stone, enemies, empties, color, level, egroups) {
    var isSnapback = false, eg;
    var allies = stone.uniqueAllies(color);
    // now look for groups with 2 lives
    for (var egNdx = enemies.length - 1; egNdx >= 0; egNdx--) {
        eg = enemies[egNdx];
        if (eg.lives !== 2) continue;
        // no good if enemy can escape by killing one of our weak groups around
        if (this._gotLivesFromKillingAround(eg, 2)) continue; // >=2 because killing 1 stone is not enough to escape
        // same but for the group of our new stone; if should not become atari either
        if (empties.length === 0 && allies.length === 1 && allies[0].lives === 2) continue;

        if (empties.length === 1 && allies.length === 0) {
            // unless this is a snapback, this is a dumb move
            // it is a snapback if the last empty point (where the enemy will have to play)
            // would not make the enemy group connect to another enemy group
            // (equivalent to: the empty point has no other enemy group as neighbor)
            var enemiesAroundEmpty = empties[0].uniqueAllies(eg.color);
            if (enemiesAroundEmpty.length !== 1 || enemiesAroundEmpty[0] !== eg) {
                continue;
            }
            // here we know this is a snapback
            isSnapback = true;
            if (main.debug) main.log.debug('Hunter ' + Grid.colorName(color) + ' sees a snapback in ' + stone);
        }
        if (main.debug) main.log.debug('Hunter ' + Grid.colorName(color) + '(level ' + level + ') looking at threat ' + stone + ' on ' + eg);
        egroups.push(eg);
    }
    return isSnapback;
};

Hunter.prototype._countPressureAndRace = function (stone, enemies, level, isEasyPrisoner) {
    var threat = 0, raceThreat = 0;
    for (var egNdx = enemies.length - 1; egNdx >= 0; egNdx--) {
        var enemy = enemies[egNdx];
        var egl = enemy.lives, allyInRace = enemy.inRaceWith;
        if (allyInRace && egl === allyInRace.lives && !stone.isNextTo(allyInRace)) {
            raceThreat += this.groupThreat(enemy) +
                this.groupThreat(allyInRace, /*saved=*/true);
            continue;
        }
        // Could be 2 or 3 below; see TestAi#testSemiAndEndGame h1 & b8 for examples
        if (egl >= 2 && level === 0 && !isEasyPrisoner) {
            threat += 1 / (egl + 1);
        }
    }
    return threat * this.pressureCoeff + raceThreat;
};

Hunter.prototype._beforeEvalBoard = function () {
    this.snapbacks = [];
};

Hunter.prototype._evalMove = function (i, j, color, level) {
    level = level || 0;
    var stone = this.goban.stoneAt(i, j);
    var empties = stone.empties();
    var enemies = stone.uniqueEnemies(color);

    // count groups already in atari
    var threat1 = this._countAtariThreat(enemies, level);

    // now look for groups with 2 lives
    var egroups = [];
    var isSnapback = this._countPreAtariThreat(stone, enemies, empties, color, level, egroups);
    if (level === 0 && isSnapback) {
        this.snapbacks.push(stone); // for other heuristics to look at...
    }
    // unless snapback, make sure our new stone's group can survive
    if (!isSnapback && empties.length <= 1) {
        var killScore = this._killScore(stone, color); //TODO: make this easier!
        if (killScore !== KO_KILL_SCORE &&
            (killScore < 0.02 || (killScore > 1 && killScore < 1.01))) {
            return 0; // REVIEW ME: we ignore threat1 to penalize more snapback creation
        }
    }
    // count some profit in removing enemy lives
    var isEasyPrisoner = !isSnapback && this.noEasyPrisonerYx[j][i] < 0;
    threat1 += this._countPressureAndRace(stone, enemies, level, isEasyPrisoner);

    if (!egroups.length) return threat1;

    this.goban.tryAt(i, j, color); // our attack takes one of the 2 last lives (the one in i,j)

    // see attacks that fail
    var canEscape = [false, false, false];
    for (var g = egroups.length - 1; g >= 0; g--) {
        if (this._isAtariGroupCaught(egroups[g], level)) continue;
        if (egroups.length === 1) { egroups.pop(); break; }
        canEscape[g] = true;
    }

    this.goban.untry(); // important to undo before, so we compute threat right

    var threat2 = this._getMultipleChaseThreat(egroups, canEscape);

    if (main.debug && (threat1 || threat2)) main.log.debug('Hunter ' + Grid.colorName(color) +
        ' found a threat of ' + threat1.toFixed(2) + ' + ' + threat2 + ' at ' + Grid.xy2move(i, j));
    return threat1 + threat2;
};

function basicSort(a, b) { return a - b; }

/** Returns the maximum threat we can hope for when several groups can be chased.
 *  Some of these chases might fail, but even so, the enemy can only defend one.
 *  Rule of thumb:
 *  - if 0 can escape => we capture the bigger one
 *  - if　1 or more can escape => we capture nothing if only 1, or the 2nd bigger if the 1st can escape
 */
Hunter.prototype._getMultipleChaseThreat = function (egroups, canEscape) {
    switch (egroups.length) {
    case 0: return 0;
    case 1: return canEscape[0] ? 0 : this.groupThreat(egroups[0]);
    case 2:
        if (!canEscape[0] && !canEscape[1]) return Math.max(this.groupThreat(egroups[0]), this.groupThreat(egroups[1]));
        if ( canEscape[0] &&  canEscape[1]) return Math.min(this.groupThreat(egroups[0]), this.groupThreat(egroups[1]));
        return canEscape[0] ? this.groupThreat(egroups[1]) : this.groupThreat(egroups[0]);
    case 3:
        var threats = [this.groupThreat(egroups[0]), this.groupThreat(egroups[1]), this.groupThreat(egroups[2])];
        if (!canEscape[0] && !canEscape[1] && !canEscape[2]) return Math.max(threats[0], threats[1], threats[2]);
        var sortedThreats = threats.concat().sort(basicSort);
        var bigger = threats.indexOf(sortedThreats[0]);
        if (!canEscape[bigger]) return threats[bigger];
        var secondBigger = threats.indexOf(sortedThreats[1]);
        return threats[secondBigger];
    default: throw new Error('Unexpected in Hunter#getMultipleChaseThreat');
    }
};

/** Evaluates if group g in atari (1 last escape move) can escape */
Hunter.prototype._isAtariGroupCaught = function (g, level) {
    var allLives = g.allLives();
    if (allLives.length !== 1) throw new Error('Unexpected: hunter #1: ' + allLives.length);

    var lastLife = allLives[0];
    var stone = this.goban.tryAt(lastLife.i, lastLife.j, g.color); // enemy's escape move
    var isCaught = this._escapingAtariThreat(stone, level) > 0;
    this.goban.untry();
    if (main.debug) main.log.debug('Hunter: group with last life ' + lastLife + ' would ' + (isCaught ? 'be caught: ' : 'escape: ') + g);
    return isCaught;
};

/** Returns true if played stone has put a nearby enemy group in atari */
Hunter.prototype._isStoneCreatingAtari = function (stone) {
    var enemyColor = 1 - stone.color;
    var neighbors = stone.neighbors;
    for (var n = neighbors.length - 1; n >= 0; n--) {
        if (neighbors[n].color !== enemyColor) continue;
        if (neighbors[n].group.lives === 1) {
            return true;
        }
    }
    return false;
};

/** @param stone is the enemy group's escape move (just been played)
 *  @param [level] - just to keep track for logging purposes
 *  @return the best score possible by chasing stone's group (could be the killing of a bystander though)
 */
Hunter.prototype._escapingAtariThreat = function (stone, level) {
    var g = stone.group;
    if (g.lives <= 1) return this.groupThreat(g); // caught
    if (g.lives > 2) {
        return 0; //TODO look better
    }
    // g.lives is 2

    // if escape move just put one of our groups in atari the chase fails
    if (this._isStoneCreatingAtari(stone)) return 0;

    // get 2 possible escape moves
    var empties = stone.empties();
    if (empties.length !== 2) {
        empties = g.allLives();
    }
    if (empties.length !== 2) throw new Error('Unexpected: hunter #2');
    var e1 = empties[0];
    var e2 = empties[1];
    if (main.debug) main.log.debug('Hunter: group has 2 lives left: ' + e1 + ' and ' + e2);

    // try blocking the 2 moves (recursive descent)
    var color = 1 - g.color;
    level = (level || 0) + 1;
    return Math.max(this._evalMove(e1.i, e1.j, color, level), this._evalMove(e2.i, e2.j, color, level));
};

/** @param stone is the enemy group's escape move (just been played)
 *  @return true if the group gets captured
 */
Hunter.prototype.isEscapingAtariCaught = function (stone) {
    return this._escapingAtariThreat(stone, 1);
};

Hunter.prototype.catchThreat = function (i, j, color) {
    return this._evalMove(i, j, color, 1);
};

/** Called by other heuristics to know if a stone is a snapback for current move */
Hunter.prototype.isSnapback = function (stone) {
    return this.snapbacks.indexOf(stone) !== -1;
};
<|endoftext|>


<commit_before>function Stream() {
  // Properties
  this.s_id = null;
  this.s_name = null;
  this.s_terms = new Array();
  this.s_logglytoken = null;

  // Getter Methods
  this.getId = function() { return s_id; };
  this.getName = function() { return s_name; };
  this.getTerms = function() { return s_terms; };
  this.getLogglyToken = function() { return s_logglytoken; };

  // Setter Methods
  this.setId = function(id) { s_id = id; };
  this.setName = function(name) { s_name = name; };
  this.setTerms = function(terms) { s_terms = terms; };
  this.setLogglyToken = function(logglytoken) { s_logglytoken = logglytoken; };

  // Other Methods
  this.addTerm = function(term) { s_terms.push(term); }
}

var streams = new Array();
var inAnimation = false;

function displayTerm(element, term) {
  var div = $("
").attr("id",term).attr("class","term");
  div.text(term + ' ');
  div.append('x');
  element.append(div);
  // Term deletion
  div.find('a').click(function() {
    var term_name = $(this).parent().attr('id');
    $.ajax({ url: '/streams/'+getCurrentStreamId()+'/terms/'+term_name, type: 'DELETE', timeout: 6000, success: function(data, textStatus, XMLHttpRequest) {
      if (XMLHttpRequest.status == '204') {
        // Deleted
        $('div#logs_container > div:not(.logs_hidden) > div.terms > div#'+term_name.replace(/(:|\.|\!)/g,'\\$1')).fadeOut('normal', function() {
          $(this).remove();
        });
        // Refresh streams
        getStreams();
      } else {
        // Something seriously went wrong
      }
    }});
  });
}

function addTerm(element, term) {
  $.ajax({ url: '/streams/'+getCurrentStreamId()+'/terms',  data: { term: term }, type: 'POST', timeout: 6000, success: function(data, textStatus, XMLHttpRequest) {
    if (XMLHttpRequest.status == '201') {
      // Created
      streams[getCurrentStreamId()].Terms.push(term);
      displayTerm(element, term);
      element.parent().find('input').val('');
      // Refresh streams
      getStreams();
    } else {
      // Something seriously went wrong
    }
  }});
}

function getCurrentStreamId() {
  return $('ul.tabs').find('.selected').parent().attr('id');
}

function switchToStream(stream) {
  var selected_tab = $('ul.tabs').find('.selected');
  selected_tab.removeClass('selected');
  stream.addClass('selected');
  $("div#logs_container").children("div:not(.logs_hidden)").addClass('logs_hidden');
  $("div#logs_container > div#"+stream.parent().attr('id')+"_logs").removeClass('logs_hidden');
}

function setupTabs() {
  // Tab switching
  $('ul.tabs > div#streams > li:last > a').click(function() {
    if ((!$(this).hasClass('selected')) && (inAnimation == false)) {
      var stream = $(this);
      switchToStream(stream);
    }
  });
  // Tab deletion
  $('ul.tabs > div#streams > li:last > div.stream_delete').click(function() {
    var list_element = $(this).parent();
    $.ajax({ url: '/streams/'+list_element.attr('id'), type: 'DELETE', timeout: 6000, success: function(data, textStatus, XMLHttpRequest) {
      // Deleted
      var stream = $('ul.tabs > li#all > a');
      switchToStream(stream);
      $('div#logs_container > div#'+list_element.attr('id')+'_logs').remove();
      list_element.remove();
      // Refresh streams
      getStreams();
    }});
  });
  // Term creation
  $('div#logs_container > div:last > div.term_adder > input').keyup(function(e) {
    var new_term_val = $(this).val();
    var terms_container = $('div#logs_container > div:not(.logs_hidden) > div.terms');
    if((e.keyCode == 13) && (new_term_val != '')) {
      addTerm(terms_container, new_term_val);
    }
  });
  $('div#logs_container > div:last > div.term_adder > a').click(function() {
    var new_term_val = $(this).parent().children('input').val();
    var terms_container = $('div#logs_container > div:not(.logs_hidden) > div.terms');
    addTerm(terms_container, new_term_val);
  });
}

function addStream(s) {
  if (s.ID == 'alerts') {
    var streamBlock = $("
").attr("id",s.ID).attr("class","alerts");
    var streamLink  = $("").attr("href","#");
    streamLink.text(s.Name);
    streamLink.append('(0)');
    streamBlock.append(streamLink);
    $('div#streams').append(streamBlock);
  } else {
    var streamBlock = $("
").attr("id",s.ID);
    var streamLink  = $("").attr("href","#");
    streamLink.text(s.Name);
    streamBlock.append(streamLink);
    streamBlock.append('
x
');
    $('div#streams').append(streamBlock);
  }
  $('div#streams li:last').fadeIn('normal');
  var div = $('
',{
    'id': s.ID + '_logs',
    'class': 'logs_hidden'
  }).append('

'+
    '

'+
    '
<input type="text" name="term_field"/>+
'+
    '

'+
    '

'+
    'Date	'+
    'Host	'+
    'Severity	'+
    'Facility	'+
    'Message	'+
    '

'+
    '

'+
    '
'+
    '
'+
    '

'+
    '
Stream Settings
'+
    'Loggly Token:'+
    '<input type="text" class="loggly_token" value="'+s.Forwarding.Token+'" size="45"/>'+
    '<input type="button" value="Save" class="save_settings"/>'+
    '

');

  $('div#logs_container').append(div);

  $("#stream_settings_"+s.ID+"[rel]").overlay({effect: 'apple'});
  var terms_container = div.find('div.terms');
  for (var t in s.Terms) {
    displayTerm(terms_container, s.Terms[t]);
  }

  $('div#stream_settings_form_'+s.ID+' > input.save_settings').click(function () {
    var stream_name = $('div#stream_settings_form_'+s.ID+' > input.loggly_token').val();
	$.ajax({ url: '/streams/'+s.ID+'/forwarding', type: 'POST', data: { token: stream_name }, timeout: 6000, success: function(data, textStatus, XMLHttpRequest) {
	  if (XMLHttpRequest.status == 200) {
        $("#stream_settings_"+s.ID+"[rel]").overlay().close();
      } else {
        // Something seriously went wrong
        $('div#stream_settings_form_'+s.ID+' > div.error').html('Whoops... something went wrong!').show();
      }
    }, error: function(XMLHttpRequest) {
      // Something seriously went wrong
      $('div#stream_settings_form_'+s.ID+' > div.error').html('Whoops... something went wrong!').show();
    }});
  });

  setupTabs();
}

function getStreams(pageLoad) {
  $.ajax({ url: '/streams', type: 'GET', timeout: 6000, success: function(data, textStatus, XMLHttpRequest) {
    // success
    for (var key in data) {
      var stream = {
        'ID': key,
        'Name': data[key].name,
        'Terms': data[key].terms,
        'Forwarding': {
          'Enabled': data[key].forwarding.enabled,
          'Token': data[key].forwarding.token
        }
      }
      streams[key] = stream;
      if (pageLoad) {
        addStream(stream);
      }
    }
  }});
}

$(window).load(function() {
  $("#add_tab[rel]").overlay({effect: 'apple'});
});

$(document).ready(function() {
  // Get existing stream
  getStreams(true);
  // Erase values & errors from modal dialog if closed
  $(document).bind('onClose', function() {
    $('div#new_stream_form > div.error').hide();
    $('input#stream_name').val('');
  });
  // Try to create a new stream when the create button is pressed
  $('input#create_stream').click(function() {
    $('div#new_stream_form > div.error').hide();
    var stream_name = $('input#stream_name').val();
    if (stream_name == '') {
      $('div#new_stream_form > div.error').html('Stream name must not be empty').show();
    } else {
      $.ajax({ url: '/streams', type: 'POST', data: { name: stream_name }, timeout: 6000, success: function(data, textStatus, XMLHttpRequest) {
        if (XMLHttpRequest.status == '201') {
          // Created
          var stream = {
            'ID': data,
            'Name': stream_name,
            'Terms': new Array(),
            'Forwarding': {
              'Enabled': false,
              'Token': ''
            }
          }
          streams[stream.ID] = stream;
          addStream(stream);
          $("#add_tab[rel]").overlay().close();
        } else {
          // Something seriously went wrong
          $('div#new_stream_form > div.error').html('Whoops... something went wrong!').show();
        }
      }, error: function(XMLHttpRequest) {
        if (XMLHttpRequest.status == '409') {
          // Duplicate / Conflict
          $('div#new_stream_form > div.error').html('That stream already exists!').show();
        } else {
          // Something seriously went wrong
          $('div#new_stream_form > div.error').html('Whoops... something went wrong!').show();
        }
      }});
    }
  });
  // Handle tab clicks for all / alerts
  $('ul.tabs > li > a').click(function() {
    if((!$(this).hasClass('selected')) && (inAnimation == false)) {
      var stream = $(this);
      if (stream.attr('id') != 'add_tab') {
        switchToStream(stream);
      }
    }
  });
});
<commit_msg>clear stream setting error messages after 3 seconds<commit_after>function Stream() {
  // Properties
  this.s_id = null;
  this.s_name = null;
  this.s_terms = new Array();
  this.s_logglytoken = null;

  // Getter Methods
  this.getId = function() { return s_id; };
  this.getName = function() { return s_name; };
  this.getTerms = function() { return s_terms; };
  this.getLogglyToken = function() { return s_logglytoken; };

  // Setter Methods
  this.setId = function(id) { s_id = id; };
  this.setName = function(name) { s_name = name; };
  this.setTerms = function(terms) { s_terms = terms; };
  this.setLogglyToken = function(logglytoken) { s_logglytoken = logglytoken; };

  // Other Methods
  this.addTerm = function(term) { s_terms.push(term); }
}

var streams = new Array();
var inAnimation = false;

function displayTerm(element, term) {
  var div = $("
").attr("id",term).attr("class","term");
  div.text(term + ' ');
  div.append('x');
  element.append(div);
  // Term deletion
  div.find('a').click(function() {
    var term_name = $(this).parent().attr('id');
    $.ajax({ url: '/streams/'+getCurrentStreamId()+'/terms/'+term_name, type: 'DELETE', timeout: 6000, success: function(data, textStatus, XMLHttpRequest) {
      if (XMLHttpRequest.status == '204') {
        // Deleted
        $('div#logs_container > div:not(.logs_hidden) > div.terms > div#'+term_name.replace(/(:|\.|\!)/g,'\\$1')).fadeOut('normal', function() {
          $(this).remove();
        });
        // Refresh streams
        getStreams();
      } else {
        // Something seriously went wrong
      }
    }});
  });
}

function addTerm(element, term) {
  $.ajax({ url: '/streams/'+getCurrentStreamId()+'/terms',  data: { term: term }, type: 'POST', timeout: 6000, success: function(data, textStatus, XMLHttpRequest) {
    if (XMLHttpRequest.status == '201') {
      // Created
      streams[getCurrentStreamId()].Terms.push(term);
      displayTerm(element, term);
      element.parent().find('input').val('');
      // Refresh streams
      getStreams();
    } else {
      // Something seriously went wrong
    }
  }});
}

function getCurrentStreamId() {
  return $('ul.tabs').find('.selected').parent().attr('id');
}

function switchToStream(stream) {
  var selected_tab = $('ul.tabs').find('.selected');
  selected_tab.removeClass('selected');
  stream.addClass('selected');
  $("div#logs_container").children("div:not(.logs_hidden)").addClass('logs_hidden');
  $("div#logs_container > div#"+stream.parent().attr('id')+"_logs").removeClass('logs_hidden');
}

function setupTabs() {
  // Tab switching
  $('ul.tabs > div#streams > li:last > a').click(function() {
    if ((!$(this).hasClass('selected')) && (inAnimation == false)) {
      var stream = $(this);
      switchToStream(stream);
    }
  });
  // Tab deletion
  $('ul.tabs > div#streams > li:last > div.stream_delete').click(function() {
    var list_element = $(this).parent();
    $.ajax({ url: '/streams/'+list_element.attr('id'), type: 'DELETE', timeout: 6000, success: function(data, textStatus, XMLHttpRequest) {
      // Deleted
      var stream = $('ul.tabs > li#all > a');
      switchToStream(stream);
      $('div#logs_container > div#'+list_element.attr('id')+'_logs').remove();
      list_element.remove();
      // Refresh streams
      getStreams();
    }});
  });
  // Term creation
  $('div#logs_container > div:last > div.term_adder > input').keyup(function(e) {
    var new_term_val = $(this).val();
    var terms_container = $('div#logs_container > div:not(.logs_hidden) > div.terms');
    if((e.keyCode == 13) && (new_term_val != '')) {
      addTerm(terms_container, new_term_val);
    }
  });
  $('div#logs_container > div:last > div.term_adder > a').click(function() {
    var new_term_val = $(this).parent().children('input').val();
    var terms_container = $('div#logs_container > div:not(.logs_hidden) > div.terms');
    addTerm(terms_container, new_term_val);
  });
}

function addStream(s) {
  if (s.ID == 'alerts') {
    var streamBlock = $("
").attr("id",s.ID).attr("class","alerts");
    var streamLink  = $("").attr("href","#");
    streamLink.text(s.Name);
    streamLink.append('(0)');
    streamBlock.append(streamLink);
    $('div#streams').append(streamBlock);
  } else {
    var streamBlock = $("
").attr("id",s.ID);
    var streamLink  = $("").attr("href","#");
    streamLink.text(s.Name);
    streamBlock.append(streamLink);
    streamBlock.append('
x
');
    $('div#streams').append(streamBlock);
  }
  $('div#streams li:last').fadeIn('normal');
  var div = $('
',{
    'id': s.ID + '_logs',
    'class': 'logs_hidden'
  }).append('

'+
    '

'+
    '
<input type="text" name="term_field"/>+
'+
    '

'+
    '

'+
    'Date	'+
    'Host	'+
    'Severity	'+
    'Facility	'+
    'Message	'+
    '

'+
    '

'+
    '
'+
    '
'+
    '

'+
    '
Stream Settings
'+
    'Loggly Token:'+
    '<input type="text" class="loggly_token" value="'+s.Forwarding.Token+'" size="45"/>'+
    '<input type="button" value="Save" class="save_settings"/>'+
    '

');

  $('div#logs_container').append(div);

  $("#stream_settings_"+s.ID+"[rel]").overlay({effect: 'apple'});
  var terms_container = div.find('div.terms');
  for (var t in s.Terms) {
    displayTerm(terms_container, s.Terms[t]);
  }

  $('div#stream_settings_form_'+s.ID+' > input.save_settings').click(function () {
    var stream_name = $('div#stream_settings_form_'+s.ID+' > input.loggly_token').val();
    $.ajax({ url: '/streams/'+s.ID+'/forwarding', type: 'POST', data: { token: stream_name }, timeout: 6000, success: function(data, textStatus, XMLHttpRequest) {
      if (XMLHttpRequest.status == 200) {
        $("#stream_settings_"+s.ID+"[rel]").overlay().close();
      } else {
        // Something seriously went wrong
        $('div#stream_settings_form_'+s.ID+' > div.error').html('Whoops... something went wrong!').show();
        setTimeout(function(){ $('div#stream_settings_form_'+s.ID+' > div.error').hide().html('');}, 3000);
      }
    }, error: function(XMLHttpRequest) {
      // Something seriously went wrong
      $('div#stream_settings_form_'+s.ID+' > div.error').html('Whoops... something went wrong!').show();
      setTimeout(function(){ $('div#stream_settings_form_'+s.ID+' > div.error').hide().html('');}, 3000);
    }});
  });

  setupTabs();
}

function getStreams(pageLoad) {
  $.ajax({ url: '/streams', type: 'GET', timeout: 6000, success: function(data, textStatus, XMLHttpRequest) {
    // success
    for (var key in data) {
      var stream = {
        'ID': key,
        'Name': data[key].name,
        'Terms': data[key].terms,
        'Forwarding': {
          'Enabled': data[key].forwarding.enabled,
          'Token': data[key].forwarding.token
        }
      }
      streams[key] = stream;
      if (pageLoad) {
        addStream(stream);
      }
    }
  }});
}

$(window).load(function() {
  $("#add_tab[rel]").overlay({effect: 'apple'});
});

$(document).ready(function() {
  // Get existing stream
  getStreams(true);
  // Erase values & errors from modal dialog if closed
  $(document).bind('onClose', function() {
    $('div#new_stream_form > div.error').hide();
    $('input#stream_name').val('');
  });
  // Try to create a new stream when the create button is pressed
  $('input#create_stream').click(function() {
    $('div#new_stream_form > div.error').hide();
    var stream_name = $('input#stream_name').val();
    if (stream_name == '') {
      $('div#new_stream_form > div.error').html('Stream name must not be empty').show();
    } else {
      $.ajax({ url: '/streams', type: 'POST', data: { name: stream_name }, timeout: 6000, success: function(data, textStatus, XMLHttpRequest) {
        if (XMLHttpRequest.status == '201') {
          // Created
          var stream = {
            'ID': data,
            'Name': stream_name,
            'Terms': new Array(),
            'Forwarding': {
              'Enabled': false,
              'Token': ''
            }
          }
          streams[stream.ID] = stream;
          addStream(stream);
          $("#add_tab[rel]").overlay().close();
        } else {
          // Something seriously went wrong
          $('div#new_stream_form > div.error').html('Whoops... something went wrong!').show();
        }
      }, error: function(XMLHttpRequest) {
        if (XMLHttpRequest.status == '409') {
          // Duplicate / Conflict
          $('div#new_stream_form > div.error').html('That stream already exists!').show();
        } else {
          // Something seriously went wrong
          $('div#new_stream_form > div.error').html('Whoops... something went wrong!').show();
        }
      }});
    }
  });
  // Handle tab clicks for all / alerts
  $('ul.tabs > li > a').click(function() {
    if((!$(this).hasClass('selected')) && (inAnimation == false)) {
      var stream = $(this);
      if (stream.attr('id') != 'add_tab') {
        switchToStream(stream);
      }
    }
  });
});
<|endoftext|>


<commit_before>/*
 * Developed by Radu Puspana
 * Version 1.0
 */

console.log("App has started.");
console.log("Loading controllers.");

// module that handles the budget data using the module pattern
// this controller keeps track of all the income, expenses, the budget itself, and
// also the percentage of how much an income/expense represents of the total budget
var budgetController = (function() {

    // private Expense constructor
    var prvExpense = function(id, description, value) {
        this.id = id;
        this.description = description;
        this.value = value;
        this.percentageOfExpenseOutOfTotalIncome = -1; // -1 = value can't be calculated yet
    };

    // calculate how much in % an expense represents out of the total income and store this value in
    // prvExpense.percentageOfExpenseOutOfTotalIncome
    // totalIncome  Number  the total of the submited income transactions
    prvExpense.prototype.calculatePercentageOfExpenseOutOfTotalIncome = function() {
        if (prvData.totals.inc > 0) {
            var percentageOfExpTotalInc = (this.value / prvData.totals.inc) * 100;

            // round up the second digit based on the third, and shrink the num to just two decimals
            this.percentageOfExpenseOutOfTotalIncome = rpJSframework.pblCutNdecimalsFromFloatNum(percentageOfExpTotalInc, 2);

        }
        else {
            // when we delete all of the expense transactions, we need to reset this variable
            this.percentageOfExpenseOutOfTotalIncome = -1;
        }
    }

    // getter for the prvExpense.percentageOfExpenseOutOfTotalIncome property
    prvExpense.prototype.getPercentageOfSubmitedExpense = function() {
        return this.percentageOfExpenseOutOfTotalIncome;
    }

    // private Income constructor
    var prvIncome = function(id, description, value) {
        this.id = id;
        this.description = description;
        this.value = value;
    };

    // object for storing instances of prvExpense, prvIncome, totalExpenses, totalIncomes
    // every expense entered in the UI will map to an instance of prvExpense
    // every income entered in the UI will map to an instance of prvIncome
    var prvData = {

        // object with all the expenses and all of the incomes
        allItems: {
            // all expense transactions
            exp: [],

            // all income transactions
            inc: []
        },

        // object with expenses total and income total
        totals: {
            // sum of all expense transactions
            exp: 0,

            // sum of all income transactions
            inc: 0
        },

        // total budget: incomes - expenses
        budget: 0,

        // how much of the total income, in %, the total expenses represent
        // -1 = there is no income submited
        expensesPercentageIncome: -1
    };

    // calculate total income or total expenses
    // type  String  type of transaction "exp" or "inc"
    var prvCalculateTotal = function(type) {
        var sum = 0;

        // select between the exp or inc array of the prvDate object to calculate the sum of all the values of each object in the array
        prvData.allItems[type].forEach(function(currElem) {
            sum = sum + currElem.value;
        });

        // add this sum to the prvData.totals[exp] or prvData.totals[inc]
        prvData.totals[type] = sum;
    }

    return {

        // pubic method for creating a new Expense or Income instance based on user input from the UI,
        // add it to the exp or inc array of prvData.allItems, and return the new instance created
        pblAddItem: function(type, descripton, value) {

            // new reference to a new intance of prvExpense or prvIncome
            var newItem;

             // create a unique id to put in each prvExpense or prvIncome item
            var id;

            id = prvData.allItems[type].length > 0 ? prvData.allItems[type][prvData.allItems[type].length - 1].id + 1 : 0;

            // create new instance of prvExpense or prvIncome based on the type parameter
            if(type === "exp") {
                newItem = new prvExpense(id, descripton, value);
            } else if (type === "inc") {
                newItem = new prvIncome(id, descripton, value);
            }

            // object[property]
            // add expense/income to the prvData.allItems[exp] or prvData.allItems[inc] array
            // based on the type parameter
            prvData.allItems[type].push(newItem);

            // return new instance of prvExpense or prvIncome
            return newItem;
        },

        // delete a transaction from the prvData.allItems.exp or prvData.allItems.exp arrays
        // transType  string  the trasaction type "exp" or "inc" in order to extract the element from the appropiate array
        // transId    string  the trasaction id to be extracted from prvData.allItems.exp or prvData.allItems.inc
        pblDeleteItem: function(transType, transId) {

            console.info("transType %s", transType);
            console.info("transId %d", transId);

            var trasactionTypeIds, indexDeleteElem;

            // id = 6
            // [1 2 4 6 8]
            // delete the item at index at which 6 sits

            // create an array with all the ids of the trasactions submited of type transType
            trasactionTypeIds = prvData.allItems[transType].map(function(currentElem) {
                return currentElem.id;
            });
            console.info("trasactionTypeIds %O", trasactionTypeIds);

            // get the index of the element with id transId from the trasactionTypeIds array
            indexDeleteElem = trasactionTypeIds.indexOf(transId);
            console.info("indexDeleteElem %O", indexDeleteElem);

            // if the id that was passed to the function is found in the transaction ids
            if (indexDeleteElem !== -1) {

                // detele the item at index idexDeleteElem from the prvData.allItems[transType] array
                prvData.allItems[transType].splice(indexDeleteElem, 1);
            }
            console.info(" prvData.allItems[%s] %O", transType, prvData.allItems[transType]);

        },

        // to delete
        pblTestGetDataStr: function() {
            console.info("prvData = %O", prvData);
        },

        // calculate the budget after submiting a transaction
        // calculate the % shown in the expenses red row under the available budget. aka the percentage of the budget that we already spent
        pblCalculateBudget: function() {

            // calculate total income
            prvCalculateTotal("inc");

            // calculate total expenses
            prvCalculateTotal("exp");

            // calculate the budget: income - expenses
            prvData.budget = prvData.totals.inc - prvData.totals.exp;

            // if at least one income transaction was submited
            if (prvData.totals.inc > 0){

                // 1. calculate the percent of how much the total expenses represent out of the total income.
                // 2. round up the second digit based on the third, and shrink the num to just two decimals
                var percentTotalExpOutOfTotalInc =
                    rpJSframework.pblCutNdecimalsFromFloatNum(((prvData.totals.exp * 100) / prvData.totals.inc), 2);

                prvData.expensesPercentageIncome = percentTotalExpOutOfTotalInc;
            }
            else {
                prvData.expensesPercentageIncome =  -1;
            }
        },

        // calculate percentage for each expense object that is stored in the prvData.allItems.exp array
        pblSetPercentageOfExpenseOutOfTotalIncomeForEachExp: function() {
            prvData.allItems.exp.forEach(function(currentElem) {
                currentElem.calculatePercentageOfExpenseOutOfTotalIncome();
            })
        },

        // create an array with each element being the percentage of a submited expense out of total income
        pblGetPercentageOfExpenseOutOfTotalIncomeForEachExp: function() {
            var arrayPercentageOfExpenseOutOfTotalIncome;

            arrayPercentageOfExpenseOutOfTotalIncome = prvData.allItems.exp.map(function(currentElement) {
                return currentElement.getPercentageOfSubmitedExpense();
            });

            return arrayPercentageOfExpenseOutOfTotalIncome;
        },

        // return the budget, total of expenses, of income, and expense percentage
        // it's not a good idea to return the prvData object to be altered by another module
        pblGetBudgetExpIncExpPercentage: function() {
            return {
                budget: prvData.budget,
                totalExpenses : prvData.totals.exp,
                totalIncome : prvData.totals.inc,
                expensesPercentageIncome : prvData.expensesPercentageIncome
            };
        },
    }
})();

console.log("Budget controller has finished loading.");
<commit_msg>Edit budget controller name at final log message:console.log("budgetController.js has finished loading.");<commit_after>/*
 * Developed by Radu Puspana
 * Version 1.0
 */

console.log("App has started.");
console.log("Loading controllers.");

// module that handles the budget data using the module pattern
// this controller keeps track of all the income, expenses, the budget itself, and
// also the percentage of how much an income/expense represents of the total budget
var budgetController = (function() {

    // private Expense constructor
    var prvExpense = function(id, description, value) {
        this.id = id;
        this.description = description;
        this.value = value;
        this.percentageOfExpenseOutOfTotalIncome = -1; // -1 = value can't be calculated yet
    };

    // calculate how much in % an expense represents out of the total income and store this value in
    // prvExpense.percentageOfExpenseOutOfTotalIncome
    // totalIncome  Number  the total of the submited income transactions
    prvExpense.prototype.calculatePercentageOfExpenseOutOfTotalIncome = function() {
        if (prvData.totals.inc > 0) {
            var percentageOfExpTotalInc = (this.value / prvData.totals.inc) * 100;

            // round up the second digit based on the third, and shrink the num to just two decimals
            this.percentageOfExpenseOutOfTotalIncome = rpJSframework.pblCutNdecimalsFromFloatNum(percentageOfExpTotalInc, 2);

        }
        else {
            // when we delete all of the expense transactions, we need to reset this variable
            this.percentageOfExpenseOutOfTotalIncome = -1;
        }
    }

    // getter for the prvExpense.percentageOfExpenseOutOfTotalIncome property
    prvExpense.prototype.getPercentageOfSubmitedExpense = function() {
        return this.percentageOfExpenseOutOfTotalIncome;
    }

    // private Income constructor
    var prvIncome = function(id, description, value) {
        this.id = id;
        this.description = description;
        this.value = value;
    };

    // object for storing instances of prvExpense, prvIncome, totalExpenses, totalIncomes
    // every expense entered in the UI will map to an instance of prvExpense
    // every income entered in the UI will map to an instance of prvIncome
    var prvData = {

        // object with all the expenses and all of the incomes
        allItems: {
            // all expense transactions
            exp: [],

            // all income transactions
            inc: []
        },

        // object with expenses total and income total
        totals: {
            // sum of all expense transactions
            exp: 0,

            // sum of all income transactions
            inc: 0
        },

        // total budget: incomes - expenses
        budget: 0,

        // how much of the total income, in %, the total expenses represent
        // -1 = there is no income submited
        expensesPercentageIncome: -1
    };

    // calculate total income or total expenses
    // type  String  type of transaction "exp" or "inc"
    var prvCalculateTotal = function(type) {
        var sum = 0;

        // select between the exp or inc array of the prvDate object to calculate the sum of all the values of each object in the array
        prvData.allItems[type].forEach(function(currElem) {
            sum = sum + currElem.value;
        });

        // add this sum to the prvData.totals[exp] or prvData.totals[inc]
        prvData.totals[type] = sum;
    }

    return {

        // pubic method for creating a new Expense or Income instance based on user input from the UI,
        // add it to the exp or inc array of prvData.allItems, and return the new instance created
        pblAddItem: function(type, descripton, value) {

            // new reference to a new intance of prvExpense or prvIncome
            var newItem;

             // create a unique id to put in each prvExpense or prvIncome item
            var id;

            id = prvData.allItems[type].length > 0 ? prvData.allItems[type][prvData.allItems[type].length - 1].id + 1 : 0;

            // create new instance of prvExpense or prvIncome based on the type parameter
            if(type === "exp") {
                newItem = new prvExpense(id, descripton, value);
            } else if (type === "inc") {
                newItem = new prvIncome(id, descripton, value);
            }

            // object[property]
            // add expense/income to the prvData.allItems[exp] or prvData.allItems[inc] array
            // based on the type parameter
            prvData.allItems[type].push(newItem);

            // return new instance of prvExpense or prvIncome
            return newItem;
        },

        // delete a transaction from the prvData.allItems.exp or prvData.allItems.exp arrays
        // transType  string  the trasaction type "exp" or "inc" in order to extract the element from the appropiate array
        // transId    string  the trasaction id to be extracted from prvData.allItems.exp or prvData.allItems.inc
        pblDeleteItem: function(transType, transId) {

            console.info("transType %s", transType);
            console.info("transId %d", transId);

            var trasactionTypeIds, indexDeleteElem;

            // id = 6
            // [1 2 4 6 8]
            // delete the item at index at which 6 sits

            // create an array with all the ids of the trasactions submited of type transType
            trasactionTypeIds = prvData.allItems[transType].map(function(currentElem) {
                return currentElem.id;
            });
            console.info("trasactionTypeIds %O", trasactionTypeIds);

            // get the index of the element with id transId from the trasactionTypeIds array
            indexDeleteElem = trasactionTypeIds.indexOf(transId);
            console.info("indexDeleteElem %O", indexDeleteElem);

            // if the id that was passed to the function is found in the transaction ids
            if (indexDeleteElem !== -1) {

                // detele the item at index idexDeleteElem from the prvData.allItems[transType] array
                prvData.allItems[transType].splice(indexDeleteElem, 1);
            }
            console.info(" prvData.allItems[%s] %O", transType, prvData.allItems[transType]);

        },

        // to delete
        pblTestGetDataStr: function() {
            console.info("prvData = %O", prvData);
        },

        // calculate the budget after submiting a transaction
        // calculate the % shown in the expenses red row under the available budget. aka the percentage of the budget that we already spent
        pblCalculateBudget: function() {

            // calculate total income
            prvCalculateTotal("inc");

            // calculate total expenses
            prvCalculateTotal("exp");

            // calculate the budget: income - expenses
            prvData.budget = prvData.totals.inc - prvData.totals.exp;

            // if at least one income transaction was submited
            if (prvData.totals.inc > 0){

                // 1. calculate the percent of how much the total expenses represent out of the total income.
                // 2. round up the second digit based on the third, and shrink the num to just two decimals
                var percentTotalExpOutOfTotalInc =
                    rpJSframework.pblCutNdecimalsFromFloatNum(((prvData.totals.exp * 100) / prvData.totals.inc), 2);

                prvData.expensesPercentageIncome = percentTotalExpOutOfTotalInc;
            }
            else {
                prvData.expensesPercentageIncome =  -1;
            }
        },

        // calculate percentage for each expense object that is stored in the prvData.allItems.exp array
        pblSetPercentageOfExpenseOutOfTotalIncomeForEachExp: function() {
            prvData.allItems.exp.forEach(function(currentElem) {
                currentElem.calculatePercentageOfExpenseOutOfTotalIncome();
            })
        },

        // create an array with each element being the percentage of a submited expense out of total income
        pblGetPercentageOfExpenseOutOfTotalIncomeForEachExp: function() {
            var arrayPercentageOfExpenseOutOfTotalIncome;

            arrayPercentageOfExpenseOutOfTotalIncome = prvData.allItems.exp.map(function(currentElement) {
                return currentElement.getPercentageOfSubmitedExpense();
            });

            return arrayPercentageOfExpenseOutOfTotalIncome;
        },

        // return the budget, total of expenses, of income, and expense percentage
        // it's not a good idea to return the prvData object to be altered by another module
        pblGetBudgetExpIncExpPercentage: function() {
            return {
                budget: prvData.budget,
                totalExpenses : prvData.totals.exp,
                totalIncome : prvData.totals.inc,
                expensesPercentageIncome : prvData.expensesPercentageIncome
            };
        },
    }
})();

console.log("budgetController.js has finished loading.");
<|endoftext|>


<commit_before>import React from 'react'
import { Header } from 'components/Header/Header'
import { IndexLink, Link } from 'react-router'
import { shallow } from 'enzyme'

describe('(Component) Header', () => {
  let _wrapper

  beforeEach(() => {
    _wrapper = shallow(<Header/>)
  })

  it('Renders a welcome message', () => {
    const welcome = _wrapper.find('h1')
    expect(welcome).to.exist
    expect(welcome.text()).to.match(/React Redux Starter Kit/)
  })

  describe('Navigation links...', () => {

    it('Should render an IndexLink to Home route', () => {
      expect(_wrapper.contains(<IndexLink to='/'/>)).to.equal.true
    })

    it('Should render an Link to Counter route)', () => {
      expect(_wrapper.contains(<Link to='/counter'/>)).to.equal.true
    })

  })
})
<commit_msg>fix(Header): fix always-passing test for links<commit_after>import React from 'react'
import { Header } from 'components/Header/Header'
import classes from 'components/Header/Header.scss'
import { IndexLink, Link } from 'react-router'
import { shallow } from 'enzyme'

describe('(Component) Header', () => {
  let _wrapper

  beforeEach(() => {
    _wrapper = shallow(<Header/>)
  })

  it('Renders a welcome message', () => {
    const welcome = _wrapper.find('h1')
    expect(welcome).to.exist
    expect(welcome.text()).to.match(/React Redux Starter Kit/)
  })

  describe('Navigation links...', () => {

    it('Should render a Link to Home route', () => {
      expect(_wrapper.contains(
        <IndexLink activeClassName={classes.activeRoute} to='/'>
          Home
        </Link>
      )).to.be.true
    })

    it('Should render a Link to Counter route', () => {
      expect(_wrapper.contains(
        <Link activeClassName={classes.activeRoute} to='/counter'>
          Counter
        </Link>
      )).to.be.true
    })
  })
})
<|endoftext|>


<commit_before>var Reaction = require("../reaction"),
    util = require("../util"),
    rules = require("../rules"),
    _ = require("underscore"),
    trim = util.trim,
    interpolatorRegex = /#\{([a-zA-Z0-9_\- ]+)(?:\?([a-zA-Z0-9_\- ]+))?(?:\:([a-zA-Z0-9_\- ]*))?}/g;

var AttributeReaction = Reaction.extend({

  init: function(next) {
    this.updateAttributes(this.attributes);
    next();
  },

  updateAttributes: function(attributes) {
    var that = this;
    _(attributes).each(function(value, key) {
      that.el.attr(key, value.replace(interpolatorRegex, function(match, key, valueIfTrue, valueIfFalse) {
        var value = that.get(key = trim(key));

        valueIfTrue = trim(valueIfTrue) || value;
        valueIfFalse = (typeof valueIfFalse == "undefined") ? "": trim(valueIfFalse);

        if(valueIfTrue === true) {
          valueIfTrue = key;
        }

        return value ? valueIfTrue : valueIfFalse;
      }));
    });
  },
  /**
   * TODO, ensure we never set blank src attributes
   **/
  observe: function() {
    var attrString = _.values(this.attributes),
        splitAttrString = attrString.join(" ")
                                    .split(/(#{[^}]*})/),
        interpolatedProperties = _.chain(splitAttrString)
                                  .select(function(str){ return str.match(/#{/) })
                                  .map(function(str){ return str.substr(2, str.indexOf("?") != -1 && str.indexOf("?") || str.length - 3) });
    listenHash = interpolatedProperties.value()
    if (_.isArray(listenHash)) {
      listenHash = listenHash.join(" ")
    }
    this.change(listenHash, function() {
      this.updateAttributes(this.attributes);
    }, this);
  }

}, {
  selector: "*",

  reactIf: function(el) {
    return _(el[0].attributes || []).any(function(attribute) {
      return attribute.value.match(/#\{/);
    }, this);
  },

  parse: function(el) {
    var attributes = {};

    _(el[0].attributes || []).each(function(attribute) {
      if(attribute.value.match(/#\{/)) {
        attributes[attribute.name] = attribute.value;
      }
    }, this);
    attributes.src = el.attr("data-src") || attributes.src;

    if(!attributes.src) {
      delete attributes.src;
    }
    return { attributes: attributes };
  }
});

module.exports = AttributeReaction;
<commit_msg>Optimize code that parses a string that represents an elements attribute to the properties on the model that are in that string<commit_after>var Reaction = require("../reaction"),
    util = require("../util"),
    rules = require("../rules"),
    _ = require("underscore"),
    trim = util.trim,
    interpolatorRegex = /#\{([a-zA-Z0-9_\- ]+)(?:\?([a-zA-Z0-9_\- ]+))?(?:\:([a-zA-Z0-9_\- ]*))?}/g;

var AttributeReaction = Reaction.extend({

  init: function(next) {
    this.updateAttributes(this.attributes);
    next();
  },

  updateAttributes: function(attributes) {
    var that = this;
    _(attributes).each(function(value, key) {
      that.el.attr(key, value.replace(interpolatorRegex, function(match, key, valueIfTrue, valueIfFalse) {
        var value = that.get(key = trim(key));

        valueIfTrue = trim(valueIfTrue) || value;
        valueIfFalse = (typeof valueIfFalse == "undefined") ? "": trim(valueIfFalse);

        if(valueIfTrue === true) {
          valueIfTrue = key;
        }

        return value ? valueIfTrue : valueIfFalse;
      }));
    });
  },
  /**
   * TODO, ensure we never set blank src attributes
   **/
  observe: function() {
    var attrString = _.values(this.attributes),
        splitAttrString = attrString.join(" "),
        listenHash = " ";
        splitAttrString.replace(/(?:#{)([^}?]*)/g, function(p1, match){ listenHash = listenHash + " " + match; return ""})
    this.change(listenHash, function() {
      this.updateAttributes(this.attributes);
    }, this);
  }

}, {
  selector: "*",

  reactIf: function(el) {
    return _(el[0].attributes || []).any(function(attribute) {
      return attribute.value.match(/#\{/);
    }, this);
  },

  parse: function(el) {
    var attributes = {};

    _(el[0].attributes || []).each(function(attribute) {
      if(attribute.value.match(/#\{/)) {
        attributes[attribute.name] = attribute.value;
      }
    }, this);
    attributes.src = el.attr("data-src") || attributes.src;

    if(!attributes.src) {
      delete attributes.src;
    }
    return { attributes: attributes };
  }
});

module.exports = AttributeReaction;
<|endoftext|>


<commit_before>// ==UserScript==
// @name        Duolingo Course Switcher (New Site)
// @description Simplifies switching between courses that use different interface language (i.e., base language, the language from which you learn).
// @namespace   https://github.com/zeta12ti/DuolingoCourseSwitcher
// @updateURL   https://github.com/zeta12ti/DuolingoCourseSwitcher/raw/master/DuolingoCourseSwitcher.user.js
// @include     https://*.duolingo.com/*
// @grant       none
// @version     1.1.0
// @author      zeta12ti
// ==/UserScript==

/*
 * NOTE: Annoyingly, duo.l10n.declared may change with time, which shifts the index for each phrase
 * If the header on the course menu is wrong, that's probably what happened.
*/
console.assert(window.duo.version === 'a74f7a1' || window.duo.version === '4fd6e1b')

// Language names (in english) - for use with duo.l10n.undeclared
var languages = {'ar': 'Arabic', 'bn': 'Bengali', 'ca': 'Catalan', 'cs': 'Czech', 'cy': 'Welsh', 'da': 'Danish', 'de': 'German', 'el': 'Greek', 'en': 'English', 'eo': 'Esperanto', 'es': 'Spanish', 'fr': 'French', 'ga': 'Irish', 'gn': 'Guarani (Jopar\u00e1)', 'he': 'Hebrew', 'hi': 'Hindi', 'hu': 'Hungarian', 'id': 'Indonesian', 'it': 'Italian', 'ja': 'Japanese', 'ko': 'Korean', 'nl-NL': 'Dutch', 'no-BO': 'Norwegian (Bokm\u00e5l)', 'pa': 'Punjabi (Gurmukhi)', 'pl': 'Polish', 'pt': 'Portuguese', 'ro': 'Romanian', 'ru': 'Russian', 'sv': 'Swedish', 'sw': 'Swahili', 'ta': 'Tamil', 'te': 'Telugu', 'th': 'Thai', 'tl': 'Tagalog', 'tlh': 'Klingon', 'tr': 'Turkish', 'uk': 'Ukrainian', 'vi': 'Vietnamese', 'zh-CN': 'Chinese'}

async function getLanguageString (languageCode) {
  return window.duo.l10n.undeclared[languages[languageCode] + '||capitalized'] || window.duo.l10n.undeclared[languages[languageCode]] || languages[languageCode] || 'Unknown'
}

// language flags -
// keys: remove flag- prefix and keep consistency - zs -> zh-CN, kl -> tlh, (zs -> zh-¿?), values: remove _3viv6
var flags = {'ar': '_1ARRD', 'bn': '_2TXAL', 'ca': 'mc4rg', 'cs': '_1uPQW', 'cy': '_1jO8h', 'da': '_1h0xh', 'de': 'oboa9', 'dk': '_3AA1F', 'el': '_2tQo9', 'en': '_2cR-E', 'eo': 'pWj0w', 'es': 'u5W-o', 'fr': '_2KQN3', 'ga': '_1vhNM', 'gn': '_24xu4', 'he': '_PDrK', 'hi': 'OgUIe', 'hu': '_1S3hi', 'id': '_107sn', 'it': '_1PruQ', 'ja': '_2N-Uj', 'ko': '_2lkzc', 'nl-NL': '_1fajz', 'no-BO': '_200jU', 'pl': '_3uusw', 'pt': 'pmGwL', 'ro': '_12U6e', 'ru': '_1eqxJ', 'sn': 'q_PD-', 'sv': '_2DMfV', 'sw': '_3T1km', 'th': '_2oTcA', 'tl': '_1q_MQ', 'tlh': '_6mRM', 'tr': '_1tJJ2', 'uk': '_1zZsN', 'un': 't-XH-', 'vi': '_1KtzC', 'zh': 'xi6jQ', 'zh-CN': '_2gNgd', '_circle-flag': '_2XSZu', '_flag': '_3viv6', 'medium-circle-flag': '_1ct7y _2XSZu', 'micro-circle-flag': '_3i5IF _2XSZu', 'small-circle-flag': '_3PU7E _2XSZu'}
// Use this to identify a row in the original list.
var inverseFlags = {'OgUIe': 'hi', '_107sn': 'id', '_12U6e': 'ro', '_1ARRD': 'ar', '_1KtzC': 'vi', '_1PruQ': 'it', '_1S3hi': 'hu', '_1ct7y _2XSZu': 'medium-circle-flag', '_1eqxJ': 'ru', '_1fajz': 'nl-NL', '_1h0xh': 'da', '_1jO8h': 'cy', '_1q_MQ': 'tl', '_1tJJ2': 'tr', '_1uPQW': 'cs', '_1vhNM': 'ga', '_1zZsN': 'uk', '_200jU': 'no-BO', '_24xu4': 'gn', '_2DMfV': 'sv', '_2KQN3': 'fr', '_2N-Uj': 'ja', '_2TXAL': 'bn', '_2XSZu': '_circle-flag', '_2cR-E': 'en', '_2gNgd': 'zh-CN', '_2lkzc': 'ko', '_2oTcA': 'th', '_2tQo9': 'el', '_3AA1F': 'dk', '_3PU7E _2XSZu': 'small-circle-flag', '_3T1km': 'sw', '_3i5IF _2XSZu': 'micro-circle-flag', '_3uusw': 'pl', '_3viv6': '_flag', '_6mRM': 'tlh', '_PDrK': 'he', 'mc4rg': 'ca', 'oboa9': 'de', 'pWj0w': 'eo', 'pmGwL': 'pt', 'q_PD-': 'sn', 't-XH-': 'un', 'u5W-o': 'es', 'xi6jQ': 'zh'}

async function getLanguageFlag (languageCode) {
    // The 'un' flag is a question mark.
  return flags[languageCode] || flags.un
}

async function getLanguageFromFlag (flagCode) {
  return inverseFlags[flagCode]
}

async function computeMenuOffset (languageSubmenu) {
  var header = languageSubmenu.querySelector('._2PurW')

  var offset = 0
  offset += parseFloat(window.getComputedStyle(languageSubmenu).getPropertyValue('padding-top'))

  offset += parseFloat(window.getComputedStyle(header).getPropertyValue('padding-top'))

  offset += parseFloat(window.getComputedStyle(header).getPropertyValue('padding-bottom'))

  offset += parseFloat(window.getComputedStyle(header).getPropertyValue('line-height'))

  return offset
}

async function setMenuPosition () {
  var verticalOffset = await computeMenuOffset(this)
  var verticalBase = this.parentNode.getBoundingClientRect().top - document.querySelector('._20LC5').getBoundingClientRect().top
  //  var horizontalBase = this.parentNode.getBoundingClientRect().left - document.querySelector('._20LC5').getBoundingClientRect().left
  this.style.top = (verticalBase - verticalOffset) + 'px'
}

async function addRule = function (stylesheet, rule) {
  stylesheet.insertRule(rule, stylesheet.cssRules.length)
}

async function addFromLanguageItem (fromLanguage, currentCourse) {
  var fromLanguageItem = document.createElement('li')

  var className = '_2kNgI _1qBnH from-course'
  if (currentCourse) {
    className = '_1oVFS ' + className
  }
  fromLanguageItem.className = className

  var container = document.createElement('li')
  container.className = '_2kNgI flag-container'
  fromLanguageItem.appendChild(container)

  var flag = document.createElement('span')
  className = await getLanguageFlag(fromLanguage) + ' _3viv6 _3vx2Z _1ct7y _2XSZu from-flag'
  flag.className = className
  container.appendChild(flag)

  var localizedLanguageName = await getLanguageString(fromLanguage)
  container.appendChild(document.createTextNode(localizedLanguageName))

  document.querySelector('._1XE6M').appendChild(fromLanguageItem)

  return fromLanguageItem
}

async function createInnerMenu (oldHeader) {
  var innerMenu = document.createElement('ul')
  innerMenu.className = 'OSaWc _1ZY-H language-submenu'

  var innerMenuHeader = document.createElement('li')
  innerMenuHeader.className = '_2PurW'
  innerMenuHeader.innerHTML = oldHeader
  innerMenu.appendChild(innerMenuHeader)

  var innerMenuCourseList = document.createElement('ul')
  innerMenuCourseList.className = '_1XE6M'
  innerMenu.appendChild(innerMenuCourseList)

  return innerMenu
}

async function addStyleSheet () {
  var cssStyle = document.createElement('style')
  document.querySelector('html > head').appendChild(cssStyle)
  var stylesheet = cssStyle.sheet

  addRule(stylesheet, 'li._2kNgI._1qBnH {display: none}')
  addRule(stylesheet, 'ul._1XE6M > li.qsrrc {display: none}')
  addRule(stylesheet, '.from-course:hover .language-submenu {display: block}')
  addRule(stylesheet, 'li._2kNgI._1qBnH.from-course {display: block}')
  addRule(stylesheet, 'li._2kNgI._1qBnH.learning-course {display: block}')
  addRule(stylesheet, '.flag-container {display: block}')
  addRule(stylesheet, '.flag-container {width: 100%}')

  addRule(stylesheet, '.language-submenu ._1XE6M .learning-course ._1fA14 {color: #999}')
  addRule(stylesheet, '.language-submenu ._1XE6M .learning-course:hover ._1fA14 {color: #fff}')

  addRule(stylesheet, '.from-course {position: static}')
  addRule(stylesheet, '._1XE6M {position: static}')
  addRule(stylesheet, '.language-submenu {position: absolute}')
  addRule(stylesheet, '.language-submenu {z-index: 10}')
  addRule(stylesheet, '.flag-container {position: relative}')
  addRule(stylesheet, '.from-course {padding: 0px 0px 0px 0px}')
  addRule(stylesheet, '.flag-container {padding: 3px 20px 3px 47px}')
  addRule(stylesheet, 'html[dir="ltr"] .language-submenu {left:90%}')
  addRule(stylesheet, 'html[dir="rtl"] .language-submenu {right:90%}')
}

async function copyChanges (modifications, menuItem) {
  modifications.forEach(mod => {
    if (mod.target.parentNode.classList && mod.target.parentNode.classList.contains('old-level-indicator')) {
      menuItem.querySelector('.level-indicator').innerText = mod.target.parentNode.innerText
    }
    if (mod.target.classList && mod.target.classList.contains('_2kNgI')) {
      menuItem.className = mod.target.className + ' learning-course'
    }
  })
}

async function reorganizeMenu () {
  var uiLanguage = window.duo.uiLanguage
  var baseLanguages = {}
  var courses = document.querySelectorAll('li._2kNgI._1qBnH:not(.from-course)')

  addStyleSheet()

  var oldHeader = document.querySelector('._2PurW').innerHTML
  document.querySelector('._2PurW').innerHTML = '
' + window.duo.l10n.declared[144] + '
' // Languages

  for (var i = 0, len = courses.length; i < len; i++) {
    let menuItem = courses[i].cloneNode(true)
    courses[i].classList.add('old-menu-item')
    courses[i].querySelector('._1fA14').classList.add('old-level-indicator')

    menuItem.addEventListener('click', courses[i].click.bind(courses[i]))
    menuItem.querySelector('._1fA14').classList.add('level-indicator')
    let observer = new window.MutationObserver((mods) => { copyChanges(mods, menuItem) })
    var config = {attributes: true, characterData: true, subtree: true}
    observer.observe(courses[i], config)

    var flags = menuItem.querySelectorAll('._3viv6')
    var learningLanguage, fromLanguage
    if (typeof menuItem.dataset.from === 'undefined' && typeof menuItem.dataset.learning === 'undefined') {
      if (flags.length === 2) {
        learningLanguage = await getLanguageFromFlag(flags[0].classList[0])
        fromLanguage = await getLanguageFromFlag(flags[1].classList[0])
      } else if (flags.length === 1) {
        learningLanguage = await getLanguageFromFlag(flags[0].classList[0])
        fromLanguage = uiLanguage
      } else {
        learningLanguage = 'un'
        fromLanguage = uiLanguage
      }
    } else {
      learningLanguage = menuItem.dataset.learning
      fromLanguage = menuItem.dataset.from
    }

    if (flags.length === 2) {
      flags[1].style.display = 'none'
      flags[0].classList.remove('_2IJLr')
    }
    flags[0].classList.add('learning-flag')

    menuItem.dataset.learning = learningLanguage
    menuItem.dataset.from = fromLanguage
    menuItem.classList.add('learning-course')

    if (fromLanguage in baseLanguages) {
      baseLanguages[fromLanguage].querySelector('._1XE6M').appendChild(menuItem)
    } else {
      var fromLanguageItem = await addFromLanguageItem(fromLanguage, fromLanguage === uiLanguage)
      let innerMenu = await createInnerMenu(oldHeader)
      baseLanguages[fromLanguage] = innerMenu
      baseLanguages[fromLanguage].querySelector('._1XE6M').appendChild(menuItem)

      fromLanguageItem.appendChild(baseLanguages[fromLanguage])
      fromLanguageItem.addEventListener('mouseover', setMenuPosition.bind(innerMenu))
    }
  }
}

async function routine () {
  reorganizeMenu()
}

if (document.readyState === 'complete') { routine() } else {
  window.addEventListener('load', routine)
}
<commit_msg>typo fix (can't believe I missed this)<commit_after>// ==UserScript==
// @name        Duolingo Course Switcher (New Site)
// @description Simplifies switching between courses that use different interface language (i.e., base language, the language from which you learn).
// @namespace   https://github.com/zeta12ti/DuolingoCourseSwitcher
// @updateURL   https://github.com/zeta12ti/DuolingoCourseSwitcher/raw/master/DuolingoCourseSwitcher.user.js
// @include     https://*.duolingo.com/*
// @grant       none
// @version     1.1.1
// @author      zeta12ti
// ==/UserScript==

/*
 * NOTE: Annoyingly, duo.l10n.declared may change with time, which shifts the index for each phrase
 * If the header on the course menu is wrong, that's probably what happened. I'll keep track of which duo.version's work here.
*/
console.assert(window.duo.version === 'a74f7a1' || window.duo.version === '4fd6e1b')

// Language names (in english) - for use with duo.l10n.undeclared
var languages = {'ar': 'Arabic', 'bn': 'Bengali', 'ca': 'Catalan', 'cs': 'Czech', 'cy': 'Welsh', 'da': 'Danish', 'de': 'German', 'el': 'Greek', 'en': 'English', 'eo': 'Esperanto', 'es': 'Spanish', 'fr': 'French', 'ga': 'Irish', 'gn': 'Guarani (Jopar\u00e1)', 'he': 'Hebrew', 'hi': 'Hindi', 'hu': 'Hungarian', 'id': 'Indonesian', 'it': 'Italian', 'ja': 'Japanese', 'ko': 'Korean', 'nl-NL': 'Dutch', 'no-BO': 'Norwegian (Bokm\u00e5l)', 'pa': 'Punjabi (Gurmukhi)', 'pl': 'Polish', 'pt': 'Portuguese', 'ro': 'Romanian', 'ru': 'Russian', 'sv': 'Swedish', 'sw': 'Swahili', 'ta': 'Tamil', 'te': 'Telugu', 'th': 'Thai', 'tl': 'Tagalog', 'tlh': 'Klingon', 'tr': 'Turkish', 'uk': 'Ukrainian', 'vi': 'Vietnamese', 'zh-CN': 'Chinese'}

async function getLanguageString (languageCode) {
  return window.duo.l10n.undeclared[languages[languageCode] + '||capitalized'] || window.duo.l10n.undeclared[languages[languageCode]] || languages[languageCode] || 'Unknown'
}

// language flags -
// keys: remove flag- prefix and keep consistency - zs -> zh-CN, kl -> tlh, (zs -> zh-¿?), values: remove _3viv6
var flags = {'ar': '_1ARRD', 'bn': '_2TXAL', 'ca': 'mc4rg', 'cs': '_1uPQW', 'cy': '_1jO8h', 'da': '_1h0xh', 'de': 'oboa9', 'dk': '_3AA1F', 'el': '_2tQo9', 'en': '_2cR-E', 'eo': 'pWj0w', 'es': 'u5W-o', 'fr': '_2KQN3', 'ga': '_1vhNM', 'gn': '_24xu4', 'he': '_PDrK', 'hi': 'OgUIe', 'hu': '_1S3hi', 'id': '_107sn', 'it': '_1PruQ', 'ja': '_2N-Uj', 'ko': '_2lkzc', 'nl-NL': '_1fajz', 'no-BO': '_200jU', 'pl': '_3uusw', 'pt': 'pmGwL', 'ro': '_12U6e', 'ru': '_1eqxJ', 'sn': 'q_PD-', 'sv': '_2DMfV', 'sw': '_3T1km', 'th': '_2oTcA', 'tl': '_1q_MQ', 'tlh': '_6mRM', 'tr': '_1tJJ2', 'uk': '_1zZsN', 'un': 't-XH-', 'vi': '_1KtzC', 'zh': 'xi6jQ', 'zh-CN': '_2gNgd', '_circle-flag': '_2XSZu', '_flag': '_3viv6', 'medium-circle-flag': '_1ct7y _2XSZu', 'micro-circle-flag': '_3i5IF _2XSZu', 'small-circle-flag': '_3PU7E _2XSZu'}
// Use this to identify a row in the original list.
var inverseFlags = {'OgUIe': 'hi', '_107sn': 'id', '_12U6e': 'ro', '_1ARRD': 'ar', '_1KtzC': 'vi', '_1PruQ': 'it', '_1S3hi': 'hu', '_1ct7y _2XSZu': 'medium-circle-flag', '_1eqxJ': 'ru', '_1fajz': 'nl-NL', '_1h0xh': 'da', '_1jO8h': 'cy', '_1q_MQ': 'tl', '_1tJJ2': 'tr', '_1uPQW': 'cs', '_1vhNM': 'ga', '_1zZsN': 'uk', '_200jU': 'no-BO', '_24xu4': 'gn', '_2DMfV': 'sv', '_2KQN3': 'fr', '_2N-Uj': 'ja', '_2TXAL': 'bn', '_2XSZu': '_circle-flag', '_2cR-E': 'en', '_2gNgd': 'zh-CN', '_2lkzc': 'ko', '_2oTcA': 'th', '_2tQo9': 'el', '_3AA1F': 'dk', '_3PU7E _2XSZu': 'small-circle-flag', '_3T1km': 'sw', '_3i5IF _2XSZu': 'micro-circle-flag', '_3uusw': 'pl', '_3viv6': '_flag', '_6mRM': 'tlh', '_PDrK': 'he', 'mc4rg': 'ca', 'oboa9': 'de', 'pWj0w': 'eo', 'pmGwL': 'pt', 'q_PD-': 'sn', 't-XH-': 'un', 'u5W-o': 'es', 'xi6jQ': 'zh'}

async function getLanguageFlag (languageCode) {
    // The 'un' flag is a question mark.
  return flags[languageCode] || flags.un
}

async function getLanguageFromFlag (flagCode) {
  return inverseFlags[flagCode]
}

async function computeMenuOffset (languageSubmenu) {
  var header = languageSubmenu.querySelector('._2PurW')

  var offset = 0
  offset += parseFloat(window.getComputedStyle(languageSubmenu).getPropertyValue('padding-top'))

  offset += parseFloat(window.getComputedStyle(header).getPropertyValue('padding-top'))

  offset += parseFloat(window.getComputedStyle(header).getPropertyValue('padding-bottom'))

  offset += parseFloat(window.getComputedStyle(header).getPropertyValue('line-height'))

  return offset
}

async function setMenuPosition () {
  var verticalOffset = await computeMenuOffset(this)
  var verticalBase = this.parentNode.getBoundingClientRect().top - document.querySelector('._20LC5').getBoundingClientRect().top
  //  var horizontalBase = this.parentNode.getBoundingClientRect().left - document.querySelector('._20LC5').getBoundingClientRect().left
  this.style.top = (verticalBase - verticalOffset) + 'px'
}

async function addRule function (stylesheet, rule) {
  stylesheet.insertRule(rule, stylesheet.cssRules.length)
}

async function addFromLanguageItem (fromLanguage, currentCourse) {
  var fromLanguageItem = document.createElement('li')

  var className = '_2kNgI _1qBnH from-course'
  if (currentCourse) {
    className = '_1oVFS ' + className
  }
  fromLanguageItem.className = className

  var container = document.createElement('li')
  container.className = '_2kNgI flag-container'
  fromLanguageItem.appendChild(container)

  var flag = document.createElement('span')
  className = await getLanguageFlag(fromLanguage) + ' _3viv6 _3vx2Z _1ct7y _2XSZu from-flag'
  flag.className = className
  container.appendChild(flag)

  var localizedLanguageName = await getLanguageString(fromLanguage)
  container.appendChild(document.createTextNode(localizedLanguageName))

  document.querySelector('._1XE6M').appendChild(fromLanguageItem)

  return fromLanguageItem
}

async function createInnerMenu (oldHeader) {
  var innerMenu = document.createElement('ul')
  innerMenu.className = 'OSaWc _1ZY-H language-submenu'

  var innerMenuHeader = document.createElement('li')
  innerMenuHeader.className = '_2PurW'
  innerMenuHeader.innerHTML = oldHeader
  innerMenu.appendChild(innerMenuHeader)

  var innerMenuCourseList = document.createElement('ul')
  innerMenuCourseList.className = '_1XE6M'
  innerMenu.appendChild(innerMenuCourseList)

  return innerMenu
}

async function addStyleSheet () {
  var cssStyle = document.createElement('style')
  document.querySelector('html > head').appendChild(cssStyle)
  var stylesheet = cssStyle.sheet

  addRule(stylesheet, 'li._2kNgI._1qBnH {display: none}')
  addRule(stylesheet, 'ul._1XE6M > li.qsrrc {display: none}')
  addRule(stylesheet, '.from-course:hover .language-submenu {display: block}')
  addRule(stylesheet, 'li._2kNgI._1qBnH.from-course {display: block}')
  addRule(stylesheet, 'li._2kNgI._1qBnH.learning-course {display: block}')
  addRule(stylesheet, '.flag-container {display: block}')
  addRule(stylesheet, '.flag-container {width: 100%}')

  addRule(stylesheet, '.language-submenu ._1XE6M .learning-course ._1fA14 {color: #999}')
  addRule(stylesheet, '.language-submenu ._1XE6M .learning-course:hover ._1fA14 {color: #fff}')

  addRule(stylesheet, '.from-course {position: static}')
  addRule(stylesheet, '._1XE6M {position: static}')
  addRule(stylesheet, '.language-submenu {position: absolute}')
  addRule(stylesheet, '.language-submenu {z-index: 10}')
  addRule(stylesheet, '.flag-container {position: relative}')
  addRule(stylesheet, '.from-course {padding: 0px 0px 0px 0px}')
  addRule(stylesheet, '.flag-container {padding: 3px 20px 3px 47px}')
  addRule(stylesheet, 'html[dir="ltr"] .language-submenu {left:90%}')
  addRule(stylesheet, 'html[dir="rtl"] .language-submenu {right:90%}')
}

async function copyChanges (modifications, menuItem) {
  modifications.forEach(mod => {
    if (mod.target.parentNode.classList && mod.target.parentNode.classList.contains('old-level-indicator')) {
      menuItem.querySelector('.level-indicator').innerText = mod.target.parentNode.innerText
    }
    if (mod.target.classList && mod.target.classList.contains('_2kNgI')) {
      menuItem.className = mod.target.className + ' learning-course'
    }
  })
}

async function reorganizeMenu () {
  var uiLanguage = window.duo.uiLanguage
  var baseLanguages = {}
  var courses = document.querySelectorAll('li._2kNgI._1qBnH:not(.from-course)')

  addStyleSheet()

  var oldHeader = document.querySelector('._2PurW').innerHTML
  document.querySelector('._2PurW').innerHTML = '
' + window.duo.l10n.declared[144] + '
' // Languages

  for (var i = 0, len = courses.length; i < len; i++) {
    let menuItem = courses[i].cloneNode(true)
    courses[i].classList.add('old-menu-item')
    courses[i].querySelector('._1fA14').classList.add('old-level-indicator')

    menuItem.addEventListener('click', courses[i].click.bind(courses[i]))
    menuItem.querySelector('._1fA14').classList.add('level-indicator')
    let observer = new window.MutationObserver((mods) => { copyChanges(mods, menuItem) })
    var config = {attributes: true, characterData: true, subtree: true}
    observer.observe(courses[i], config)

    var flags = menuItem.querySelectorAll('._3viv6')
    var learningLanguage, fromLanguage
    if (typeof menuItem.dataset.from === 'undefined' && typeof menuItem.dataset.learning === 'undefined') {
      if (flags.length === 2) {
        learningLanguage = await getLanguageFromFlag(flags[0].classList[0])
        fromLanguage = await getLanguageFromFlag(flags[1].classList[0])
      } else if (flags.length === 1) {
        learningLanguage = await getLanguageFromFlag(flags[0].classList[0])
        fromLanguage = uiLanguage
      } else {
        learningLanguage = 'un'
        fromLanguage = uiLanguage
      }
    } else {
      learningLanguage = menuItem.dataset.learning
      fromLanguage = menuItem.dataset.from
    }

    if (flags.length === 2) {
      flags[1].style.display = 'none'
      flags[0].classList.remove('_2IJLr')
    }
    flags[0].classList.add('learning-flag')

    menuItem.dataset.learning = learningLanguage
    menuItem.dataset.from = fromLanguage
    menuItem.classList.add('learning-course')

    if (fromLanguage in baseLanguages) {
      baseLanguages[fromLanguage].querySelector('._1XE6M').appendChild(menuItem)
    } else {
      var fromLanguageItem = await addFromLanguageItem(fromLanguage, fromLanguage === uiLanguage)
      let innerMenu = await createInnerMenu(oldHeader)
      baseLanguages[fromLanguage] = innerMenu
      baseLanguages[fromLanguage].querySelector('._1XE6M').appendChild(menuItem)

      fromLanguageItem.appendChild(baseLanguages[fromLanguage])
      fromLanguageItem.addEventListener('mouseover', setMenuPosition.bind(innerMenu))
    }
  }
}

async function routine () {
  reorganizeMenu()
}

if (document.readyState === 'complete') { routine() } else {
  window.addEventListener('load', routine)
}
<|endoftext|>