<commit_before>var Packet = require('./packet');
var Vec2 = require('./modules/Vec2');
var BinaryWriter = require("./packet/BinaryWriter");

function PlayerTracker(gameServer, socket) {
    this.gameServer = gameServer;
    this.socket = socket;
    this.pID = -1;
    this.userAuth = null;
    this.isRemoved = false;
    this.isCloseRequested = false;
    this._name = "";
    this._skin = "";
    this._nameUtf8 = null;
    this._nameUnicode = null;
    this._skinUtf8 = null;
    this.color = { r: 0, g: 0, b: 0 };
    this.viewNodes = [];
    this.clientNodes = [];
    this.cells = [];
    this.mergeOverride = false; // Triggered by console command
    this._score = 0; // Needed for leaderboard
    this._scale = 1;
    this.isMassChanged = true;
    this.borderCounter = 0;
    this.connectedTime = new Date();

    this.tickLeaderboard = 0;
    this.team = 0;
    this.spectate = false;
    this.freeRoam = false;      // Free-roam mode enables player to move in spectate mode
    this.spectateTarget = null; // Spectate target, null for largest player
    this.lastKeypressTick = 0;

    this.centerPos = new Vec2(0, 0);
    this.mouse = new Vec2(0, 0);
    this.viewBox = {
        minx: 0,
        miny: 0,
        maxx: 0,
        maxy: 0,
        halfWidth: 0,
        halfHeight: 0
    };

    // Scramble the coordinate system for anti-raga
    this.scrambleX = 0;
    this.scrambleY = 0;
    this.scrambleId = 0;
    this.isMinion = false;
    this.isMuted = false;

    // Custom commands
    this.spawnmass = 0;
    this.frozen = false;
    this.customspeed = 0;
    this.rec = false;

    // Minions
    this.miQ = 0;
    this.isMi = false;
    this.minionSplit = false;
    this.minionEject = false;
    this.minionFrozen = false;
    this.minionControl = false;
    this.collectPellets = false;

    // Gamemode function
    if (gameServer) {
        // Player id
        this.pID = gameServer.lastPlayerId++ >> 0;
        // Gamemode function
        gameServer.gameMode.onPlayerInit(this);
        // Only scramble if enabled in config
        this.scramble();
    }
    var UserRoleEnum = require("./enum/UserRoleEnum");
    this.userRole = UserRoleEnum.GUEST;
}

module.exports = PlayerTracker;

// Setters/Getters

PlayerTracker.prototype.scramble = function() {
    if (!this.gameServer.config.serverScrambleLevel) {
        this.scrambleId = 0;
        this.scrambleX = 0;
        this.scrambleY = 0;
    } else {
        this.scrambleId = (Math.random() * 0xFFFFFFFF) >>> 0;
        // avoid mouse packet limitations
        var maxx = Math.max(0, 31767 - this.gameServer.border.width);
        var maxy = Math.max(0, 31767 - this.gameServer.border.height);
        var x = maxx * Math.random();
        var y = maxy * Math.random();
        if (Math.random() >= 0.5) x = -x;
        if (Math.random() >= 0.5) y = -y;
        this.scrambleX = x;
        this.scrambleY = y;
    }
    this.borderCounter = 0;
};

PlayerTracker.prototype.setName = function(name) {
    this._name = name;
    if (!name || !name.length) {
        this._nameUnicode = null;
        this._nameUtf8 = null;
        return;
    }
    var writer = new BinaryWriter();
    writer.writeStringZeroUnicode(name);
    this._nameUnicode = writer.toBuffer();
    writer = new BinaryWriter();
    writer.writeStringZeroUtf8(name);
    this._nameUtf8 = writer.toBuffer();
};

PlayerTracker.prototype.setSkin = function(skin) {
    this._skin = skin;
    if (!skin || !skin.length) {
        this._skinUtf8 = null;
        return;
    }
    var writer = new BinaryWriter();
    writer.writeStringZeroUtf8(skin);
    this._skinUtf8 = writer.toBuffer();
};

PlayerTracker.prototype.setColor = function(color) {
    this.color.r = color.r;
    this.color.g = color.g;
    this.color.b = color.b;
};

PlayerTracker.prototype.getScale = function() {
    if (this.isMassChanged) this.updateMass();
    return this._scale;
};

PlayerTracker.prototype.updateMass = function() {
    this._score = 0; // reset to not cause bugs with playerlist
    for (var i = 0; i < this.cells.length; i++) {
        if (!this.cells[i]) continue;
        this._scale += this.cells[i]._size;
        this._score += this.cells[i]._mass;
    }
    if (!this._scale) {
        this._score = 0; // do not change score if no scale
    } else {
        this._scale = Math.pow(Math.min(64 / this._scale, 1), 0.4);
    }
    this.isMassChanged = false;
};

PlayerTracker.prototype.joinGame = function(name, skin) {
    if (this.cells.length) return;

    if (skin) this.setSkin(skin);
    if (!name) name = "An unnamed cell";
    this.setName(name);
    this.spectate = false;
    this.freeRoam = false;
    this.spectateTarget = null;

    // some old clients don't understand ClearAll message
    // so we will send update for them
    if (this.socket.packetHandler.protocol < 6) {
        this.socket.sendPacket(new Packet.UpdateNodes(this, [], [], [], this.clientNodes));
    }
    this.socket.sendPacket(new Packet.ClearAll());
    this.clientNodes = [];
    this.scramble();
    if (this.gameServer.config.serverScrambleLevel < 2) {
        // no scramble / lightweight scramble
        this.socket.sendPacket(new Packet.SetBorder(this, this.gameServer.border));
    }
    else if (this.gameServer.config.serverScrambleLevel == 3) {
        var ran = 10065536 * Math.random();
        // Ruins most known minimaps (no border)
        var border = {
            minx: this.gameServer.border.minx - ran,
            miny: this.gameServer.border.miny - ran,
            maxx: this.gameServer.border.maxx + ran,
            maxy: this.gameServer.border.maxy + ran
        };
        this.socket.sendPacket(new Packet.SetBorder(this, border));
    }
    this.gameServer.gameMode.onPlayerSpawn(this.gameServer, this);
};

PlayerTracker.prototype.checkConnection = function() {
    // Handle disconnection
    if (!this.socket.isConnected) {
        // Wait for playerDisconnectTime
        var pt = this.gameServer.config.playerDisconnectTime;
        var dt = (this.gameServer.stepDateTime - this.socket.closeTime) / 1e3;
        if (pt && (!this.cells.length || dt >= pt)) {
            // Remove all client cells
            for (var i = 0; i < this.cells.length; i++)
                this.gameServer.removeNode(this.cells[i]);
        }
        this.cells = [];
        this.isRemoved = true;
        this.mouse.x = this.centerPos.x;
        this.mouse.y = this.centerPos.y;
        this.socket.packetHandler.pressSpace = false;
        this.socket.packetHandler.pressQ = false;
        this.socket.packetHandler.pressW = false;
        this.minionControl = false;
        return;
    }

    // Check timeout
    if (!this.isCloseRequested && this.gameServer.config.serverTimeout) {
        dt = (this.gameServer.stepDateTime - this.socket.lastAliveTime) / 1000;
        if (dt >= this.gameServer.config.serverTimeout) {
            this.socket.close(1000, "Connection timeout");
            this.isCloseRequested = true;
        }
    }
};

PlayerTracker.prototype.updateTick = function() {
    if (this.isRemoved || this.isMinion)
        return; // do not update
    this.socket.packetHandler.process();
    if (this.isMi) return;

    // update viewbox
    this.updateSpecView(this.cells.length);
    var scale = Math.max(this.getScale(), this.gameServer.config.serverMinScale);
    var halfWidth = this.gameServer.config.serverViewBaseX / scale / 2;
    var halfHeight = this.gameServer.config.serverViewBaseY / scale / 2;
    this.viewBox = {
        minx: this.centerPos.x - halfWidth,
        miny: this.centerPos.y - halfHeight,
        maxx: this.centerPos.x + halfWidth,
        maxy: this.centerPos.y + halfHeight,
        halfWidth: halfWidth,
        halfHeight: halfHeight
    };

    // update visible nodes
    this.viewNodes = [];
    var self = this;
    this.gameServer.quadTree.find(this.viewBox, function(quadItem) {
        if (quadItem.cell.owner != self)
            self.viewNodes.push(quadItem.cell);
    });
    this.viewNodes = this.viewNodes.concat(this.cells);
    this.viewNodes.sort(function(a, b) { return a.nodeId - b.nodeId; });
};

PlayerTracker.prototype.sendUpdate = function() {
    if (this.isRemoved || !this.socket.packetHandler.protocol ||
        !this.socket.isConnected || this.isMi || this.isMinion ||
        (this.socket._socket.writable != null && !this.socket._socket.writable) ||
        this.socket.readyState != this.socket.OPEN) {
        // do not send update for disconnected clients
        // also do not send if initialization is not complete yet
        return;
    }

    if (this.spectate) {
        if (!this.freeRoam) {
            // spectate target
            var player = this.getSpecTarget();
            if (player) {
                this.setCenterPos(player.centerPos);
                this._scale = player.getScale();
                this.viewBox = player.viewBox;
                this.viewNodes = player.viewNodes;
            }
        }
        // sends camera packet
        this.socket.sendPacket(new Packet.UpdatePosition(
            this, this.centerPos.x, this.centerPos.y, this.getScale()
        ));
    }

    if (this.gameServer.config.serverScrambleLevel == 2) {
        // scramble (moving border)
        if (!this.borderCounter) {
            var b = this.gameServer.border, v = this.viewBox;
            var bound = {
                minx: Math.max(b.minx, v.minx - v.halfWidth),
                miny: Math.max(b.miny, v.miny - v.halfHeight),
                maxx: Math.min(b.maxx, v.maxx + v.halfWidth),
                maxy: Math.min(b.maxy, v.maxy + v.halfHeight)
            };
            this.socket.sendPacket(new Packet.SetBorder(this, bound));
        }
        this.borderCounter++;
        if (this.borderCounter >= 20)
            this.borderCounter = 0;
    }

    var delNodes = [];
    var eatNodes = [];
    var addNodes = [];
    var updNodes = [];
    var oldIndex = 0;
    var newIndex = 0;
    for (; newIndex < this.viewNodes.length && oldIndex < this.clientNodes.length;) {
        if (this.viewNodes[newIndex].nodeId < this.clientNodes[oldIndex].nodeId) {
            addNodes.push(this.viewNodes[newIndex]);
            newIndex++;
            continue;
        }
        if (this.viewNodes[newIndex].nodeId > this.clientNodes[oldIndex].nodeId) {
            var node = this.clientNodes[oldIndex];
            if (node.isRemoved && node.killedBy !== null && node.owner != node.killedBy.owner)
                eatNodes.push(node);
            else
                delNodes.push(node);
            oldIndex++;
            continue;
        }
        var node = this.viewNodes[newIndex];
        // skip food & eject if not moving
        if (node.isMoving || (node.cellType != 1 && node.cellType != 3))
            updNodes.push(node);
        newIndex++;
        oldIndex++;
    }
    for (; newIndex < this.viewNodes.length; ) {
        addNodes.push(this.viewNodes[newIndex]);
        newIndex++;
    }
    for (; oldIndex < this.clientNodes.length; ) {
        var node = this.clientNodes[oldIndex];
        if (node.isRemoved && node.killedBy !== null && node.owner != node.killedBy.owner)
            eatNodes.push(node);
        else
            delNodes.push(node);
        oldIndex++;
    }
    this.clientNodes = this.viewNodes;

    // Send packet
    this.socket.sendPacket(new Packet.UpdateNodes(
        this, addNodes, updNodes, eatNodes, delNodes
    ));

    // Update leaderboard
    if (++this.tickLeaderboard > 25) {
        // 1 / 0.040 = 25 (once per second)
        this.tickLeaderboard = 0;
        if (this.gameServer.leaderboardType >= 0) {
            var packet = new Packet.UpdateLeaderboard(this, this.gameServer.leaderboard, this.gameServer.leaderboardType);
            this.socket.sendPacket(packet);
        }
    }
};

PlayerTracker.prototype.updateSpecView = function(len) {
    if (!this.spectate && len) {
        // in game
        var cx = 0, cy = 0;
        for (var i = 0; i < len; i++) {
            if (!this.cells[i]) continue;
            cx += this.cells[i].position.x;
            cy += this.cells[i].position.y;
            this.centerPos = new Vec2(cx / len, cy / len);
        }
    } else {
        if (this.freeRoam || this.getSpecTarget() == null) {
            // free roam
            var d = this.mouse.clone().sub(this.centerPos);
            this.setCenterPos(this.centerPos.add2(d, 32 / d.sqDist(d)));
            this._scale = this.gameServer.config.serverSpectatorScale;
        } else {
            // spectate target
            this.viewBox = null;
        }
    }
}

PlayerTracker.prototype.pressSpace = function() {
    if (this.spectate) {
        // Check for spam first (to prevent too many add/del updates)
        var tick = this.gameServer.tickCounter;
        if (tick - this.lastKeypressTick < 40)
            return;
        this.lastKeypressTick = tick;

        // Space doesn't work for freeRoam mode
        if (this.freeRoam || this.gameServer.largestClient == null)
            return;
    } else if (this.gameServer.run) {
        // Disable mergeOverride on the last merging cell
        if (this.cells.length <= 2)
            this.mergeOverride = false;
        // Cant split if merging or frozen
        if (this.mergeOverride || this.frozen)
            return;
        this.gameServer.splitCells(this);
    }
};

PlayerTracker.prototype.pressW = function() {
    if (this.spectate) return;
    else if (this.gameServer.run)
        this.gameServer.ejectMass(this);
};

PlayerTracker.prototype.pressQ = function() {
    if (this.spectate) {
        // Check for spam first (to prevent too many add/del updates)
        var tick = this.gameServer.tickCounter;
        if (tick - this.lastKeypressTick < 40)
            return;

        this.lastKeypressTick = tick;
        if (this.spectateTarget == null)
            this.freeRoam = !this.freeRoam;
        this.spectateTarget = null;
    }
};

PlayerTracker.prototype.getSpecTarget = function() {
    if (this.spectateTarget == null || this.spectateTarget.isRemoved || !this.spectateTarget.cells.length) {
        this.spectateTarget = null;
        return this.gameServer.largestClient;
    }
    return this.spectateTarget;
};

PlayerTracker.prototype.setCenterPos = function(p) {
    p.x = Math.max(p.x, this.gameServer.border.minx);
    p.y = Math.max(p.y, this.gameServer.border.miny);
    p.x = Math.min(p.x, this.gameServer.border.maxx);
    p.y = Math.min(p.y, this.gameServer.border.maxy);
    this.centerPos.x = p.x;
    this.centerPos.y = p.y;
};
<commit_msg>Some optimisations<commit_after>var Packet = require('./packet');
var Vec2 = require('./modules/Vec2');
var BinaryWriter = require("./packet/BinaryWriter");

function PlayerTracker(gameServer, socket) {
    this.gameServer = gameServer;
    this.socket = socket;
    this.pID = -1;
    this.userAuth = null;
    this.isRemoved = false;
    this.isCloseRequested = false;
    this._name = "";
    this._skin = "";
    this._nameUtf8 = null;
    this._nameUnicode = null;
    this._skinUtf8 = null;
    this.color = { r: 0, g: 0, b: 0 };
    this.viewNodes = [];
    this.clientNodes = [];
    this.cells = [];
    this.mergeOverride = false; // Triggered by console command
    this._score = 0; // Needed for leaderboard
    this._scale = 1;
    this.borderCounter = 0;
    this.connectedTime = new Date();

    this.tickLeaderboard = 0;
    this.team = 0;
    this.spectate = false;
    this.freeRoam = false;      // Free-roam mode enables player to move in spectate mode
    this.spectateTarget = null; // Spectate target, null for largest player
    this.lastKeypressTick = 0;

    this.centerPos = new Vec2(0, 0);
    this.mouse = new Vec2(0, 0);
    this.viewBox = {
        minx: 0,
        miny: 0,
        maxx: 0,
        maxy: 0,
        halfWidth: 0,
        halfHeight: 0
    };

    // Scramble the coordinate system for anti-raga
    this.scrambleX = 0;
    this.scrambleY = 0;
    this.scrambleId = 0;
    this.isMinion = false;
    this.isMuted = false;

    // Custom commands
    this.spawnmass = 0;
    this.frozen = false;
    this.customspeed = 0;
    this.rec = false;

    // Minions
    this.miQ = 0;
    this.isMi = false;
    this.minionSplit = false;
    this.minionEject = false;
    this.minionFrozen = false;
    this.minionControl = false;
    this.collectPellets = false;

    // Gamemode function
    if (gameServer) {
        // Player id
        this.pID = gameServer.lastPlayerId++ >> 0;
        // Gamemode function
        gameServer.gameMode.onPlayerInit(this);
        // Only scramble if enabled in config
        this.scramble();
    }
    var UserRoleEnum = require("./enum/UserRoleEnum");
    this.userRole = UserRoleEnum.GUEST;
}

module.exports = PlayerTracker;

// Setters/Getters

PlayerTracker.prototype.scramble = function() {
    if (!this.gameServer.config.serverScrambleLevel) {
        this.scrambleId = 0;
        this.scrambleX = 0;
        this.scrambleY = 0;
    } else {
        this.scrambleId = (Math.random() * 0xFFFFFFFF) >>> 0;
        // avoid mouse packet limitations
        var maxx = Math.max(0, 31767 - this.gameServer.border.width);
        var maxy = Math.max(0, 31767 - this.gameServer.border.height);
        var x = maxx * Math.random();
        var y = maxy * Math.random();
        if (Math.random() >= 0.5) x = -x;
        if (Math.random() >= 0.5) y = -y;
        this.scrambleX = x;
        this.scrambleY = y;
    }
    this.borderCounter = 0;
};

PlayerTracker.prototype.setName = function(name) {
    this._name = name;
    var writer = new BinaryWriter();
    writer.writeStringZeroUtf8(name);
    this._nameUtf8 = writer.toBuffer();
};

PlayerTracker.prototype.setSkin = function(skin) {
    this._skin = skin;
    var writer = new BinaryWriter();
    writer.writeStringZeroUtf8(skin);
    this._skinUtf8 = writer.toBuffer();
};

PlayerTracker.prototype.setColor = function(color) {
    this.color.r = color.r;
    this.color.g = color.g;
    this.color.b = color.b;
};

PlayerTracker.prototype.getScale = function() {
    this._score = 0; // reset to not cause bugs with leaderboard
    for (var i = 0; i < this.cells.length; i++) {
        if (!this.cells[i]) continue;
        this._scale += this.cells[i]._size;
        this._score += this.cells[i]._mass;
    }
    if (!this._scale) return this._scale = this._score = 0; // reset
    else return this._scale = Math.pow(Math.min(64 / this._scale, 1), 0.4);
};

PlayerTracker.prototype.joinGame = function(name, skin) {
    if (this.cells.length) return;

    if (skin) this.setSkin(skin);
    if (!name) name = "An unnamed cell";
    this.setName(name);
    this.spectate = false;
    this.freeRoam = false;
    this.spectateTarget = null;

    if (!this.isMi && this.socket.isConnected != null) {
        // some old clients don't understand ClearAll message
        // so we will send update for them
        if (this.socket.packetHandler.protocol < 6) {
            this.socket.sendPacket(new Packet.UpdateNodes(this, [], [], [], this.clientNodes));
        }
        this.socket.sendPacket(new Packet.ClearAll());
        this.clientNodes = [];
        this.scramble();
        if (this.gameServer.config.serverScrambleLevel < 2) {
            // no scramble / lightweight scramble
            this.socket.sendPacket(new Packet.SetBorder(this, this.gameServer.border));
        }
        else if (this.gameServer.config.serverScrambleLevel == 3) {
            var ran = 10065536 * Math.random();
            // Ruins most known minimaps (no border)
            var border = {
                minx: this.gameServer.border.minx - ran,
                miny: this.gameServer.border.miny - ran,
                maxx: this.gameServer.border.maxx + ran,
                maxy: this.gameServer.border.maxy + ran
            };
            this.socket.sendPacket(new Packet.SetBorder(this, border));
        }
    }
    this.gameServer.gameMode.onPlayerSpawn(this.gameServer, this);
};

PlayerTracker.prototype.checkConnection = function() {
    // Handle disconnection
    if (!this.socket.isConnected) {
        // Wait for playerDisconnectTime
        var pt = this.gameServer.config.playerDisconnectTime;
        var dt = (this.gameServer.stepDateTime - this.socket.closeTime) / 1e3;
        if (pt && (!this.cells.length || dt >= pt)) {
            // Remove all client cells
            while (this.cells.length) this.gameServer.removeNode(this.cells[0]);
        }
        this.cells = [];
        this.isRemoved = true;
        this.mouse = null;
        this.socket.packetHandler.pressSpace = false;
        this.socket.packetHandler.pressQ = false;
        this.socket.packetHandler.pressW = false;
        return;
    }

    // Check timeout
    if (!this.isCloseRequested && this.gameServer.config.serverTimeout) {
        dt = (this.gameServer.stepDateTime - this.socket.lastAliveTime) / 1000;
        if (dt >= this.gameServer.config.serverTimeout) {
            this.socket.close(1000, "Connection timeout");
            this.isCloseRequested = true;
        }
    }
};

PlayerTracker.prototype.updateTick = function() {
    if (this.isRemoved || this.isMinion)
        return; // do not update
    this.socket.packetHandler.process();
    if (this.isMi) return;

    // update viewbox
    this.updateSpecView(this.cells.length);
    var scale = Math.max(this.getScale(), this.gameServer.config.serverMinScale);
    var halfWidth = this.gameServer.config.serverViewBaseX / scale / 2;
    var halfHeight = this.gameServer.config.serverViewBaseY / scale / 2;
    this.viewBox = {
        minx: this.centerPos.x - halfWidth,
        miny: this.centerPos.y - halfHeight,
        maxx: this.centerPos.x + halfWidth,
        maxy: this.centerPos.y + halfHeight
    };

    // update visible nodes
    this.viewNodes = [];
    var self = this;
    this.gameServer.quadTree.find(this.viewBox, function(quadItem) {
        if (quadItem.cell.owner != self)
            self.viewNodes.push(quadItem.cell);
    });
    this.viewNodes = this.viewNodes.concat(this.cells);
    this.viewNodes.sort(function(a, b) { return a.nodeId - b.nodeId; });
};

PlayerTracker.prototype.sendUpdate = function() {
    if (this.isRemoved || !this.socket.packetHandler.protocol ||
        !this.socket.isConnected || this.isMi || this.isMinion ||
        (this.socket._socket.writable != null && !this.socket._socket.writable) ||
        this.socket.readyState != this.socket.OPEN) {
        // do not send update for disconnected clients
        // also do not send if initialization is not complete yet
        return;
    }

    if (this.spectate) {
        if (!this.freeRoam) {
            // spectate target
            var player = this.getSpecTarget();
            if (player) {
                this.setCenterPos(player.centerPos);
                this._scale = player._scale;
                this.viewBox = player.viewBox;
                this.viewNodes = player.viewNodes;
            }
        }
        // sends camera packet
        this.socket.sendPacket(new Packet.UpdatePosition(
            this, this.centerPos.x, this.centerPos.y, this._scale
        ));
    }

    if (this.gameServer.config.serverScrambleLevel == 2) {
        // scramble (moving border)
        if (!this.borderCounter) {
            var b = this.gameServer.border, v = this.viewBox;
            var bound = {
                minx: Math.max(b.minx, v.minx - v.halfWidth),
                miny: Math.max(b.miny, v.miny - v.halfHeight),
                maxx: Math.min(b.maxx, v.maxx + v.halfWidth),
                maxy: Math.min(b.maxy, v.maxy + v.halfHeight)
            };
            this.socket.sendPacket(new Packet.SetBorder(this, bound));
        }
        if (++this.borderCounter >= 20) this.borderCounter = 0;
    }

    var delNodes = [];
    var eatNodes = [];
    var addNodes = [];
    var updNodes = [];
    var oldIndex = 0;
    var newIndex = 0;
    for (; newIndex < this.viewNodes.length && oldIndex < this.clientNodes.length;) {
        if (this.viewNodes[newIndex].nodeId < this.clientNodes[oldIndex].nodeId) {
            addNodes.push(this.viewNodes[newIndex]);
            newIndex++;
            continue;
        }
        if (this.viewNodes[newIndex].nodeId > this.clientNodes[oldIndex].nodeId) {
            var node = this.clientNodes[oldIndex];
            if (node.isRemoved && node.killedBy && node.owner != node.killedBy.owner)
                eatNodes.push(node);
            else
                delNodes.push(node);
            oldIndex++;
            continue;
        }
        var node = this.viewNodes[newIndex];
        // skip food & eject if not moving
        if (node.isRemoved) continue;
        if (node.isMoving || (node.cellType != 1 && node.cellType != 3))
            updNodes.push(node);
        newIndex++;
        oldIndex++;
    }
    for (; newIndex < this.viewNodes.length; newIndex++)
        addNodes.push(this.viewNodes[newIndex]);

    for (; oldIndex < this.clientNodes.length; oldIndex++) {
        var node = this.clientNodes[oldIndex];
        if (node.isRemoved && node.killedBy && node.owner != node.killedBy.owner)
            eatNodes.push(node);
        else
            delNodes.push(node);
    }
    this.clientNodes = this.viewNodes;

    // Send update packet
    this.socket.sendPacket(new Packet.UpdateNodes(this, addNodes, updNodes, eatNodes, delNodes));

    // Update leaderboard
    if (++this.tickLeaderboard > 25) {
        // 1 / 0.040 = 25 (once per second)
        this.tickLeaderboard = 0;
        if (this.gameServer.leaderboardType >= 0)
            this.socket.sendPacket(new Packet.UpdateLeaderboard(this, this.gameServer.leaderboard, this.gameServer.leaderboardType));
    }
};

PlayerTracker.prototype.updateSpecView = function(len) {
    if (!this.spectate && len) {
        // in game
        var cx = 0, cy = 0;
        for (var i = 0; i < len; i++) {
            cx += this.cells[i].position.x / len;
            cy += this.cells[i].position.y / len;
            this.centerPos = new Vec2(cx, cy);
        }
    } else {
        if (this.freeRoam || this.getSpecTarget() == null) {
            // free roam
            var d = this.mouse.clone().sub(this.centerPos);
            this.setCenterPos(this.centerPos.add2(d, 32 / d.sqDist(d)));
            this._scale = Math.max(this.gameServer.config.serverSpectatorScale, this.getScale());
        } else
            // spectate target
            this.viewBox = null;
    }
}

PlayerTracker.prototype.pressSpace = function() {
    if (this.spectate) {
        // Check for spam first (to prevent too many add/del updates)
        if (this.gameServer.tickCounter - this.lastKeypressTick < 40)
            return;
        this.lastKeypressTick = this.gameServer.tickCounter;

        // Space doesn't work for freeRoam mode
        if (this.freeRoam || this.gameServer.largestClient == null)
            return;
    } else if (this.gameServer.run) {
        // Disable mergeOverride on the last merging cell
        if (this.cells.length <= 2)
            this.mergeOverride = false;
        // Cant split if merging or frozen
        if (this.mergeOverride || this.frozen)
            return;
        this.gameServer.splitCells(this);
    }
};

PlayerTracker.prototype.pressW = function() {
    if (this.spectate || !this.gameServer.run) return;
    this.gameServer.ejectMass(this);
};

PlayerTracker.prototype.pressQ = function() {
    if (this.spectate) {
        // Check for spam first (to prevent too many add/del updates)
        if (this.gameServer.tickCounter - this.lastKeypressTick < 40)
            return;

        this.lastKeypressTick = this.gameServer.tickCounter;
        if (this.spectateTarget == null)
            this.freeRoam = !this.freeRoam;
        this.spectateTarget = null;
    }
};

PlayerTracker.prototype.getSpecTarget = function() {
    if (this.spectateTarget == null || this.spectateTarget.isRemoved) {
        this.spectateTarget = null;
        return this.gameServer.largestClient;
    }
    return this.spectateTarget;
};

PlayerTracker.prototype.setCenterPos = function(p) {
    p.x = Math.max(p.x, this.gameServer.border.minx);
    p.y = Math.max(p.y, this.gameServer.border.miny);
    p.x = Math.min(p.x, this.gameServer.border.maxx);
    p.y = Math.min(p.y, this.gameServer.border.maxy);
    this.centerPos = p;
};
<|endoftext|>


<commit_before>SyntaxHighlighter.brushes.Bash = function()
{
	var keywords =	'if fi then elif else for do done until while break continue case function return in eq ne ge le';
	var commands =  'alias apropos awk basename bash bc bg builtin bzip2 cal cat cd cfdisk chgrp chmod chown chroot' +
					'cksum clear cmp comm command cp cron crontab csplit cut date dc dd ddrescue declare df ' +
					'diff diff3 dig dir dircolors dirname dirs du echo egrep eject enable env ethtool eval ' +
					'exec exit expand export expr false fdformat fdisk fg fgrep file find fmt fold format ' +
					'free fsck ftp gawk getopts grep groups gzip hash head history hostname id ifconfig ' +
					'import install join kill less let ln local locate logname logout look lpc lpr lprint ' +
					'lprintd lprintq lprm ls lsof make man mkdir mkfifo mkisofs mknod more mount mtools ' +
					'mv netstat nice nl nohup nslookup open op passwd paste pathchk ping popd pr printcap ' +
					'printenv printf ps pushd pwd quota quotacheck quotactl ram rcp read readonly renice ' +
					'remsync rm rmdir rsync screen scp sdiff sed select seq set sftp shift shopt shutdown ' +
					'sleep sort source split ssh strace su sudo sum symlink sync tail tar tee test time ' +
					'times touch top traceroute trap tr true tsort tty type ulimit umask umount unalias ' +
					'uname unexpand uniq units unset unshar useradd usermod users uuencode uudecode v vdir ' +
					'vi watch wc whereis which who whoami Wget xargs yes'
					;

	this.regexList = [
		{ regex: SyntaxHighlighter.regexLib.singleLinePerlComments,		css: 'comments' },		// one line comments
		{ regex: SyntaxHighlighter.regexLib.doubleQuotedString,			css: 'string' },		// double quoted strings
		{ regex: SyntaxHighlighter.regexLib.singleQuotedString,			css: 'string' },		// single quoted strings
		{ regex: new RegExp(this.getKeywords(keywords), 'gm'),			css: 'keyword' },		// keywords
		{ regex: new RegExp(this.getKeywords(commands), 'gm'),			css: 'functions' }		// commands
		];
}

SyntaxHighlighter.brushes.Bash.prototype	= new SyntaxHighlighter.Highlighter();
SyntaxHighlighter.brushes.Bash.aliases		= ['bash', 'shell'];
<commit_msg>Added #!/bin/bash and /path/to/command recognition to the Bash brush.<commit_after>SyntaxHighlighter.brushes.Bash = function()
{
	var keywords =	'if fi then elif else for do done until while break continue case function return in eq ne ge le';
	var commands =  'alias apropos awk basename bash bc bg builtin bzip2 cal cat cd cfdisk chgrp chmod chown chroot' +
					'cksum clear cmp comm command cp cron crontab csplit cut date dc dd ddrescue declare df ' +
					'diff diff3 dig dir dircolors dirname dirs du echo egrep eject enable env ethtool eval ' +
					'exec exit expand export expr false fdformat fdisk fg fgrep file find fmt fold format ' +
					'free fsck ftp gawk getopts grep groups gzip hash head history hostname id ifconfig ' +
					'import install join kill less let ln local locate logname logout look lpc lpr lprint ' +
					'lprintd lprintq lprm ls lsof make man mkdir mkfifo mkisofs mknod more mount mtools ' +
					'mv netstat nice nl nohup nslookup open op passwd paste pathchk ping popd pr printcap ' +
					'printenv printf ps pushd pwd quota quotacheck quotactl ram rcp read readonly renice ' +
					'remsync rm rmdir rsync screen scp sdiff sed select seq set sftp shift shopt shutdown ' +
					'sleep sort source split ssh strace su sudo sum symlink sync tail tar tee test time ' +
					'times touch top traceroute trap tr true tsort tty type ulimit umask umount unalias ' +
					'uname unexpand uniq units unset unshar useradd usermod users uuencode uudecode v vdir ' +
					'vi watch wc whereis which who whoami Wget xargs yes'
					;

	this.regexList = [
		{ regex: /^#!.*$/gm,											css: 'preprocessor bold' },
		{ regex: /\/[\w-\/]+/gm,										css: 'plain' },
		{ regex: SyntaxHighlighter.regexLib.singleLinePerlComments,		css: 'comments' },		// one line comments
		{ regex: SyntaxHighlighter.regexLib.doubleQuotedString,			css: 'string' },		// double quoted strings
		{ regex: SyntaxHighlighter.regexLib.singleQuotedString,			css: 'string' },		// single quoted strings
		{ regex: new RegExp(this.getKeywords(keywords), 'gm'),			css: 'keyword' },		// keywords
		{ regex: new RegExp(this.getKeywords(commands), 'gm'),			css: 'functions' }		// commands
		];
}

SyntaxHighlighter.brushes.Bash.prototype	= new SyntaxHighlighter.Highlighter();
SyntaxHighlighter.brushes.Bash.aliases		= ['bash', 'shell'];
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import {
    View,
    ViewPropTypes,
    TouchableOpacity,
    StyleSheet,
    Text
} from 'react-native'
import PropTypes from 'prop-types';

const handleTabPress = (index, multiple, selectedIndex, onTabPress) => {
    if (multiple) {
        onTabPress(index);
    }
    else if (selectedIndex !== index) {
        onTabPress(index);
    }
};

const TabOption = ({
    isTabActive, index, badge, text,
    firstTabStyle, lastTabStyle,
    tabStyle, activeTabStyle,
    tabTextStyle, activeTabTextStyle,
    tabBadgeContainerStyle, activeTabBadgeContainerStyle,
    tabBadgeStyle, activeTabBadgeStyle,
    onTabPress,
}) => {
    return (
        <TouchableOpacity style={[
            styles.tabStyle,
            tabStyle,
            isTabActive ? [styles.activeTabStyle, activeTabStyle] : {},
            firstTabStyle,
            lastTabStyle]}
            onPress={() => onTabPress(index)}
            activeOpacity={1}>
            <View style={{flexDirection:"row"}}>
              <Text style={[
                  styles.tabTextStyle,
                  tabTextStyle,
                  isTabActive ? [styles.activeTabTextStyle, activeTabTextStyle] : {}]}
                  numberOfLines={1}
                  ellipsizeMode="tail">
                  {text}
              </Text>
              {
                  badge ?
                  <View style={[
                      styles.tabBadgeContainerStyle,
                      tabBadgeContainerStyle,
                      isTabActive ? [styles.activeTabBadgeContainerStyle, activeTabBadgeContainerStyle]: {}]}>
                      <Text style={[
                          styles.tabBadgeStyle,
                          tabBadgeStyle,
                          isTabActive ? [styles.activeTabBadgeStyle, activeTabBadgeStyle]: {}]}>
                          {badge}
                      </Text>
                  </View>: false
              }
          </View>
        </TouchableOpacity>
    );
}

const SegmentedControlTab = ({
    multiple, selectedIndex, selectedIndices, values,
    badges, borderRadius, tabsContainerStyle,
    tabStyle, activeTabStyle,
    tabTextStyle, activeTabTextStyle,
    tabBadgeContainerStyle, activeTabBadgeContainerStyle,
    tabBadgeStyle, activeTabBadgeStyle,
    onTabPress,
}) => {
    const firstTabStyle = [{ borderTopLeftRadius: borderRadius, borderBottomLeftRadius: borderRadius }];
    const lastTabStyle = [{ borderTopRightRadius: borderRadius, borderBottomRightRadius: borderRadius }];

    return (
        <View
            style={[styles.tabsContainerStyle, tabsContainerStyle]}
            removeClippedSubviews={false}>
            {
                values.map((item, index) => {
                    return (
                        <TabOption
                            key={index}
                            index={index}
                            badge={badges && badges[index] ? badges[index] : false}
                            isTabActive={multiple ? selectedIndices.includes(index) : selectedIndex === index}
                            text={item}
                            onTabPress={(index) => handleTabPress(index, multiple, selectedIndex, onTabPress)}
                            firstTabStyle={index === 0 ? [{ borderRightWidth: 0 }, firstTabStyle ] : {}}
                            lastTabStyle={index === values.length - 1 ? [{ borderLeftWidth: 0 }, lastTabStyle ] : {}}
                            tabStyle={[tabStyle, index !== 0 && index !== values.length - 1 ? { marginHorizontal: -1 } : {}]}
                            activeTabStyle={activeTabStyle}
                            tabTextStyle={tabTextStyle}
                            activeTabTextStyle={activeTabTextStyle}
                            tabBadgeContainerStyle={tabBadgeContainerStyle}
                            activeTabBadgeContainerStyle={activeTabBadgeContainerStyle}
                            tabBadgeStyle={tabBadgeStyle}
                            activeTabBadgeStyle={activeTabBadgeStyle} />
                    );
                })
            }
        </View>
    );
};

SegmentedControlTab.propTypes = {
    values: PropTypes.array,
    badges: PropTypes.array,
    multiple: PropTypes.bool,
    onTabPress: PropTypes.func,
    selectedIndex: PropTypes.number,
    selectedIndices: PropTypes.arrayOf(PropTypes.number),
    tabsContainerStyle: ViewPropTypes.style,
    tabStyle: ViewPropTypes.style,
    activeTabStyle: ViewPropTypes.style,
    tabTextStyle: Text.propTypes.style,
    activeTabTextStyle: Text.propTypes.style,
    tabBadgeContainerStyle: Text.propTypes.style,
    activeTabBadgeContainerStyle: Text.propTypes.style,
    tabBadgeStyle: Text.propTypes.style,
    activeTabBadgeStyle: Text.propTypes.style,
    borderRadius: PropTypes.number
}

SegmentedControlTab.defaultProps =  {
    values: ['One', 'Two', 'Three'],
    badges: ['', '', ''],
    multiple: false,
    selectedIndex: 0,
    selectedIndices: [0],
    onTabPress() { },
    tabsContainerStyle: {},
    tabStyle: {},
    activeTabStyle: {},
    tabTextStyle: {},
    activeTabTextStyle: {},
    tabBadgeContainerStyle: {},
    activeTabBadgeContainerStyle: {},
    tabBadgeStyle: {},
    activeTabBadgeStyle: {},
    borderRadius: 5
}

const styles = StyleSheet.create({
    tabsContainerStyle: {
        backgroundColor: 'transparent',
        flexDirection: 'row',
    },
    tabStyle: {
        paddingVertical: 5,
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        borderColor: '#0076FF',
        borderWidth: 1,
        backgroundColor: 'white',
    },
    activeTabStyle: {
        backgroundColor: '#0076FF'
    },
    tabTextStyle: {
        color: '#0076FF'
    },
    activeTabTextStyle: {
        color: 'white'
    },
    tabBadgeContainerStyle: {
        borderRadius: 20,
        backgroundColor: 'red',
        paddingLeft: 5,
        paddingRight: 5,
        marginLeft: 5,
        marginBottom: 3
    },
    activeTabBadgeContainerStyle: {
        backgroundColor: 'white'
    },
    tabBadgeStyle: {
        color: 'white',
        fontSize: 11,
        fontWeight: 'bold'
    },
    activeTabBadgeStyle: {
        color: 'black'
    }
})

export default SegmentedControlTab
<commit_msg>Fix the rendering issue on IOS 10. (border bleeding)<commit_after>import React, { Component } from 'react'
import {
    View,
    ViewPropTypes,
    TouchableOpacity,
    StyleSheet,
    Text
} from 'react-native'
import PropTypes from 'prop-types';

const handleTabPress = (index, multiple, selectedIndex, onTabPress) => {
    if (multiple) {
        onTabPress(index);
    }
    else if (selectedIndex !== index) {
        onTabPress(index);
    }
};

const TabOption = ({
    isTabActive, index, badge, text,
    firstTabStyle, lastTabStyle,
    tabStyle, activeTabStyle,
    tabTextStyle, activeTabTextStyle,
    tabBadgeContainerStyle, activeTabBadgeContainerStyle,
    tabBadgeStyle, activeTabBadgeStyle,
    onTabPress,
}) => {
    return (
        <TouchableOpacity style={[
            styles.tabStyle,
            tabStyle,
            isTabActive ? [styles.activeTabStyle, activeTabStyle] : {},
            firstTabStyle,
            lastTabStyle]}
            onPress={() => onTabPress(index)}
            activeOpacity={1}>
            <View style={{ flexDirection: "row" }}>
                <Text style={[
                    styles.tabTextStyle,
                    tabTextStyle,
                    isTabActive ? [styles.activeTabTextStyle, activeTabTextStyle] : {}]}
                    numberOfLines={1}
                    ellipsizeMode="tail">
                    {text}
                </Text>
                {
                    badge ?
                        <View style={[
                            styles.tabBadgeContainerStyle,
                            tabBadgeContainerStyle,
                            isTabActive ? [styles.activeTabBadgeContainerStyle, activeTabBadgeContainerStyle] : {}]}>
                            <Text style={[
                                styles.tabBadgeStyle,
                                tabBadgeStyle,
                                isTabActive ? [styles.activeTabBadgeStyle, activeTabBadgeStyle] : {}]}>
                                {badge}
                            </Text>
                        </View> : false
                }
            </View>
        </TouchableOpacity>
    );
}

const SegmentedControlTab = ({
    multiple, selectedIndex, selectedIndices, values,
    badges, borderRadius, tabsContainerStyle,
    tabStyle, activeTabStyle,
    tabTextStyle, activeTabTextStyle,
    tabBadgeContainerStyle, activeTabBadgeContainerStyle,
    tabBadgeStyle, activeTabBadgeStyle,
    onTabPress,
}) => {

    const firstTabStyle = [{ borderRightWidth: 0, borderTopLeftRadius: borderRadius, borderBottomLeftRadius: borderRadius }]
    const lastTabStyle = [{ borderLeftWidth: 0, borderTopRightRadius: borderRadius, borderBottomRightRadius: borderRadius }]

    return (
        <View
            style={[styles.tabsContainerStyle, tabsContainerStyle]}
            removeClippedSubviews={false}>
            {
                values.map((item, index) => {
                    return (
                        <TabOption
                            key={index}
                            index={index}
                            badge={badges && badges[index] ? badges[index] : false}
                            isTabActive={multiple ? selectedIndices.includes(index) : selectedIndex === index}
                            text={item}
                            onTabPress={(index) => handleTabPress(index, multiple, selectedIndex, onTabPress)}
                            firstTabStyle={index === 0 ? [{ borderRightWidth: 0 }, firstTabStyle] : {}}
                            lastTabStyle={index === values.length - 1 ? [{ borderLeftWidth: 0 }, lastTabStyle] : {}}
                            tabStyle={[tabStyle, index !== 0 && index !== values.length - 1 ? { paddingLeft: -1 } : {}]}
                            activeTabStyle={activeTabStyle}
                            tabTextStyle={tabTextStyle}
                            activeTabTextStyle={activeTabTextStyle}
                            tabBadgeContainerStyle={tabBadgeContainerStyle}
                            activeTabBadgeContainerStyle={activeTabBadgeContainerStyle}
                            tabBadgeStyle={tabBadgeStyle}
                            activeTabBadgeStyle={activeTabBadgeStyle} />
                    );
                })
            }
        </View>
    );
};

SegmentedControlTab.propTypes = {
    values: PropTypes.array,
    badges: PropTypes.array,
    multiple: PropTypes.bool,
    onTabPress: PropTypes.func,
    selectedIndex: PropTypes.number,
    selectedIndices: PropTypes.arrayOf(PropTypes.number),
    tabsContainerStyle: ViewPropTypes.style,
    tabStyle: ViewPropTypes.style,
    activeTabStyle: ViewPropTypes.style,
    tabTextStyle: Text.propTypes.style,
    activeTabTextStyle: Text.propTypes.style,
    tabBadgeContainerStyle: Text.propTypes.style,
    activeTabBadgeContainerStyle: Text.propTypes.style,
    tabBadgeStyle: Text.propTypes.style,
    activeTabBadgeStyle: Text.propTypes.style,
    borderRadius: PropTypes.number
}

SegmentedControlTab.defaultProps = {
    values: ['One', 'Two', 'Three'],
    badges: ['', '', ''],
    multiple: false,
    selectedIndex: 0,
    selectedIndices: [0],
    onTabPress() { },
    tabsContainerStyle: {},
    tabStyle: {},
    activeTabStyle: {},
    tabTextStyle: {},
    activeTabTextStyle: {},
    tabBadgeContainerStyle: {},
    activeTabBadgeContainerStyle: {},
    tabBadgeStyle: {},
    activeTabBadgeStyle: {},
    borderRadius: 5
}

const styles = StyleSheet.create({
    tabsContainerStyle: {
        backgroundColor: 'transparent',
        flexDirection: 'row',
    },
    tabStyle: {
        paddingVertical: 5,
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        borderColor: '#0076FF',
        borderWidth: 1,
        backgroundColor: 'white',
    },
    activeTabStyle: {
        backgroundColor: '#0076FF'
    },
    tabTextStyle: {
        color: '#0076FF'
    },
    activeTabTextStyle: {
        color: 'white'
    },
    tabBadgeContainerStyle: {
        borderRadius: 20,
        backgroundColor: 'red',
        paddingLeft: 5,
        paddingRight: 5,
        marginLeft: 5,
        marginBottom: 3
    },
    activeTabBadgeContainerStyle: {
        backgroundColor: 'white'
    },
    tabBadgeStyle: {
        color: 'white',
        fontSize: 11,
        fontWeight: 'bold'
    },
    activeTabBadgeStyle: {
        color: 'black'
    }
})

export default SegmentedControlTab
<|endoftext|>


<commit_before>#!/usr/bin/env node
'use strict';

const fs = require('fs');
const { execSync } = require('child_process');
const editor = require('editor');
const { version, author } = require('./package.json');

const CHANGES_FILE = 'CHANGES.md';
const CHANGES_HEADING = '# Changes\n\n';

// Get previous file content:
let previous;
try {
  previous = fs.readFileSync(CHANGES_FILE, 'utf8');
  if (previous.indexOf(CHANGES_HEADING) !== 0) {
    console.error('Unexpected changes file content');
    process.exit(1);
  }
} catch (e) {
  previous = CHANGES_HEADING;
}

// Generate changes for this release:
let changes;
try {
  const opts = { encoding: 'utf8' };
  if (previous === CHANGES_HEADING) {
    changes = execSync('git log --format="- %s (%an)"', opts);
  } else {
    changes = execSync(`git log v${version}..HEAD --format="- %s (%an)"`, opts);
  }
} catch (e) {
  process.exit(1);
}

// Only mention contributors:
if (author) {
  const author_name = author.substring(0, author.indexOf('<')).trim();
  changes = changes.replace(new RegExp(` \\(${author_name}\\)`, 'g'), '');
}

const remain = previous.substring(CHANGES_HEADING.length);
const heading = `## ${version}`;
fs.writeFileSync(CHANGES_FILE,
  `${CHANGES_HEADING}${heading}\n\n${changes}\n${remain}`);

editor('CHANGES.md', (code) => {
  if (code !== 0
      || fs.readFileSync(CHANGES_FILE, 'utf8').indexOf(heading) === -1) {
    fs.writeFileSync(CHANGES_FILE, previous);
    process.exit(1);
  }
  execSync(`git add ${CHANGES_FILE}`);
});
<commit_msg>Exit less aggressively in abort and improve comments<commit_after>#!/usr/bin/env node
'use strict';

const fs = require('fs');
const { execSync } = require('child_process');
const editor = require('editor');
const { version, author } = require('./package.json');

const CHANGES_FILE = 'CHANGES.md';
const CHANGES_HEADING = '# Changes\n\n';

// Get previous file content
let previous;
try {
  previous = fs.readFileSync(CHANGES_FILE, 'utf8');
  if (previous.indexOf(CHANGES_HEADING) !== 0) {
    console.error('Unexpected changes file content');
    process.exit(1);
  }
} catch (e) {
  previous = CHANGES_HEADING;
}

// Generate changes for this release
let changes;
try {
  const opts = { encoding: 'utf8' };
  if (previous === CHANGES_HEADING) {
    changes = execSync('git log --format="- %s (%an)"', opts);
  } else {
    changes = execSync(`git log v${version}..HEAD --format="- %s (%an)"`, opts);
  }
} catch (e) {
  process.exit(1);
}

// Only mention contributors
if (author) {
  const author_name = author.substring(0, author.indexOf('<')).trim();
  changes = changes.replace(new RegExp(` \\(${author_name}\\)`, 'g'), '');
}

// Generate new changes
const remain = previous.substring(CHANGES_HEADING.length);
const heading = `## ${version}`;
fs.writeFileSync(CHANGES_FILE,
  `${CHANGES_HEADING}${heading}\n\n${changes}\n${remain}`);

// Let the user edit the changes
editor('CHANGES.md', (code) => {
  if (code !== 0
      || fs.readFileSync(CHANGES_FILE, 'utf8').indexOf(heading) === -1) {
    // Abort
    fs.writeFileSync(CHANGES_FILE, previous);
    process.exitCode = 1;
  } else {
    // Add changes file so that npm includes it in the release commit
    execSync(`git add ${CHANGES_FILE}`);
  }
});
<|endoftext|>


<commit_before>/*
 * "Ignore but Retain" plugin.
 *
 * This plugin allows you to mark any node in an XForm's XML with
 * 'ignore="true"'.
 *
 * Marked nodes will be removed from the XML before it's processed by Vellum,
 * and reinserted when Vellum writes out XML.  Crucially, marked nodes are
 * reinserted in the same position relative to the node that they originally
 * followed, as identified by that node's attribute which includes a question
 * ID.
 *
 * Question ID changes are handled.
 *
 * If a reference node for an ignore node is deleted by the user, then that node
 * will be lost.
 *
 * Spec:
 * https://docs.google.com/document/d/12see6m3Lr6nVVgjfstS3oS6Vc1UT7l4bqYNRtDB-GXQ/
 */
define([
    'underscore',
    'jquery',
    'vellum/mugs',
    'vellum/parser',
    'vellum/util',
    'vellum/core'
], function (
    _,
    $,
    mugs,
    parser,
    util
) {
    var xmls = new XMLSerializer(),
        MUG = "mug",
        PARENT = "parent";

    $.vellum.plugin("ignore", {}, {
        loadXML: function (xmlStr, options) {
            this.data.ignore.ignoredNodes = [];
            if (!xmlStr) {
                this.__callOld();
                return;
            }
            var _this = this,
                ignoredEls = [],
                xmlDoc = $.parseXML(xmlStr),
                xml = $(xmlDoc),
                ignores = xml.find('[vellum\\:ignore="retain"]');

            this.data.ignore.active = ignores.length;
            if (!this.data.ignore.active) {
                // skip serialize
                this.__callOld();
                return;
            }

            options.enableInstanceRefCounting = false;
            this.data.ignore.ignoredMugs = [];

            var model = xml.find('h\\:html > h\\:head > model:first'),
                instance = model.find('instance:first'),
                body = xml.find('h\\:html > body:first');
            _.each([model, instance, body], function (el) {
                if (!el.length) {
                    window.console.log("WARNING", el.selector, "not found");
                }
            });
            ignores = ignores.not(function (i, el) {
                var isDataOrControl = _.any($(el).parents(), function (parent) {
                        return instance.is(parent) || body.is(parent) ||
                            _.any(ignores, function (ignored) {
                                // exclude nested nodes ... O(n^2)
                                return $(ignored).is(parent);
                            });
                    });
                if (!isDataOrControl && el.nodeName === "bind") {
                    return $(el).parent().is(model);
                }
                return isDataOrControl;
            });
            ignores.each(function (i, el) {
                _this.data.ignore.ignoredNodes.push(getPathAndPosition(el));
                ignoredEls.push(el);
            });

            // do this last in case one ignore node follows another; it can still
            // use the first one as its reference
            _.each(ignoredEls, function (el) {
                el.parentElement.removeChild(el);
            });

            this.__callOld(xmls.serializeToString(xml[0]), options);
        },
        createXML: function () {
            var xmlStr = this.__callOld(),
                ignoredNodes = this.data.ignore.ignoredNodes;
            if (!this.data.ignore.active) {
                return xmlStr;
            }

            var xml = $($.parseXML(xmlStr));

            _.each(ignoredNodes, function (node) {
                if (node.path === "h\\:html > h\\:body") {
                    // Something weird happens involving body since it's an HTML
                    // tag, apparently.  But only when it's the terminal node in a
                    // selector. Also behaves differently in FF and Chrome.
                    node.path = "h\\:html > body, h\\:html > h\\:body";
                } else if (node.path === "h\\:html > h\\:head") {
                    // same as above?
                    node.path = "h\\:html > head, h\\:html > h\\:head";
                }

                var parentNode = xml.find(node.path),
                    ignored = $($.parseXML(node.nodeXML).childNodes[0]),
                    prev = node.prev && parentNode.find(node.prev),
                    next = node.next && parentNode.find(node.next);

                if (prev && prev.length) {
                    prev.first().after(ignored);
                } else if (next && next.length) {
                    next.first().before(ignored);
                } else {
                    // sibling was deleted, insert at end
                    parentNode.append(ignored);
                }
            });

            var count = 0;
            return xmls.serializeToString(xml[0])
                // firefox adds xmlns="" to fragments when adding them to the
                // document
                .replace(/ xmlns=""/g, '')
                .replace(/ xmlns:vellum="(.+?)"/g, function (match) {
                    return count++ ? '' : match;
                });
        },
        getMugTypes: function () {
            var types = this.__callOld();
            types.normal.Ignored = IgnoredQuestion;
            return types;
        },
        parseDataElement: function (form, el, parentMug) {
            if (this.data.ignore.active) {
                var $el = $(el);
                if ($el.attr("vellum:ignore") === "retain") {
                    var mug = form.mugTypes.make("Ignored", form);
                    mug.p.nodeID = el.nodeName;
                    mug.p.rawDataAttributes = parser.getAttributes(el);
                    if ($el.children().length) {
                        mug.p.dataNodeXML = serializeXML($el)
                            // strip wrapper element
                            .replace(/^<[^>]+>([^]*)<\/[^>]+>$/, "$1");
                    } else {
                        mug.p.dataNodeXML = "";
                    }
                    this.data.ignore.ignoredMugs.push(mug);
                    return mug;
                }
            }
            return this.__callOld();
        },
        parseBindElement: function (form, el, path) {
            if (this.data.ignore.active) {
                var mug = form.getMugByPath(path);
                if (!mug) {
                    mug = findParent(path, form);
                }
                if ((mug && mug.__className === "Ignored") ||
                    el.attr("vellum:ignore") === "retain")
                {
                    var basePath, relativeTo;
                    if (mug && mug.__className === "Ignored") {
                        basePath = mug.absolutePath;
                        relativeTo = MUG;
                    } else {
                        var parent = null;
                        if (mug) {
                            parent = mug.options.isSpecialGroup ? mug : mug.parentMug;
                        }
                        mug = makeIgnoredMug(form, parent);
                        form.tree.insertMug(mug, 'into', parent);
                        // HACK fix abstraction broken by direct tree insert
                        form._fixMugState(mug);
                        basePath = parent ? parent.absolutePath : form.getBasePath(true);
                        relativeTo = PARENT;
                        this.data.ignore.ignoredMugs.push(mug);
                    }
                    mug.p.binds.push({
                        path: path.startsWith(basePath) ?
                                    path.slice(basePath.length) : path,
                        relativeTo: path.startsWith(basePath) ? relativeTo : null,
                        attrs: parser.getAttributes(el)
                    });
                    return;
                }
            }
            this.__callOld();
        },
        getControlNodeAdaptorFactory: function (tagName) {
            var getAdaptor = this.__callOld();
            if (this.data.ignore.active) {
                var ignoredMugs = this.data.ignore.ignoredMugs;
                return function ($cEl, appearance, form, parentMug) {
                    function adapt() {
                        if (!mug || mug.__className !== "Ignored") {
                            mug = makeIgnoredMug(form, parentMug);
                            ignoredMugs.push(mug);
                        }
                        restoreAttributes($cEl);
                        mug.p.controlNode = serializeXML($cEl);
                        return mug;
                    }
                    adapt.skipPopulate = true;
                    var path = parser.getPathFromControlElement(
                                        $cEl, form, parentMug, true),
                        mug = form.getMugByPath(path);
                    if ($cEl.attr("vellum:ignore") === "retain") {
                        adapt.ignoreDataNode = mug && mug.__className !== "Ignored";
                        return adapt;
                    }
                    if (mug && mug.__className === "Ignored") {
                        return adapt;
                    }
                    var args = Array.prototype.slice.call(arguments);
                    return getAdaptor.apply(null, args);
                };
            }
            return getAdaptor;
        },
        getSections: function (mug) {
            if (this.data.ignore.active && mug.__className === "Ignored") {
                return [{
                    slug: "advanced",
                    type: "accordion",
                    displayName: "Advanced",
                    properties: [
                        "nodeID",
                    ],
                    isCollapsed: true,
                    help: {
                        title: "Advanced",
                        text: "This question represents advanced content " +
                          "that is not supported by the form builder. Please " +
                          "only change it if you have a specific need!"
                    }
                }];
            }
            return this.__callOld();
        },
        handleMugRename: function (form, mug, newID, oldID, newPath, oldPath) {
            this.__callOld();
            if (this.data.ignore.active) {
                var oldEscaped = oldPath ? RegExp.escape(oldPath) : oldPath,
                    pathRegex = new RegExp(oldEscaped + '(\\W|$)', 'g'),
                    newPattern = newPath + "$1";
                _.each(this.data.ignore.ignoredNodes, function (node) {
                    node.nodeXML = node.nodeXML.replace(pathRegex, newPattern);
                });
                _.each(this.data.ignore.ignoredMugs, function (mug) {
                    if (mug.p.controlNode) {
                        mug.p.controlNode =
                            mug.p.controlNode.replace(pathRegex, newPattern);
                    }
                    _.each(mug.p.binds, function (bind) {
                        bind.attrs = _.object(_.map(bind.attrs, function (value, key) {
                            return [key, value.replace(pathRegex, newPattern)];
                        }));
                    });
                });
            }
        }
    });

    var IgnoredQuestion = {
            typeName: "Ignored XML",
            icon: 'icon-question-sign',
            isTypeChangeable: false,
            isRemoveable: false,
            isCopyable: false,
            init: function (mug) {
                mug.p.binds = [];
            },
            parseDataNode: function (mug, node) {
                return $([]);
            },
            getTagName: function (mug, nodeID) {
                return mug.p.rawDataAttributes ? nodeID : null;
            },
            writeDataNodeXML: function (writer, mug) {
                if (mug.p.dataNodeXML) {
                    writer.writeXML(mug.p.dataNodeXML);
                }
            },
            getBindList: function (mug) {
                return _.map(mug.p.binds, function (bind) {
                    var attrs = _.clone(bind.attrs),
                        basePath = "";
                    if (bind.relativeTo === MUG) {
                        basePath = mug.absolutePath;
                    } else if (bind.relativeTo === PARENT) {
                        var parent = mug.parentMug;
                        basePath = parent ? parent.absolutePath :
                                            mug.form.getBasePath(true);
                    }
                    attrs.nodeset = basePath + bind.path;
                    return attrs;
                });
            },
            writesOnlyCustomXML: true,
            writeCustomXML: function (writer, mug) {
                if (mug.p.controlNode) {
                    writer.writeXML(mug.p.controlNode);
                }
            },
            spec: {
                label: { presence: 'notallowed' },
                labelItext: { presence: 'notallowed' },
                labelItextID: { presence: 'notallowed' },
                hintLabel: { presence: 'notallowed' },
                hintItext: { presence: 'notallowed' },
                hintItextID: { presence: 'notallowed' },
                helpItext: { presence: 'notallowed' },
                helpItextID: { presence: 'notallowed' },
                mediaItext: { presence: 'notallowed' },
                otherItext: { presence: 'notallowed' },
                appearance: { presence: 'notallowed' },
            }
        };

    function findParent(path, form) {
        var parent = null,
            regex = /[\/[][^\/[]+$/;
        while (!parent && path && regex.test(path)) {
            path = path.replace(regex, "");
            if (path) {
                parent = form.getMugByPath(path);
            }
        }
        return parent;
    }

    function makeIgnoredMug(form, parent) {
        var mug = form.mugTypes.make("Ignored", form);
        mug.p.nodeID = form.generate_item_label(parent, "ignored--", 1);
        return mug;
    }

    function restoreAttributes(el) {
        if (el.length && el[0].poppedAttributes) {
            el.attr(el[0].poppedAttributes);
        }
    }

    function serializeXML(xml) {
        var string = xmls.serializeToString(xml[0]),
            i = string.indexOf(">");
        if (i === -1) {
            return string;
        }
        // remove xmlns attributes from the root node
        return string.slice(0, i)
                     .replace(/ xmlns(:.*?)?="(.*?)"/g, "") + string.slice(i);
    }

    function getPathAndPosition(node) {
        var origNode = node,
            path = [],
            prev, next;

        prev = getSiblingSelector(node, "previous");
        next = getSiblingSelector(node, "next");
        node = node.parentElement;

        while (node) {
            path.unshift(getNodeIdentifierSelector(node));
            node = node.parentElement;
        }

        var nodeXML = xmls.serializeToString(origNode)
            // XMLSerializer adds xmlns to fragments, which we don't want.  This
            // should only remove the xmlns from the root node, so if inner elements
            // have an XMLNS, it will be preserved.
                .replace(/xmlns="(.*?)"/, "");

        return {
            prev: prev,
            next: next,
            path: path.join(" > "),
            nodeXML: "\n" + nodeXML
        };
    }

    function getSiblingSelector(node, position) {
        var sel = getNodeIdentifierSelector(node[position + "ElementSibling"]);
        return sel && sel + ", " + sel.replace(/\w+:([^:]*)$/, "$1");
    }

    function getNodeIdentifierSelector(node) {
        if (!node) {
            return null;
        }
        var $node = $(node),
            nodeset = $node.attr('nodeset'),
            ref = $node.attr('ref'),
            id = $node.attr('id'),
            tagName = ($node.prop('tagName') || '').toLowerCase();

        if (tagName === 'setvalue') {
            return '[event="' + $node.attr('event') + '"]' +
                   '[ref="' + ref + '"]';
        } else if (nodeset) {
            return '[nodeset="' + nodeset + '"]';
        } else if (ref) {
            return '[ref="' + ref + '"]';
        } else if (id) {
            return '[id="' + id + '"]';
        } else {
            // escape ':' in namespaced selector for jQuery selector usage
            return node.nodeName.replace(":", "\\:");

            // to have this be fully generic for a node with any path,
            // we would have to add an :nth-child or even better :nth-of-type
            // selector for intermediate nodes without a question ID but which
            // could be at a level with multiple nodes with the same tag name.
            // But this would have complex edge cases for when nodes at inner
            // levels were added or deleted -- being able to recognize that a
            // find failed and then retry with a different selector expression.
            //
            // For XForms and the ignored nodes that we expect to see, the
            // current implementation is sufficient.
        }
    }
});
<commit_msg>Improve mug rename handler<commit_after>/*
 * "Ignore but Retain" plugin.
 *
 * This plugin allows you to mark any node in an XForm's XML with
 * 'ignore="true"'.
 *
 * Marked nodes will be removed from the XML before it's processed by Vellum,
 * and reinserted when Vellum writes out XML.  Crucially, marked nodes are
 * reinserted in the same position relative to the node that they originally
 * followed, as identified by that node's attribute which includes a question
 * ID.
 *
 * Question ID changes are handled.
 *
 * If a reference node for an ignore node is deleted by the user, then that node
 * will be lost.
 *
 * Spec:
 * https://docs.google.com/document/d/12see6m3Lr6nVVgjfstS3oS6Vc1UT7l4bqYNRtDB-GXQ/
 */
define([
    'underscore',
    'jquery',
    'vellum/mugs',
    'vellum/parser',
    'vellum/util',
    'vellum/core'
], function (
    _,
    $,
    mugs,
    parser,
    util
) {
    var xmls = new XMLSerializer(),
        MUG = "mug",
        PARENT = "parent";

    $.vellum.plugin("ignore", {}, {
        loadXML: function (xmlStr, options) {
            this.data.ignore.ignoredNodes = [];
            if (!xmlStr) {
                this.__callOld();
                return;
            }
            var _this = this,
                ignoredEls = [],
                xmlDoc = $.parseXML(xmlStr),
                xml = $(xmlDoc),
                ignores = xml.find('[vellum\\:ignore="retain"]');

            this.data.ignore.active = ignores.length;
            if (!this.data.ignore.active) {
                // skip serialize
                this.__callOld();
                return;
            }

            options.enableInstanceRefCounting = false;
            this.data.ignore.ignoredMugs = [];

            var model = xml.find('h\\:html > h\\:head > model:first'),
                instance = model.find('instance:first'),
                body = xml.find('h\\:html > body:first');
            _.each([model, instance, body], function (el) {
                if (!el.length) {
                    window.console.log("WARNING", el.selector, "not found");
                }
            });
            ignores = ignores.not(function (i, el) {
                var isDataOrControl = _.any($(el).parents(), function (parent) {
                        return instance.is(parent) || body.is(parent) ||
                            _.any(ignores, function (ignored) {
                                // exclude nested nodes ... O(n^2)
                                return $(ignored).is(parent);
                            });
                    });
                if (!isDataOrControl && el.nodeName === "bind") {
                    return $(el).parent().is(model);
                }
                return isDataOrControl;
            });
            ignores.each(function (i, el) {
                _this.data.ignore.ignoredNodes.push(getPathAndPosition(el));
                ignoredEls.push(el);
            });

            // do this last in case one ignore node follows another; it can still
            // use the first one as its reference
            _.each(ignoredEls, function (el) {
                el.parentElement.removeChild(el);
            });

            this.__callOld(xmls.serializeToString(xml[0]), options);
        },
        createXML: function () {
            var xmlStr = this.__callOld(),
                ignoredNodes = this.data.ignore.ignoredNodes;
            if (!this.data.ignore.active) {
                return xmlStr;
            }

            var xml = $($.parseXML(xmlStr));

            _.each(ignoredNodes, function (node) {
                if (node.path === "h\\:html > h\\:body") {
                    // Something weird happens involving body since it's an HTML
                    // tag, apparently.  But only when it's the terminal node in a
                    // selector. Also behaves differently in FF and Chrome.
                    node.path = "h\\:html > body, h\\:html > h\\:body";
                } else if (node.path === "h\\:html > h\\:head") {
                    // same as above?
                    node.path = "h\\:html > head, h\\:html > h\\:head";
                }

                var parentNode = xml.find(node.path),
                    ignored = $($.parseXML(node.nodeXML).childNodes[0]),
                    prev = node.prev && parentNode.find(node.prev),
                    next = node.next && parentNode.find(node.next);

                if (prev && prev.length) {
                    prev.first().after(ignored);
                } else if (next && next.length) {
                    next.first().before(ignored);
                } else {
                    // sibling was deleted, insert at end
                    parentNode.append(ignored);
                }
            });

            var count = 0;
            return xmls.serializeToString(xml[0])
                // firefox adds xmlns="" to fragments when adding them to the
                // document
                .replace(/ xmlns=""/g, '')
                .replace(/ xmlns:vellum="(.+?)"/g, function (match) {
                    return count++ ? '' : match;
                });
        },
        getMugTypes: function () {
            var types = this.__callOld();
            types.normal.Ignored = IgnoredQuestion;
            return types;
        },
        parseDataElement: function (form, el, parentMug) {
            if (this.data.ignore.active) {
                var $el = $(el);
                if ($el.attr("vellum:ignore") === "retain") {
                    var mug = form.mugTypes.make("Ignored", form);
                    mug.p.nodeID = el.nodeName;
                    mug.p.rawDataAttributes = parser.getAttributes(el);
                    if ($el.children().length) {
                        mug.p.dataNodeXML = serializeXML($el)
                            // strip wrapper element
                            .replace(/^<[^>]+>([^]*)<\/[^>]+>$/, "$1");
                    } else {
                        mug.p.dataNodeXML = "";
                    }
                    this.data.ignore.ignoredMugs.push(mug);
                    return mug;
                }
            }
            return this.__callOld();
        },
        parseBindElement: function (form, el, path) {
            if (this.data.ignore.active) {
                var mug = form.getMugByPath(path);
                if (!mug) {
                    mug = findParent(path, form);
                }
                if ((mug && mug.__className === "Ignored") ||
                    el.attr("vellum:ignore") === "retain")
                {
                    var basePath, relativeTo;
                    if (mug && mug.__className === "Ignored") {
                        basePath = mug.absolutePath;
                        relativeTo = MUG;
                    } else {
                        var parent = null;
                        if (mug) {
                            parent = mug.options.isSpecialGroup ? mug : mug.parentMug;
                        }
                        mug = makeIgnoredMug(form, parent);
                        form.tree.insertMug(mug, 'into', parent);
                        // HACK fix abstraction broken by direct tree insert
                        form._fixMugState(mug);
                        basePath = parent ? parent.absolutePath : form.getBasePath(true);
                        relativeTo = PARENT;
                        this.data.ignore.ignoredMugs.push(mug);
                    }
                    mug.p.binds.push({
                        path: path.startsWith(basePath) ?
                                    path.slice(basePath.length) : path,
                        relativeTo: path.startsWith(basePath) ? relativeTo : null,
                        attrs: parser.getAttributes(el)
                    });
                    return;
                }
            }
            this.__callOld();
        },
        getControlNodeAdaptorFactory: function (tagName) {
            var getAdaptor = this.__callOld();
            if (this.data.ignore.active) {
                var ignoredMugs = this.data.ignore.ignoredMugs;
                return function ($cEl, appearance, form, parentMug) {
                    function adapt() {
                        if (!mug || mug.__className !== "Ignored") {
                            mug = makeIgnoredMug(form, parentMug);
                            ignoredMugs.push(mug);
                        }
                        restoreAttributes($cEl);
                        mug.p.controlNode = serializeXML($cEl);
                        return mug;
                    }
                    adapt.skipPopulate = true;
                    var path = parser.getPathFromControlElement(
                                        $cEl, form, parentMug, true),
                        mug = form.getMugByPath(path);
                    if ($cEl.attr("vellum:ignore") === "retain") {
                        adapt.ignoreDataNode = mug && mug.__className !== "Ignored";
                        return adapt;
                    }
                    if (mug && mug.__className === "Ignored") {
                        return adapt;
                    }
                    var args = Array.prototype.slice.call(arguments);
                    return getAdaptor.apply(null, args);
                };
            }
            return getAdaptor;
        },
        getSections: function (mug) {
            if (this.data.ignore.active && mug.__className === "Ignored") {
                return [{
                    slug: "advanced",
                    type: "accordion",
                    displayName: "Advanced",
                    properties: [
                        "nodeID",
                    ],
                    isCollapsed: true,
                    help: {
                        title: "Advanced",
                        text: "This question represents advanced content " +
                          "that is not supported by the form builder. Please " +
                          "only change it if you have a specific need!"
                    }
                }];
            }
            return this.__callOld();
        },
        handleMugRename: function (form, mug, newID, oldID, newPath, oldPath) {
            this.__callOld();
            if (this.data.ignore.active && oldPath) {
                var oldEscaped = RegExp.escape(oldPath),
                    pathRegex = new RegExp(oldEscaped + '(\\W|$)', 'g'),
                    newPattern = newPath + "$1";
                _.each(this.data.ignore.ignoredNodes, function (node) {
                    node.nodeXML = node.nodeXML.replace(pathRegex, newPattern);
                });
                _.each(this.data.ignore.ignoredMugs, function (mug) {
                    if (mug.p.controlNode) {
                        mug.p.controlNode =
                            mug.p.controlNode.replace(pathRegex, newPattern);
                    }
                    _.each(mug.p.binds, function (bind) {
                        bind.attrs = _.object(_.map(bind.attrs, function (value, key) {
                            return [key, value.replace(pathRegex, newPattern)];
                        }));
                    });
                });
            }
        }
    });

    var IgnoredQuestion = {
            typeName: "Ignored XML",
            icon: 'icon-question-sign',
            isTypeChangeable: false,
            isRemoveable: false,
            isCopyable: false,
            init: function (mug) {
                mug.p.binds = [];
            },
            parseDataNode: function (mug, node) {
                return $([]);
            },
            getTagName: function (mug, nodeID) {
                return mug.p.rawDataAttributes ? nodeID : null;
            },
            writeDataNodeXML: function (writer, mug) {
                if (mug.p.dataNodeXML) {
                    writer.writeXML(mug.p.dataNodeXML);
                }
            },
            getBindList: function (mug) {
                return _.map(mug.p.binds, function (bind) {
                    var attrs = _.clone(bind.attrs),
                        basePath = "";
                    if (bind.relativeTo === MUG) {
                        basePath = mug.absolutePath;
                    } else if (bind.relativeTo === PARENT) {
                        var parent = mug.parentMug;
                        basePath = parent ? parent.absolutePath :
                                            mug.form.getBasePath(true);
                    }
                    attrs.nodeset = basePath + bind.path;
                    return attrs;
                });
            },
            writesOnlyCustomXML: true,
            writeCustomXML: function (writer, mug) {
                if (mug.p.controlNode) {
                    writer.writeXML(mug.p.controlNode);
                }
            },
            spec: {
                label: { presence: 'notallowed' },
                labelItext: { presence: 'notallowed' },
                labelItextID: { presence: 'notallowed' },
                hintLabel: { presence: 'notallowed' },
                hintItext: { presence: 'notallowed' },
                hintItextID: { presence: 'notallowed' },
                helpItext: { presence: 'notallowed' },
                helpItextID: { presence: 'notallowed' },
                mediaItext: { presence: 'notallowed' },
                otherItext: { presence: 'notallowed' },
                appearance: { presence: 'notallowed' },
            }
        };

    function findParent(path, form) {
        var parent = null,
            regex = /[\/[][^\/[]+$/;
        while (!parent && path && regex.test(path)) {
            path = path.replace(regex, "");
            if (path) {
                parent = form.getMugByPath(path);
            }
        }
        return parent;
    }

    function makeIgnoredMug(form, parent) {
        var mug = form.mugTypes.make("Ignored", form);
        mug.p.nodeID = form.generate_item_label(parent, "ignored--", 1);
        return mug;
    }

    function restoreAttributes(el) {
        if (el.length && el[0].poppedAttributes) {
            el.attr(el[0].poppedAttributes);
        }
    }

    function serializeXML(xml) {
        var string = xmls.serializeToString(xml[0]),
            i = string.indexOf(">");
        if (i === -1) {
            return string;
        }
        // remove xmlns attributes from the root node
        return string.slice(0, i)
                     .replace(/ xmlns(:.*?)?="(.*?)"/g, "") + string.slice(i);
    }

    function getPathAndPosition(node) {
        var origNode = node,
            path = [],
            prev, next;

        prev = getSiblingSelector(node, "previous");
        next = getSiblingSelector(node, "next");
        node = node.parentElement;

        while (node) {
            path.unshift(getNodeIdentifierSelector(node));
            node = node.parentElement;
        }

        var nodeXML = xmls.serializeToString(origNode)
            // XMLSerializer adds xmlns to fragments, which we don't want.  This
            // should only remove the xmlns from the root node, so if inner elements
            // have an XMLNS, it will be preserved.
                .replace(/xmlns="(.*?)"/, "");

        return {
            prev: prev,
            next: next,
            path: path.join(" > "),
            nodeXML: "\n" + nodeXML
        };
    }

    function getSiblingSelector(node, position) {
        var sel = getNodeIdentifierSelector(node[position + "ElementSibling"]);
        return sel && sel + ", " + sel.replace(/\w+:([^:]*)$/, "$1");
    }

    function getNodeIdentifierSelector(node) {
        if (!node) {
            return null;
        }
        var $node = $(node),
            nodeset = $node.attr('nodeset'),
            ref = $node.attr('ref'),
            id = $node.attr('id'),
            tagName = ($node.prop('tagName') || '').toLowerCase();

        if (tagName === 'setvalue') {
            return '[event="' + $node.attr('event') + '"]' +
                   '[ref="' + ref + '"]';
        } else if (nodeset) {
            return '[nodeset="' + nodeset + '"]';
        } else if (ref) {
            return '[ref="' + ref + '"]';
        } else if (id) {
            return '[id="' + id + '"]';
        } else {
            // escape ':' in namespaced selector for jQuery selector usage
            return node.nodeName.replace(":", "\\:");

            // to have this be fully generic for a node with any path,
            // we would have to add an :nth-child or even better :nth-of-type
            // selector for intermediate nodes without a question ID but which
            // could be at a level with multiple nodes with the same tag name.
            // But this would have complex edge cases for when nodes at inner
            // levels were added or deleted -- being able to recognize that a
            // find failed and then retry with a different selector expression.
            //
            // For XForms and the ignored nodes that we expect to see, the
            // current implementation is sufficient.
        }
    }
});
<|endoftext|>


<commit_before>'use strict';

const funnel = require('broccoli-funnel');
const babel = require('broccoli-babel-transpiler');
const merge = require('broccoli-merge-trees');
const concat = require('broccoli-concat');
const resolveModuleSource = require('amd-name-resolver').moduleResolve;

const transpileToES5 = require('./transpile-to-es5');
const writePackageJSON = require('./write-package-json');
const writeLicense = require('./write-license');

const Project = require('../utils/project');
const project = Project.from('packages');

module.exports = function buildPackages(es2017, matrix) {
  // Filter out test files from the package builds.
  es2017 = funnel(es2017, {
    exclude: ['**/test/**']
  });

  // Create an ES5 version of the higher-fidelity ES2017 code.
  let es5 = transpileToES5(es2017);
  let targets = { es5, es2017 };

  let packages = project.packages
    .map(buildPackage);

  packages = flatten(packages);
  packages = merge(flatten(packages), { overwrite: true });

  return packages;

  function buildPackage(pkg) {
    let pkgName = pkg.name;
    let builds;

    // The TypeScript compiler doesn't re-emit `.d.ts` files, which is all that
    // @glimmer/interfaces exports. We need to special case this package and
    // copy over the definition files from source.
    if (pkgName === '@glimmer/interfaces') {
      builds = [copyVerbatim('@glimmer/interfaces')];
    } else {
      builds = buildMatrix(pkgName, matrix);
    }

    return [
      writePackageJSON(pkgName),
      writeLicense(`${pkgName}/LICENSE`),
      ...builds
    ];
  }

  function buildMatrix(pkgName) {
    return matrix.map(([modules, target]) => {
      let source = targets[target];
      switch (modules) {
        case 'amd':
          return transpileAMD(pkgName, target, source);
        case 'commonjs':
          return transpileCommonJS(pkgName, target, source);
        case 'modules':
          return copyESModules(pkgName, target, source);
        case 'types':
          return copyTypes(pkgName, targets.es2017);
        default:
          throw new Error(`Unsupported module target '${target}'.`);
      }
    });
  }
}

function copyVerbatim(pkgName) {
  return funnel(`packages/${pkgName}`, {
    destDir: `${pkgName}/dist/types/`
  });
}

function flatten(arr) {
  return arr.reduce((out, cur) => out.concat(cur), []);
}

function copyESModules(pkgName, target, source) {
  return funnel(source, {
    srcDir: pkgName,
    destDir: `${pkgName}/dist/modules/${target}/`,
    exclude: ['**/*.d.ts']
  });
}

function copyTypes(pkg, source) {
  return funnel(source, {
    srcDir: pkg,
    include: ['**/*.d.ts'],
    destDir: `${pkg}/dist/types`
  });
}

function transpileAMD(pkgName, esVersion, tree) {
  let bundleName = pkgName.replace('/', '-').replace('@', '');
  let pkgTree = funnel(tree, {
    include: [`${pkgName}/**/*`],
    exclude: ['**/*.d.ts']
  });

  let amd = babel(pkgTree, {
    moduleId: true,
    resolveModuleSource,
    plugins: [['transform-es2015-modules-amd', { noInterop: true, strict: true }]]
  });

  return concat(amd, {
   inputFiles: ['**/*'],
   sourceMapConfig: { enabled: false },
   outputFile: `/${pkgName}/dist/amd/${esVersion}/${bundleName}.js`
  });
}

function transpileCommonJS(pkgName, esVersion, tree) {
  let pkgTree = funnel(tree, {
    include: [`${pkgName}/**/*`],
    exclude: ['**/*.d.ts']
  });

  let options = {
    annotation: `Transpile CommonJS - ${pkgName} - ${esVersion}`,
    plugins: ['transform-es2015-modules-commonjs'],
    sourceMaps: 'inline'
  };

  let commonjsTree = babel(pkgTree, options);

  return funnel(commonjsTree, {
    srcDir: pkgName,
    destDir: `${pkgName}/dist/commonjs/${esVersion}`
  });
}
<commit_msg>Fix @glimmer/blueprint build<commit_after>'use strict';

const funnel = require('broccoli-funnel');
const babel = require('broccoli-babel-transpiler');
const merge = require('broccoli-merge-trees');
const concat = require('broccoli-concat');
const resolveModuleSource = require('amd-name-resolver').moduleResolve;

const transpileToES5 = require('./transpile-to-es5');
const writePackageJSON = require('./write-package-json');
const writeLicense = require('./write-license');

const Project = require('../utils/project');
const project = Project.from('packages');

module.exports = function buildPackages(es2017, matrix) {
  // Filter out test files from the package builds.
  es2017 = funnel(es2017, {
    exclude: ['**/test/**']
  });

  // Create an ES5 version of the higher-fidelity ES2017 code.
  let es5 = transpileToES5(es2017);
  let targets = { es5, es2017 };

  let packages = project.packages
    .map(buildPackage);

  packages = flatten(packages);
  packages = merge(flatten(packages), { overwrite: true });

  return packages;

  function buildPackage(pkg) {
    let pkgName = pkg.name;
    let builds;

    // The blueprint package is structured differently from other packages, so
    // we just copy it over verbatim to the build output.
    if (pkgName === '@glimmer/blueprint') {
      builds = [funnel(`packages/${pkgName}`, {
        destDir: `${pkgName}/`,
        exclude: ['**/node_modules/**']
      })];
    } else {
      builds = buildMatrix(pkgName, matrix);
    }

    return [
      writePackageJSON(pkgName),
      writeLicense(`${pkgName}/LICENSE`),
      ...builds
    ];
  }

  function buildMatrix(pkgName) {
    return matrix.map(([modules, target]) => {
      let source = targets[target];
      switch (modules) {
        case 'amd':
          return transpileAMD(pkgName, target, source);
        case 'commonjs':
          return transpileCommonJS(pkgName, target, source);
        case 'modules':
          return copyESModules(pkgName, target, source);
        case 'types':
          return copyTypes(pkgName, targets.es2017);
        default:
          throw new Error(`Unsupported module target '${target}'.`);
      }
    });
  }
}

function flatten(arr) {
  return arr.reduce((out, cur) => out.concat(cur), []);
}

function copyESModules(pkgName, target, source) {
  return funnel(source, {
    srcDir: pkgName,
    destDir: `${pkgName}/dist/modules/${target}/`,
    exclude: ['**/*.d.ts']
  });
}

function copyTypes(pkg, source) {
  return funnel(source, {
    srcDir: pkg,
    include: ['**/*.d.ts'],
    destDir: `${pkg}/dist/types`
  });
}

function transpileAMD(pkgName, esVersion, tree) {
  let bundleName = pkgName.replace('/', '-').replace('@', '');
  let pkgTree = funnel(tree, {
    include: [`${pkgName}/**/*`],
    exclude: ['**/*.d.ts']
  });

  let amd = babel(pkgTree, {
    moduleId: true,
    resolveModuleSource,
    plugins: [['transform-es2015-modules-amd', { noInterop: true, strict: true }]]
  });

  return concat(amd, {
   inputFiles: ['**/*'],
   sourceMapConfig: { enabled: false },
   outputFile: `/${pkgName}/dist/amd/${esVersion}/${bundleName}.js`
  });
}

function transpileCommonJS(pkgName, esVersion, tree) {
  let pkgTree = funnel(tree, {
    include: [`${pkgName}/**/*`],
    exclude: ['**/*.d.ts']
  });

  let options = {
    annotation: `Transpile CommonJS - ${pkgName} - ${esVersion}`,
    plugins: ['transform-es2015-modules-commonjs'],
    sourceMaps: 'inline'
  };

  let commonjsTree = babel(pkgTree, options);

  return funnel(commonjsTree, {
    srcDir: pkgName,
    destDir: `${pkgName}/dist/commonjs/${esVersion}`
  });
}
<|endoftext|>