<commit_before>window.requestAnimFrame = (function (callback) {
  return window.requestAnimationFrame ||
  window.webkitRequestAnimationFrame ||
  window.mozRequestAnimationFrame ||
  window.oRequestAnimationFrame ||
  window.msRequestAnimationFrame ||
  function (callback) {
    window.setTimeout(callback, 1000 / 60);
  }
})();

var dx = 0;
var dy = 0;

var screenWidth = 1600;
var screenHeight = 900;

var linearSpeed = 50;

var field = {
  grid: 10, width: 160, height: 90, parts: []
};

var id = getRandomInt(1000, 9999);
var connectedCombines = {};

var combine = {
  id: id,
  grain: 0,
  fuel: 300,
  x: 100, y: 100, angle: 0, width: 80, height: 80,
  header: { x1: 0, y1: 0, x2: 10, y2: 10},
  back: { x1: 0, y1: 0, x2: 10, y2: 10},
  diagonal: 0,
  diagonalAngleRad: 0,
  diagonalAngleDeg: 0,
  diagonal2: 0,
  diagonal2AngleRad: 0,
  diagonal2AngleDeg: 0,
  sprite: null
};

combine.diagonal = combine.height / 2;
combine.diagonal2 = Math.sqrt(Math.pow(combine.height / 10, 2) + Math.pow(combine.width - 30, 2));
combine.diagonal2AngleRad = Math.atan2(combine.height / 10, combine.width - 30);
combine.diagonal2AngleDeg = combine.diagonal2AngleRad * 180 / Math.PI;

var canvas = document.getElementById("canvas");
canvas.width = screenWidth;
canvas.height = screenHeight;
var context = canvas.getContext("2d");

var bufferCanvas = document.createElement('canvas');
bufferCanvas.width = screenWidth;
bufferCanvas.height = screenHeight;
var bufferContext = bufferCanvas.getContext("2d");

var serverCanvas = document.createElement('canvas');
serverCanvas.width = screenWidth;
serverCanvas.height = screenHeight;
var serverContext = serverCanvas.getContext("2d");

var fieldCanvas = document.createElement('canvas');
fieldCanvas.width = screenWidth;
fieldCanvas.height = screenHeight;
var fieldContext = fieldCanvas.getContext("2d");

var spritesImage = new Image();
spritesImage.src = 'atlas.png';
spritesImage.onload = function() {
  generateField();
  renderField(fieldContext);
  var date = new Date();
  var time = date.getTime();
  animate(time);
}

/*
var socket = io();
socket.on('combine', function(combineMsg){
connectedCombines[combineMsg.id] = combineMsg;
});

socket.on('empty', function(msg){
fieldContext.drawImage(spritesImage, 20, 60, 20, 20, msg.i * field.grid, msg.j * field.grid, field.grid, field.grid);
});

socket.on('straw', function(msg){
fieldContext.drawImage(spritesImage, 40, 60, 20, 20, msg.i * field.grid, msg.j * field.grid, field.grid, field.grid);
});
*/

document.onkeydown = function (e) {
  var key = e.keyCode;
  if (key == 37) dx = -1;
  else if (key == 38) dy = -1;
  else if (key == 39) dx = 1;
  else if (key == 40) dy = 1;
  else return true;
  return false;
};

document.onkeyup = function (e) {
  var key = e.keyCode;
  if (key == 37 || key == 39) dx = 0;
  else if (key == 38 || key == 40) dy = 0;
  else return true;
  return false;
};

function generateField() {
  for (var i = 0; i < field.width; i++) {
    if (!field.parts[i]) {
      field.parts[i] = [];
    }
    for (var j = 0; j < field.height; j++) {
      field.parts[i][j] = {
        type: 0
      };
    }
  }
}

function renderField(ctx) {
  for (var i = 0; i < field.width; i++) {
    for (var j = 0; j < field.height; j++) {
      var partOfField = field.parts[i][j];
      ctx.drawImage(spritesImage, 0, 60, 20, 20, i * field.grid, j * field.grid, field.grid, field.grid);
    }
  }
}

function updateFieldView(ctx, i, j, type) {
  if (field.parts[i] === undefined || field.parts[i][j] === undefined) {
    return false;
  }
  var partOfField = field.parts[i][j];
  if (partOfField.type === 0 && type === 1) {
    partOfField.type = type;
    if (combine.grain < 5000) {
      combine.grain += 1;
    }
    // socket.emit('empty', {i:i, j:j});
    ctx.drawImage(spritesImage, 20, 60, 20, 20, i * field.grid, j * field.grid, field.grid, field.grid);
    return true;
  } else if (partOfField.type === 1 && type === 2) {
    partOfField.type = type;
    // socket.emit('straw', {i:i, j:j});
    ctx.drawImage(spritesImage, 40, 60, 20, 20, i * field.grid, j * field.grid, field.grid, field.grid);
    return false;
  }
}

function animate(lastTime) {

  // update
  var date = new Date();
  var time = date.getTime();
  var timeDiff = time - lastTime;
  var linearDistEachFrame = linearSpeed * timeDiff / 1000;

  if (dy !== 0) {
    if (combine.fuel > 0) {
      combine.fuel -= timeDiff * 0.001;
    } else {
      combine.fuel = 0;
    }
    if (dy == 1) {
      combine.angle += linearDistEachFrame * -dx;
    } else {
      combine.angle += linearDistEachFrame * dx;
    }
  }

  combine.x -= dy * Math.cos(combine.angle * Math.PI / 180) * linearDistEachFrame;
  combine.y -= dy * Math.sin(combine.angle * Math.PI / 180) * linearDistEachFrame;

  var diagonalAngle1 = combine.angle + 60;//combine.diagonalAngleDeg;
  var diagonalAngle2 = combine.angle - 60;//combine.diagonalAngleDeg;
  var diagonalAngleRad1 = diagonalAngle1 * Math.PI / 180;
  var diagonalAngleRad2 = diagonalAngle2 * Math.PI / 180;
  combine.header.x1 = Math.cos(diagonalAngleRad1) * combine.diagonal + combine.x;
  combine.header.y1 = Math.sin(diagonalAngleRad1) * combine.diagonal + combine.y;
  combine.header.x2 = Math.cos(diagonalAngleRad2) * combine.diagonal + combine.x;
  combine.header.y2 = Math.sin(diagonalAngleRad2) * combine.diagonal + combine.y;

  var diagonalAngleBack1 = combine.angle + combine.diagonal2AngleDeg - 180;
  var diagonalAngleBack2 = combine.angle - combine.diagonal2AngleDeg - 180;
  var diagonalAngleBackRad1 = diagonalAngleBack1 * Math.PI / 180;
  var diagonalAngleBackRad2 = diagonalAngleBack2 * Math.PI / 180;
  combine.back.x1 = Math.cos(diagonalAngleBackRad1) * combine.diagonal2 + combine.x;
  combine.back.y1 = Math.sin(diagonalAngleBackRad1) * combine.diagonal2 + combine.y;
  combine.back.x2 = Math.cos(diagonalAngleBackRad2) * combine.diagonal2 + combine.x;
  combine.back.y2 = Math.sin(diagonalAngleBackRad2) * combine.diagonal2 + combine.y;

  // socket.emit('combine', {id:combine.id, x:combine.x, y:combine.y, angle:combine.angle});

  bufferContext.clearRect(0, 0, bufferCanvas.width, bufferCanvas.height);
  renderCombine(bufferContext, combine);
  // for (var prop in connectedCombines) {
  //   if (connectedCombines.hasOwnProperty(prop)) {
  //     renderCombine(bufferContext, connectedCombines[prop]);
  //   }
  // }

  if (dy < 0) {
    if (bline(combine.header.x1, combine.header.y1, combine.header.x2, combine.header.y2, 1)) {
      bline(combine.back.x1, combine.back.y1, combine.back.x2, combine.back.y2, 2);
    }
  }

  var grainLevel = combine.grain * 100/5000;
  var fuelLevel = combine.fuel * 100/300;
  renderBar(bufferContext, grainLevel, 10, 10);
  renderBar(bufferContext, fuelLevel, 40, 10);

  context.clearRect(0, 0, canvas.width, canvas.height);
  context.drawImage(fieldCanvas, 0, 0);
  context.drawImage(bufferCanvas, 0, 0);
  // context.drawImage(serverCanvas, 0, 0);

  lastTime = time;
  requestAnimFrame(function () {
    animate(lastTime);
  });
}

function renderBar(ctx, level, x, y) {
  ctx.save();
  ctx.fillStyle="#FFFFFF";
  ctx.fillRect(x, y, 20, 100);
  ctx.fillStyle="#68c1e7";
  ctx.fillRect(x,100 + y - level, 20, level);
  ctx.strokeStyle="#000000";
  ctx.strokeRect(x, y, 20, 100);
  ctx.restore();
}

function renderCombine(ctx, combineObj) {
  ctx.save();
  ctx.translate(combineObj.x, combineObj.y);
  ctx.rotate(combineObj.angle * Math.PI / 180);
  ctx.translate(-combine.width, -combine.height / 2);
  ctx.translate(30, 0);
  ctx.drawImage(spritesImage, 0, 0, 20, 20, 0, 0, combine.width, combine.height);
  ctx.restore();
}

function bline(x0, y0, x1, y1, type) {
  x0 = Math.floor(x0/field.grid);
  y0 = Math.floor(y0/field.grid);
  x1 = Math.floor(x1/field.grid);
  y1 = Math.floor(y1/field.grid);
  var dx = Math.abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
  var dy = Math.abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
  var err = (dx>dy ? dx : -dy)/2;
  var notEmptyField = false;
  while (true) {
    if (updateFieldView(fieldContext, x0, y0, type)) {
      notEmptyField = true;
    }
    if (x0 === x1 && y0 === y1) break;
    var e2 = err;
    if (e2 > -dx) { err -= dy; x0 += sx; }
    if (e2 < dy) { err += dx; y0 += sy; }
  }
  return notEmptyField;
}

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}
<commit_msg>done level added<commit_after>window.requestAnimFrame = (function (callback) {
  return window.requestAnimationFrame ||
  window.webkitRequestAnimationFrame ||
  window.mozRequestAnimationFrame ||
  window.oRequestAnimationFrame ||
  window.msRequestAnimationFrame ||
  function (callback) {
    window.setTimeout(callback, 1000 / 60);
  }
})();

var dx = 0;
var dy = 0;

var screenWidth = 1600;
var screenHeight = 900;

var linearSpeed = 50;

var field = {
  grid: 10, width: 160, height: 90, parts: []
};

var fieldPartsCount = field.width * field.height;
var fieldPartsLeft = fieldPartsCount;

var id = getRandomInt(1000, 9999);
var connectedCombines = {};

var combine = {
  id: id,
  grain: 0,
  maxGrain: 5000,
  fuel: 300,
  maxFuel: 300,
  x: 100, y: 100, angle: 0, width: 80, height: 80,
  header: { x1: 0, y1: 0, x2: 10, y2: 10},
  back: { x1: 0, y1: 0, x2: 10, y2: 10},
  diagonal: 0,
  diagonalAngleRad: 0,
  diagonalAngleDeg: 0,
  diagonal2: 0,
  diagonal2AngleRad: 0,
  diagonal2AngleDeg: 0,
  sprite: null
};

combine.diagonal = combine.height / 2;
combine.diagonal2 = Math.sqrt(Math.pow(combine.height / 10, 2) + Math.pow(combine.width - 30, 2));
combine.diagonal2AngleRad = Math.atan2(combine.height / 10, combine.width - 30);
combine.diagonal2AngleDeg = combine.diagonal2AngleRad * 180 / Math.PI;

var canvas = document.getElementById("canvas");
canvas.width = screenWidth;
canvas.height = screenHeight;
var context = canvas.getContext("2d");

var bufferCanvas = document.createElement('canvas');
bufferCanvas.width = screenWidth;
bufferCanvas.height = screenHeight;
var bufferContext = bufferCanvas.getContext("2d");

var serverCanvas = document.createElement('canvas');
serverCanvas.width = screenWidth;
serverCanvas.height = screenHeight;
var serverContext = serverCanvas.getContext("2d");

var fieldCanvas = document.createElement('canvas');
fieldCanvas.width = screenWidth;
fieldCanvas.height = screenHeight;
var fieldContext = fieldCanvas.getContext("2d");

var spritesImage = new Image();
spritesImage.src = 'atlas.png';
spritesImage.onload = function() {
  generateField();
  renderField(fieldContext);
  var date = new Date();
  var time = date.getTime();
  animate(time);
}

/*
var socket = io();
socket.on('combine', function(combineMsg){
connectedCombines[combineMsg.id] = combineMsg;
});

socket.on('empty', function(msg){
fieldContext.drawImage(spritesImage, 20, 60, 20, 20, msg.i * field.grid, msg.j * field.grid, field.grid, field.grid);
});

socket.on('straw', function(msg){
fieldContext.drawImage(spritesImage, 40, 60, 20, 20, msg.i * field.grid, msg.j * field.grid, field.grid, field.grid);
});
*/

document.onkeydown = function (e) {
  var key = e.keyCode;
  if (key == 37) dx = -1;
  else if (key == 38) dy = -1;
  else if (key == 39) dx = 1;
  else if (key == 40) dy = 1;
  else return true;
  return false;
};

document.onkeyup = function (e) {
  var key = e.keyCode;
  if (key == 37 || key == 39) dx = 0;
  else if (key == 38 || key == 40) dy = 0;
  else return true;
  return false;
};

function generateField() {
  for (var i = 0; i < field.width; i++) {
    if (!field.parts[i]) {
      field.parts[i] = [];
    }
    for (var j = 0; j < field.height; j++) {
      field.parts[i][j] = {
        type: 0
      };
    }
  }
}

function renderField(ctx) {
  for (var i = 0; i < field.width; i++) {
    for (var j = 0; j < field.height; j++) {
      var partOfField = field.parts[i][j];
      ctx.drawImage(spritesImage, 0, 60, 20, 20, i * field.grid, j * field.grid, field.grid, field.grid);
    }
  }
}

function updateFieldView(ctx, i, j, type) {
  if (field.parts[i] === undefined || field.parts[i][j] === undefined) {
    return false;
  }
  var partOfField = field.parts[i][j];
  if (partOfField.type === 0 && type === 1) {
    partOfField.type = type;
    if (combine.grain < combine.maxGrain) {
      combine.grain += 1;
    }
    fieldPartsLeft--;
    // socket.emit('empty', {i:i, j:j});
    ctx.drawImage(spritesImage, 20, 60, 20, 20, i * field.grid, j * field.grid, field.grid, field.grid);
    return true;
  } else if (partOfField.type === 1 && type === 2) {
    partOfField.type = type;
    // socket.emit('straw', {i:i, j:j});
    ctx.drawImage(spritesImage, 40, 60, 20, 20, i * field.grid, j * field.grid, field.grid, field.grid);
    return false;
  }
}

function animate(lastTime) {

  // update
  var date = new Date();
  var time = date.getTime();
  var timeDiff = time - lastTime;
  var linearDistEachFrame = linearSpeed * timeDiff / 1000;

  if (dy !== 0) {
    if (combine.fuel > 0) {
      combine.fuel -= timeDiff * 0.001;
    } else {
      combine.fuel = 0;
    }
    if (dy == 1) {
      combine.angle += linearDistEachFrame * -dx;
    } else {
      combine.angle += linearDistEachFrame * dx;
    }
  }

  combine.x -= dy * Math.cos(combine.angle * Math.PI / 180) * linearDistEachFrame;
  combine.y -= dy * Math.sin(combine.angle * Math.PI / 180) * linearDistEachFrame;

  var diagonalAngle1 = combine.angle + 60;//combine.diagonalAngleDeg;
  var diagonalAngle2 = combine.angle - 60;//combine.diagonalAngleDeg;
  var diagonalAngleRad1 = diagonalAngle1 * Math.PI / 180;
  var diagonalAngleRad2 = diagonalAngle2 * Math.PI / 180;
  combine.header.x1 = Math.cos(diagonalAngleRad1) * combine.diagonal + combine.x;
  combine.header.y1 = Math.sin(diagonalAngleRad1) * combine.diagonal + combine.y;
  combine.header.x2 = Math.cos(diagonalAngleRad2) * combine.diagonal + combine.x;
  combine.header.y2 = Math.sin(diagonalAngleRad2) * combine.diagonal + combine.y;

  var diagonalAngleBack1 = combine.angle + combine.diagonal2AngleDeg - 180;
  var diagonalAngleBack2 = combine.angle - combine.diagonal2AngleDeg - 180;
  var diagonalAngleBackRad1 = diagonalAngleBack1 * Math.PI / 180;
  var diagonalAngleBackRad2 = diagonalAngleBack2 * Math.PI / 180;
  combine.back.x1 = Math.cos(diagonalAngleBackRad1) * combine.diagonal2 + combine.x;
  combine.back.y1 = Math.sin(diagonalAngleBackRad1) * combine.diagonal2 + combine.y;
  combine.back.x2 = Math.cos(diagonalAngleBackRad2) * combine.diagonal2 + combine.x;
  combine.back.y2 = Math.sin(diagonalAngleBackRad2) * combine.diagonal2 + combine.y;

  // socket.emit('combine', {id:combine.id, x:combine.x, y:combine.y, angle:combine.angle});

  bufferContext.clearRect(0, 0, bufferCanvas.width, bufferCanvas.height);
  renderCombine(bufferContext, combine);
  // for (var prop in connectedCombines) {
  //   if (connectedCombines.hasOwnProperty(prop)) {
  //     renderCombine(bufferContext, connectedCombines[prop]);
  //   }
  // }

  if (dy < 0) {
    if (bline(combine.header.x1, combine.header.y1, combine.header.x2, combine.header.y2, 1)) {
      bline(combine.back.x1, combine.back.y1, combine.back.x2, combine.back.y2, 2);
    }
  }

  var grainLevel = combine.grain * 100/combine.maxGrain;
  var fuelLevel = combine.fuel * 100/combine.maxFuel;
  renderBar(bufferContext, grainLevel, 10, 10);
  renderBar(bufferContext, fuelLevel, 40, 10);

  bufferContext.fillText(Math.floor((fieldPartsCount-fieldPartsLeft)/fieldPartsCount * 100) + "% done", 80, 20);

  context.clearRect(0, 0, canvas.width, canvas.height);
  context.drawImage(fieldCanvas, 0, 0);
  context.drawImage(bufferCanvas, 0, 0);
  // context.drawImage(serverCanvas, 0, 0);

  lastTime = time;
  requestAnimFrame(function () {
    animate(lastTime);
  });
}

function renderBar(ctx, level, x, y) {
  ctx.save();
  ctx.fillStyle="#FFFFFF";
  ctx.fillRect(x, y, 20, 100);
  ctx.fillStyle="#68c1e7";
  ctx.fillRect(x,100 + y - level, 20, level);
  ctx.strokeStyle="#000000";
  ctx.strokeRect(x, y, 20, 100);
  ctx.restore();
}

function renderCombine(ctx, combineObj) {
  ctx.save();
  ctx.translate(combineObj.x, combineObj.y);
  ctx.rotate(combineObj.angle * Math.PI / 180);
  ctx.translate(-combine.width, -combine.height / 2);
  ctx.translate(30, 0);
  ctx.drawImage(spritesImage, 0, 0, 20, 20, 0, 0, combine.width, combine.height);
  ctx.restore();
}

function bline(x0, y0, x1, y1, type) {
  x0 = Math.floor(x0/field.grid);
  y0 = Math.floor(y0/field.grid);
  x1 = Math.floor(x1/field.grid);
  y1 = Math.floor(y1/field.grid);
  var dx = Math.abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
  var dy = Math.abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
  var err = (dx>dy ? dx : -dy)/2;
  var notEmptyField = false;
  while (true) {
    if (updateFieldView(fieldContext, x0, y0, type)) {
      notEmptyField = true;
    }
    if (x0 === x1 && y0 === y1) break;
    var e2 = err;
    if (e2 > -dx) { err -= dy; x0 += sx; }
    if (e2 < dy) { err += dx; y0 += sy; }
  }
  return notEmptyField;
}

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}
<|endoftext|>


<commit_before><commit_msg>Refs #302. Updated rangy to version 1.2 for the containsNodeText method<commit_after><|endoftext|>


<commit_before>(function() {

var checkVersion = Dagaz.Model.checkVersion;

Dagaz.Model.checkVersion = function(design, name, value) {
  if (name != "descent-goal") {
      checkVersion(design, name, value);
  }
}

var checkGoals = Dagaz.Model.checkGoals;

Dagaz.Model.checkGoals = function(design, board, player) {
  var f = false;
  _.each(design.allPositions(), function(pos) {
      if (design.inZone(0, 2, pos)) {
          var piece = board.getPiece(pos);
          if ((piece !== null) && (piece.player == 2)) f = true;
      }
  });
  if (f) {
      if (player == 1) {
          return -1;
      } else {
          return 1;
      }
  }
  return checkGoals(design, board, player);
}

})();
<commit_msg>Descent (ai)<commit_after>(function() {

var checkVersion = Dagaz.Model.checkVersion;

Dagaz.Model.checkVersion = function(design, name, value) {
  if (name != "descent-goal") {
      checkVersion(design, name, value);
  }
}

Dagaz.AI.heuristic = function(ai, design, board, move) {
  var group = []; var weight = [];
  _.each(design.allPositions(), function(pos) {
      if (design.inZone(0, 2, pos) && (board.getPiece(pos) === null)) {
          group.push(pos);
          weight[pos] = 0;
      }
  });
  for (var i = 0; i < group.length; i++) {
      _.each(design.allDirections(), function(dir) {
           var pos = design.navigate(board.player, group[i], dir);
           if ((pos !== null) && (_.indexOf(group, pos) < 0) && (board.getPiece(pos) === null)) {
                group.push(pos);
                weight[pos] = weight[group[i]] + 1;
           }
      });
  }
  if (move.isDropMove()) {
      var pos = move.actions[0][1][0];
      if (!_.isUndefined(weight[pos])) {
          if (weight[pos] == 0) {
              return 1000;
          } else {
              return 100 - weight[pos];
          }
      }
  }
  return -1;
}

var checkGoals = Dagaz.Model.checkGoals;

Dagaz.Model.checkGoals = function(design, board, player) {
  var f = false;
  _.each(design.allPositions(), function(pos) {
      if (design.inZone(0, 2, pos)) {
          var piece = board.getPiece(pos);
          if ((piece !== null) && (piece.player == 2)) f = true;
      }
  });
  if (f) {
      if (player == 1) {
          return -1;
      } else {
          return 1;
      }
  }
  return checkGoals(design, board, player);
}

})();
<|endoftext|>


<commit_before>'use strict';
// Look up URLs in SURBL

var url       = require('url');
var dns       = require('dns');
var isIPv4    = require('net').isIPv4;
var net_utils = require('./net_utils');
var utils     = require('./utils');

// Default regexps to extract the URIs from the message
var numeric_ip = /\w{3,16}:\/+(\S+@)?(\d+|0[xX][0-9A-Fa-f]+)\.(\d+|0[xX][0-9A-Fa-f]+)\.(\d+|0[xX][0-9A-Fa-f]+)\.(\d+|0[xX][0-9A-Fa-f]+)/gi;
var schemeless = /(?:%(?:25)?(?:2F|3D|40))?((?:www\.)?[a-zA-Z0-9][a-zA-Z0-9\-.]{0,250}\.(?:aero|arpa|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|xxx|[a-zA-Z]{2}))(?!\w)/gi;
var schemed    = /(\w{3,16}:\/+(?:\S+@)?([a-zA-Z0-9][a-zA-Z0-9\-.]+\.(?:aero|arpa|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|xxx|[a-zA-Z]{2})))(?!\w)/gi;

var lists;
var zones;
var excludes = {};

function check_excludes_list(host) {
    host = host.toLowerCase().split('.').reverse();
    for (var i=0; i < host.length; i++) {
        var check;
        if (i === 0) {
            check = host[i];
        }
        else {
            check = [ host[i], check ].join('.');
        }
        if (excludes[check]) {
            return true;
        }
    }
    return false;
}

exports.register = function() {
    // Override regexps if top_level_tlds file is present
    if (!net_utils.top_level_tlds) return;
    if (!Object.keys(net_utils.top_level_tlds).length) return;

    this.logdebug('Building new regexps from TLD file');
    var re_schemeless = '(?:%(?:25)?(?:2F|3D|40))?((?:www\\.)?[a-zA-Z0-9][a-zA-Z0-9\\-.]{0,250}\\.(?:' +
        Object.keys(net_utils.top_level_tlds).join('|') + '))(?!\\w)';
    schemeless = new RegExp(re_schemeless, 'gi');
    var re_schemed = '(\\w{3,16}:\\/+(?:\\S+@)?([a-zA-Z0-9][a-zA-Z0-9\\-.]+\\.(?:' +
        Object.keys(net_utils.top_level_tlds).join('|') + ')))(?!\\w)';
    schemed = new RegExp(re_schemed, 'gi');
};

exports.load_uri_config = function (next) {
    lists = this.config.get('data.uribl.ini');
    zones = Object.keys(lists);
    if (!zones || zones.length <= 1) {
        this.logerr('aborting: no zones configured');
        return next();
    }
    // Load excludes
    this.config.get('data.uribl.excludes', 'list').forEach(function (domain) {
        excludes[domain.toLowerCase()] = 1;
    });
    // Set defaults
    if (lists.main && !lists.main.max_uris_per_list) {
        lists.main.max_uris_per_list = 20;
    }
};

exports.do_lookups = function (connection, next, hosts, type) {
    var plugin = this;

    // Store the results in the correct place based on the lookup type
    var results = connection.results;
    if (connection.transaction) {
        results = connection.transaction.results;
    }

    if (typeof hosts === 'string') {
        hosts = [ hosts ];
    }
    if (!hosts || !hosts.length) {
        connection.logdebug(plugin, '(' + type + ') no items found for lookup');
        results.add(plugin, {skip: type});
        return next();
    }
    connection.logdebug(plugin, '(' + type + ') found ' + hosts.length + ' items for lookup');
    utils.shuffle(hosts);

    var j;
    var queries = {};
    for (var i=0; i < hosts.length; i++) {
        var host = hosts[i].toLowerCase();
        connection.logdebug(plugin, '(' + type + ') checking: ' + host);
        // Make sure we have a valid TLD
        if (!isIPv4(host) && !net_utils.top_level_tlds[(host.split('.').reverse())[0]]) {
            continue;
        }
        // Check the exclusion list
        if (check_excludes_list(host)) {
            results.add(plugin, {skip: 'excluded domain:' + host });
            continue;
        }
        // Loop through the zones
        for (j=0; j < zones.length; j++) {
            var zone = zones[j];
            if (zone === 'main') continue;  // skip config
            if (!lists[zone] || (lists[zone] && !/^(?:1|true|yes|enabled|on)$/i.test(lists[zone][type]))) {
                results.add(plugin, {skip: type + ' unsupported for ' + zone });
                continue;
            }
            // Convert in-addr.arpa into bare IPv4 lookup
            var arpa = host.split(/\./).reverse();
            if (arpa.shift() === 'arpa' && arpa.shift() === 'in-addr') {
                if (arpa.length < 4) continue; // Only full IP addresses
                host = arpa.join('.');
            }
            var lookup;
            // Handle zones that do not allow IP queries (e.g. Spamhaus DBL)
            if (isIPv4(host)) {
                if (/^(?:1|true|yes|enabled|on)$/i.test(lists[zone].no_ip_lookups)) {
                    results.add(plugin, {skip: 'IP (' + host + ') not supported for ' + zone });
                    continue;
                }
                // Skip any private IPs
                if (net_utils.is_private_ip(host)) {
                    results.add(plugin, {skip: 'private IP' });
                    continue;
                }
                // Reverse IP for lookup
                lookup = host.split(/\./).reverse().join('.');
            }
            // Handle zones that require host to be stripped to a domain boundary
            else if (/^(?:1|true|yes|enabled|on)$/i.test(lists[zone].strip_to_domain)) {
                lookup = (net_utils.split_hostname(host, 3))[1];
            }
            // Anything else..
            else {
                lookup = host;
            }
            if (!lookup) continue;
            if (!queries[zone]) queries[zone] = {};
            if (Object.keys(queries[zone]).length > lists.main.max_uris_per_list) {
                connection.logwarn(plugin, 'discarding lookup ' + lookup + ' for zone ' +
                              zone + ' maximum query limit reached');
                results.add(plugin, {skip: 'max query limit for ' + zone });
                continue;
            }
            queries[zone][lookup] = 1;
        }
    }

    // Flatten object into array for easier querying
    var queries_to_run = [];
    for (j=0; j < Object.keys(queries).length; j++) {
        for (var k=0; k < Object.keys(queries[Object.keys(queries)[j]]).length; k++) {
            // host/domain, zone
            queries_to_run.push( [ Object.keys(queries[Object.keys(queries)[j]])[k], Object.keys(queries)[j] ] );
        }
    }
    // Randomize the order a bit
    utils.shuffle(queries_to_run);

    if (!queries_to_run.length) {
        results.add(plugin, {skip: type + ' (no queries)' });
        return next();
    }

    // Perform the lookups
    var pending_queries = 0;
    var called_next = false;
    var timer;
    function call_next (code, msg) {
        if (timer) clearTimeout(timer);
        if (called_next) return;
        called_next = true;
        next(code, msg);
    }

    timer = setTimeout(function () {
        connection.logdebug(plugin, 'timeout');
        results.add(plugin, {err: type + ' timeout' });
        call_next();
    }, ((lists.main && lists.main.timeout) ?
        lists.main.timeout : 30) * 1000);

    queries_to_run.forEach(function (query) {
        var lookup = query.join('.');
        // Add root dot if necessary
        if (lookup[lookup.length-1] !== '.') {
            lookup = lookup + '.';
        }
        pending_queries++;
        dns.resolve4(lookup, function(err, addrs) {
            pending_queries--;
            connection.logdebug(plugin, lookup + ' => ' + ((err) ? err : addrs.join(', ')));

            if (!err) {
                var skip = false;
                var do_reject = function (msg) {
                    if (!skip && !called_next) {
                        if (!msg) {
                            msg = query[0] + ' blacklisted in ' + query[1];
                        }
                        // Check for custom message
                        if (lists[query[1]] && lists[query[1]].custom_msg) {
                            msg = lists[query[1]].custom_msg.replace(/\{uri\}/g, query[0]).replace(/\{zone\}/g, query[1]);
                        }
                        results.add(plugin, {fail:
                            [type, query[0], query[1]].join('/') });
                        call_next(DENY, msg);
                    }
                };
                // Optionally validate first result against a regexp
                if (lists[query[1]] && lists[query[1]].validate) {
                    var re = new RegExp(lists[query[1]].validate);
                    if (!re.test(addrs[0])) {
                        connection.logdebug(plugin, 'ignoring result (' + addrs[0] + ') for: ' +
                                lookup + ' as it did not match validation rule');
                        skip = true;
                    }
                }
                // Check for optional bitmask
                if (lists[query[1]] && lists[query[1]].bitmask) {
                    // A bitmask zone should only return a single result
                    // We only support a bitmask of up to 128 in a single octet
                    var last_octet = Number((addrs[0].split('.'))[3]);
                    var bitmask = Number(lists[query[1]].bitmask);
                    if ((last_octet & bitmask) > 0) {
                        connection.loginfo(plugin, 'found ' + query[0] + ' in zone ' + query[1] +
                            ' (' + addrs.join(',') + '; bitmask=' + bitmask + ')');
                        do_reject();
                    } else {
                        connection.logdebug(plugin, 'ignoring result (' + addrs[0] + ') for: ' +
                                lookup + ' as the bitmask did not match');
                        skip = true;
                    }
                }
                else {
                    connection.loginfo(plugin, 'found ' + query[0] + ' in zone ' + query[1] +
                        ' (' + addrs.join(',') + ')');
                    do_reject();
                }
            }

            if (pending_queries === 0) {
                results.add(plugin, {pass: type});
                return call_next();
            }
        });
    });

    if (pending_queries === 0) {
        results.add(plugin, {pass: type});
        return call_next();
    }
};

exports.hook_lookup_rdns = function (next, connection) {
    this.load_uri_config(next);
    var plugin = this;
    dns.reverse(connection.remote_ip, function (err, rdns) {
        if (err) {
            if (err.code) {
                if (err.code === dns.NXDOMAIN) return next();
                if (err.code === 'ENOTFOUND') return next();
            }
            connection.results.add(plugin, {err: err });
            return next();
        }
        plugin.do_lookups(connection, next, rdns, 'rdns');
    });
};

exports.hook_ehlo = function (next, connection, helo) {
    this.load_uri_config(next);
    // Handle IP literals
    var literal;
    if ((literal = /^\[(\d+\.\d+\.\d+\.\d+)\]$/.exec(helo))) {
        this.do_lookups(connection, next, literal[1], 'helo');
    } else {
        this.do_lookups(connection, next, helo, 'helo');
    }
};
exports.hook_helo = exports.hook_ehlo;

exports.hook_mail = function (next, connection, params) {
    this.load_uri_config(next);
    this.do_lookups(connection, next, params[0].host, 'envfrom');
};

exports.hook_data = function (next, connection) {
    // enable mail body parsing
    connection.transaction.parse_body = 1;
    return next();
};

exports.hook_data_post = function (next, connection) {
    this.load_uri_config(next);
    var email_re = /<?[^@]+@([^> ]+)>?/;
    var plugin = this;
    var trans = connection.transaction;

    // From header
    var do_from_header = function (cb) {
        var from = trans.header.get('from');
        var fmatch;
        if (fmatch = email_re.exec(from)) {
            return plugin.do_lookups(connection, cb, fmatch[1], 'from');
        }
        cb();
    };

    // Reply-To header
    var do_replyto_header = function (cb) {
        var replyto = trans.header.get('reply-to');
        var rmatch = email_re.exec(replyto);
        if (rmatch) {
            return plugin.do_lookups(connection, cb, rmatch[1], 'replyto');
        }
        cb();
    };

    // Message-Id header
    var do_msgid_header = function (cb) {
        var msgid = trans.header.get('message-id');
        var mmatch = /@([^>]+)>/.exec(msgid);
        if (mmatch) {
            return plugin.do_lookups(connection, cb, mmatch[1], 'msgid');
        }
         cb();
    };

    // Body
    var do_body = function (cb) {
        var urls = {};
        extract_urls(urls, trans.body, connection, plugin);
        return plugin.do_lookups(connection, cb, Object.keys(urls), 'body');
    };

    var chain = [ do_from_header, do_replyto_header, do_msgid_header, do_body ];
    var chain_caller = function (code, msg) {
        if (code) {
            return next(code, msg);
        }
        if (!chain.length) {
            return next();
        }
        var next_in_chain = chain.shift();
        next_in_chain(chain_caller);
    };
    chain_caller();
};

function extract_urls (urls, body, connection, self) {
    // extract from body.bodytext
    var match;
    if (!body || !body.bodytext) { return; }

    var uri;
    // extract numeric URIs
    while (match = numeric_ip.exec(body.bodytext)) {
        try {
            uri = url.parse(match[0]);
            // Don't reverse the IPs here; we do it in the lookup
            urls[uri.hostname] = uri;
        }
        catch (error) {
            connection.logerror(self, 'parse error: ' + match[0] +
                                      ' ' + error.message);
        }
    }

    // match plain hostname.tld
    while (match = schemeless.exec(body.bodytext)) {
        try {
            uri = url.parse('http://' + match[1]);
            urls[uri.hostname] = uri;
        }
        catch (error) {
            connection.logerror(self, 'parse error: ' + match[1] +
                                      ' ' + error.message);
        }
    }

    // match scheme:// URI
    while (match = schemed.exec(body.bodytext)) {
        try {
            uri = url.parse(match[1]);
            urls[uri.hostname] = uri;
        }
        catch (error) {
            connection.logerror(self, 'parse error: ' + match[1] +
                                      ' ' + error.message);
        }
    }

    // TODO: URIHASH
    // TODO: MAILHASH

    for (var i=0,l=body.children.length; i < l; i++) {
        extract_urls(urls, body.children[i], connection, self);
    }
}
<commit_msg>uribl: call_next always run clearTimeout<commit_after>'use strict';
// Look up URLs in SURBL

var url       = require('url');
var dns       = require('dns');
var isIPv4    = require('net').isIPv4;
var net_utils = require('./net_utils');
var utils     = require('./utils');

// Default regexps to extract the URIs from the message
var numeric_ip = /\w{3,16}:\/+(\S+@)?(\d+|0[xX][0-9A-Fa-f]+)\.(\d+|0[xX][0-9A-Fa-f]+)\.(\d+|0[xX][0-9A-Fa-f]+)\.(\d+|0[xX][0-9A-Fa-f]+)/gi;
var schemeless = /(?:%(?:25)?(?:2F|3D|40))?((?:www\.)?[a-zA-Z0-9][a-zA-Z0-9\-.]{0,250}\.(?:aero|arpa|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|xxx|[a-zA-Z]{2}))(?!\w)/gi;
var schemed    = /(\w{3,16}:\/+(?:\S+@)?([a-zA-Z0-9][a-zA-Z0-9\-.]+\.(?:aero|arpa|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|xxx|[a-zA-Z]{2})))(?!\w)/gi;

var lists;
var zones;
var excludes = {};

function check_excludes_list (host) {
    host = host.toLowerCase().split('.').reverse();
    for (var i=0; i < host.length; i++) {
        var check;
        if (i === 0) {
            check = host[i];
        }
        else {
            check = [ host[i], check ].join('.');
        }
        if (excludes[check]) {
            return true;
        }
    }
    return false;
}

exports.register = function () {
    // Override regexps if top_level_tlds file is present
    if (!net_utils.top_level_tlds) return;
    if (!Object.keys(net_utils.top_level_tlds).length) return;

    this.logdebug('Building new regexps from TLD file');
    var re_schemeless = '(?:%(?:25)?(?:2F|3D|40))?((?:www\\.)?[a-zA-Z0-9][a-zA-Z0-9\\-.]{0,250}\\.(?:' +
        Object.keys(net_utils.top_level_tlds).join('|') + '))(?!\\w)';
    schemeless = new RegExp(re_schemeless, 'gi');
    var re_schemed = '(\\w{3,16}:\\/+(?:\\S+@)?([a-zA-Z0-9][a-zA-Z0-9\\-.]+\\.(?:' +
        Object.keys(net_utils.top_level_tlds).join('|') + ')))(?!\\w)';
    schemed = new RegExp(re_schemed, 'gi');
};

exports.load_uri_config = function (next) {
    lists = this.config.get('data.uribl.ini');
    zones = Object.keys(lists);
    if (!zones || zones.length <= 1) {
        this.logerr('aborting: no zones configured');
        return next();
    }
    // Load excludes
    this.config.get('data.uribl.excludes', 'list').forEach(function (domain) {
        excludes[domain.toLowerCase()] = 1;
    });
    // Set defaults
    if (lists.main && !lists.main.max_uris_per_list) {
        lists.main.max_uris_per_list = 20;
    }
};

exports.do_lookups = function (connection, next, hosts, type) {
    var plugin = this;

    // Store the results in the correct place based on the lookup type
    var results = connection.results;
    if (connection.transaction) {
        results = connection.transaction.results;
    }

    if (typeof hosts === 'string') {
        hosts = [ hosts ];
    }
    if (!hosts || !hosts.length) {
        connection.logdebug(plugin, '(' + type + ') no items found for lookup');
        results.add(plugin, {skip: type});
        return next();
    }
    connection.logdebug(plugin, '(' + type + ') found ' + hosts.length + ' items for lookup');
    utils.shuffle(hosts);

    var j;
    var queries = {};
    for (var i=0; i < hosts.length; i++) {
        var host = hosts[i].toLowerCase();
        connection.logdebug(plugin, '(' + type + ') checking: ' + host);
        // Make sure we have a valid TLD
        if (!isIPv4(host) && !net_utils.top_level_tlds[(host.split('.').reverse())[0]]) {
            continue;
        }
        // Check the exclusion list
        if (check_excludes_list(host)) {
            results.add(plugin, {skip: 'excluded domain:' + host });
            continue;
        }
        // Loop through the zones
        for (j=0; j < zones.length; j++) {
            var zone = zones[j];
            if (zone === 'main') continue;  // skip config
            if (!lists[zone] || (lists[zone] && !/^(?:1|true|yes|enabled|on)$/i.test(lists[zone][type]))) {
                results.add(plugin, {skip: type + ' unsupported for ' + zone });
                continue;
            }
            // Convert in-addr.arpa into bare IPv4 lookup
            var arpa = host.split(/\./).reverse();
            if (arpa.shift() === 'arpa' && arpa.shift() === 'in-addr') {
                if (arpa.length < 4) continue; // Only full IP addresses
                host = arpa.join('.');
            }
            var lookup;
            // Handle zones that do not allow IP queries (e.g. Spamhaus DBL)
            if (isIPv4(host)) {
                if (/^(?:1|true|yes|enabled|on)$/i.test(lists[zone].no_ip_lookups)) {
                    results.add(plugin, {skip: 'IP (' + host + ') not supported for ' + zone });
                    continue;
                }
                // Skip any private IPs
                if (net_utils.is_private_ip(host)) {
                    results.add(plugin, {skip: 'private IP' });
                    continue;
                }
                // Reverse IP for lookup
                lookup = host.split(/\./).reverse().join('.');
            }
            // Handle zones that require host to be stripped to a domain boundary
            else if (/^(?:1|true|yes|enabled|on)$/i.test(lists[zone].strip_to_domain)) {
                lookup = (net_utils.split_hostname(host, 3))[1];
            }
            // Anything else..
            else {
                lookup = host;
            }
            if (!lookup) continue;
            if (!queries[zone]) queries[zone] = {};
            if (Object.keys(queries[zone]).length > lists.main.max_uris_per_list) {
                connection.logwarn(plugin, 'discarding lookup ' + lookup + ' for zone ' +
                              zone + ' maximum query limit reached');
                results.add(plugin, {skip: 'max query limit for ' + zone });
                continue;
            }
            queries[zone][lookup] = 1;
        }
    }

    // Flatten object into array for easier querying
    var queries_to_run = [];
    for (j=0; j < Object.keys(queries).length; j++) {
        for (var k=0; k < Object.keys(queries[Object.keys(queries)[j]]).length; k++) {
            // host/domain, zone
            queries_to_run.push( [ Object.keys(queries[Object.keys(queries)[j]])[k], Object.keys(queries)[j] ] );
        }
    }

    if (!queries_to_run.length) {
        results.add(plugin, {skip: type + ' (no queries)' });
        return next();
    }

    utils.shuffle(queries_to_run); // Randomize the order

    // Perform the lookups
    var pending_queries = 0;
    var called_next = false;
    var timer;
    function call_next (code, msg) {
        clearTimeout(timer);
        if (called_next) return;
        called_next = true;
        next(code, msg);
    }

    timer = setTimeout(function () {
        connection.logdebug(plugin, 'timeout');
        results.add(plugin, {err: type + ' timeout' });
        call_next();
    }, ((lists.main && lists.main.timeout) ? lists.main.timeout : 30) * 1000);

    function conclude_if_no_pending () {
        if (pending_queries !== 0) return;
        results.add(plugin, {pass: type});
        call_next();
    }

    queries_to_run.forEach(function (query) {
        var lookup = query.join('.');
        // Add root dot if necessary
        if (lookup[lookup.length-1] !== '.') {
            lookup = lookup + '.';
        }
        pending_queries++;
        dns.resolve4(lookup, function (err, addrs) {
            pending_queries--;
            connection.logdebug(plugin, lookup + ' => ' + ((err) ? err : addrs.join(', ')));

            if (err) return conclude_if_no_pending();

            var skip = false;
            var do_reject = function (msg) {
                if (skip) return;
                if (called_next) return;
                if (!msg) {
                    msg = query[0] + ' blacklisted in ' + query[1];
                }
                // Check for custom message
                if (lists[query[1]] && lists[query[1]].custom_msg) {
                    msg = lists[query[1]].custom_msg
                        .replace(/\{uri\}/g,  query[0])
                        .replace(/\{zone\}/g, query[1]);
                }
                results.add(plugin,
                    {fail: [type, query[0], query[1]].join('/') });
                call_next(DENY, msg);
            };
            // Optionally validate first result against a regexp
            if (lists[query[1]] && lists[query[1]].validate) {
                var re = new RegExp(lists[query[1]].validate);
                if (!re.test(addrs[0])) {
                    connection.logdebug(plugin, 'ignoring result (' + addrs[0] + ') for: ' +
                            lookup + ' as it did not match validation rule');
                    skip = true;
                }
            }
            // Check for optional bitmask
            if (lists[query[1]] && lists[query[1]].bitmask) {
                // A bitmask zone should only return a single result
                // We only support a bitmask of up to 128 in a single octet
                var last_octet = Number((addrs[0].split('.'))[3]);
                var bitmask = Number(lists[query[1]].bitmask);
                if ((last_octet & bitmask) > 0) {
                    connection.loginfo(plugin, 'found ' + query[0] + ' in zone ' + query[1] +
                        ' (' + addrs.join(',') + '; bitmask=' + bitmask + ')');
                    do_reject();
                } else {
                    connection.logdebug(plugin, 'ignoring result (' + addrs[0] + ') for: ' +
                            lookup + ' as the bitmask did not match');
                    skip = true;
                }
            }
            else {
                connection.loginfo(plugin, 'found ' + query[0] + ' in zone ' + query[1] +
                    ' (' + addrs.join(',') + ')');
                do_reject();
            }

            conclude_if_no_pending();
        });
    });

    conclude_if_no_pending();
};

exports.hook_lookup_rdns = function (next, connection) {
    this.load_uri_config(next);
    var plugin = this;
    dns.reverse(connection.remote_ip, function (err, rdns) {
        if (err) {
            if (err.code) {
                if (err.code === dns.NXDOMAIN) return next();
                if (err.code === 'ENOTFOUND') return next();
            }
            connection.results.add(plugin, {err: err });
            return next();
        }
        plugin.do_lookups(connection, next, rdns, 'rdns');
    });
};

exports.hook_ehlo = function (next, connection, helo) {
    this.load_uri_config(next);
    // Handle IP literals
    var literal;
    if ((literal = /^\[(\d+\.\d+\.\d+\.\d+)\]$/.exec(helo))) {
        this.do_lookups(connection, next, literal[1], 'helo');
    } else {
        this.do_lookups(connection, next, helo, 'helo');
    }
};
exports.hook_helo = exports.hook_ehlo;

exports.hook_mail = function (next, connection, params) {
    this.load_uri_config(next);
    this.do_lookups(connection, next, params[0].host, 'envfrom');
};

exports.hook_data = function (next, connection) {
    // enable mail body parsing
    connection.transaction.parse_body = 1;
    return next();
};

exports.hook_data_post = function (next, connection) {
    this.load_uri_config(next);
    var email_re = /<?[^@]+@([^> ]+)>?/;
    var plugin = this;
    var trans = connection.transaction;

    // From header
    var do_from_header = function (cb) {
        var from = trans.header.get('from');
        var fmatch;
        if (fmatch = email_re.exec(from)) {
            return plugin.do_lookups(connection, cb, fmatch[1], 'from');
        }
        cb();
    };

    // Reply-To header
    var do_replyto_header = function (cb) {
        var replyto = trans.header.get('reply-to');
        var rmatch = email_re.exec(replyto);
        if (rmatch) {
            return plugin.do_lookups(connection, cb, rmatch[1], 'replyto');
        }
        cb();
    };

    // Message-Id header
    var do_msgid_header = function (cb) {
        var msgid = trans.header.get('message-id');
        var mmatch = /@([^>]+)>/.exec(msgid);
        if (mmatch) {
            return plugin.do_lookups(connection, cb, mmatch[1], 'msgid');
        }
         cb();
    };

    // Body
    var do_body = function (cb) {
        var urls = {};
        extract_urls(urls, trans.body, connection, plugin);
        return plugin.do_lookups(connection, cb, Object.keys(urls), 'body');
    };

    var chain = [ do_from_header, do_replyto_header, do_msgid_header, do_body ];
    var chain_caller = function (code, msg) {
        if (code) {
            return next(code, msg);
        }
        if (!chain.length) {
            return next();
        }
        var next_in_chain = chain.shift();
        next_in_chain(chain_caller);
    };
    chain_caller();
};

function extract_urls (urls, body, connection, self) {
    // extract from body.bodytext
    var match;
    if (!body || !body.bodytext) { return; }

    var uri;
    // extract numeric URIs
    while (match = numeric_ip.exec(body.bodytext)) {
        try {
            uri = url.parse(match[0]);
            // Don't reverse the IPs here; we do it in the lookup
            urls[uri.hostname] = uri;
        }
        catch (error) {
            connection.logerror(self, 'parse error: ' + match[0] +
                                      ' ' + error.message);
        }
    }

    // match plain hostname.tld
    while (match = schemeless.exec(body.bodytext)) {
        try {
            uri = url.parse('http://' + match[1]);
            urls[uri.hostname] = uri;
        }
        catch (error) {
            connection.logerror(self, 'parse error: ' + match[1] +
                                      ' ' + error.message);
        }
    }

    // match scheme:// URI
    while (match = schemed.exec(body.bodytext)) {
        try {
            uri = url.parse(match[1]);
            urls[uri.hostname] = uri;
        }
        catch (error) {
            connection.logerror(self, 'parse error: ' + match[1] +
                                      ' ' + error.message);
        }
    }

    // TODO: URIHASH
    // TODO: MAILHASH

    for (var i=0,l=body.children.length; i < l; i++) {
        extract_urls(urls, body.children[i], connection, self);
    }
}
<|endoftext|>


<commit_before>
function ProjectsViewCtrl($scope, $routeParams, $location, $http, $modal, $timeout, session) {
    $scope.project = {};

    $http.get('/api/1/projects/' + $routeParams.slug).then(function(res) {
        $scope.project = res.data;
    });
}

ProjectsViewCtrl.$inject = ['$scope', '$routeParams', '$location', '$http', '$modal', '$timeout', 'session'];

function ProjectsNewCtrl($scope, $routeParams, $modalInstance, $location, $http, session) {
    $scope.project = {};

    $scope.cancel = function() {
        $modalInstance.dismiss('cancel');
    };

    $scope.create = function(form) {
        var res = $http.post('/api/1/projects', $scope.project);
        res.success(function(data) {
            $location.path('/p/' + data.slug);
            $modalInstance.dismiss('ok');
        });
        res.error(grano.handleFormError(form));
    };
}

ProjectsNewCtrl.$inject = ['$scope', '$routeParams', '$modalInstance', '$location', '$http', 'session'];

function ProjectsEditCtrl($scope, $route, $routeParams, $location, $http, core) {
    $scope.navSection = 'settings';
    $scope.project = {};

    $http.get('/api/1/projects/' + $routeParams.slug).then(function(res) {
        $scope.project = res.data;
        core.setTitle($scope.project.label);
    });

    $scope.update = function(form) {
        var res = $http.post('/api/1/projects/' + $scope.project.slug, $scope.project);
        res.success(function(data) {
            $location.path('/p/' + $scope.project.slug);
        });
        res.error(grano.handleFormError(form));
    };
}

ProjectsEditCtrl.$inject = ['$scope', '$route', '$routeParams', '$location', '$http', 'core'];
<commit_msg>Reload after saving. <commit_after>
function ProjectsViewCtrl($scope, $routeParams, $location, $http, $modal, $timeout, session) {
    $scope.project = {};

    $http.get('/api/1/projects/' + $routeParams.slug).then(function(res) {
        $scope.project = res.data;
    });
}

ProjectsViewCtrl.$inject = ['$scope', '$routeParams', '$location', '$http', '$modal', '$timeout', 'session'];

function ProjectsNewCtrl($scope, $routeParams, $modalInstance, $location, $http, session) {
    $scope.project = {};

    $scope.cancel = function() {
        $modalInstance.dismiss('cancel');
    };

    $scope.create = function(form) {
        var res = $http.post('/api/1/projects', $scope.project);
        res.success(function(data) {
            $location.path('/p/' + data.slug);
            $modalInstance.dismiss('ok');
        });
        res.error(grano.handleFormError(form));
    };
}

ProjectsNewCtrl.$inject = ['$scope', '$routeParams', '$modalInstance', '$location', '$http', 'session'];

function ProjectsEditCtrl($scope, $route, $routeParams, $location, $http, core) {
    $scope.navSection = 'settings';
    $scope.project = {};

    $http.get('/api/1/projects/' + $routeParams.slug).then(function(res) {
        $scope.project = res.data;
        core.setTitle($scope.project.label);
    });

    $scope.update = function(form) {
        var res = $http.post('/api/1/projects/' + $scope.project.slug, $scope.project);
        res.success(function(data) {
            $route.reload();
        });
        res.error(grano.handleFormError(form));
    };
}

ProjectsEditCtrl.$inject = ['$scope', '$route', '$routeParams', '$location', '$http', 'core'];
<|endoftext|>


<commit_before>define([
	'jquery' //requires jquery
], function( $ ) {

	var FloatingWarning = function(warning) {
		this.warning = $(warning); // the floating warning div
		this.init();
	};

	FloatingWarning.prototype = {
		init: function(){
			var self = this;
			$(document).ready(function () {
				// Clone warning to top of document, without fixed positioning,
				// to force correct spacing of header.
				self.warning.clone().prependTo('body').css('position', 'relative');

				// Here we are doing something really annoying, we catch clicks on
				// internal anchors and scroll to the right place of the page.
				// This is only needed because we want to show the developement version
				// warning on top.
				// First handle the case in which someone clicks on a link
				 $('a[href*=#]').click(function () {
					// check to see if this is an internal link, sigh.
					if (location.pathname.replace(/^\//, '') == this.pathname.replace(/^\//, '') &&
						location.hostname == this.hostname) {
						// get the link target, use the weird id= query since it escapes dots
						var target = $("[id='" + this.hash.slice(1) + "']");
						// calculate the offset
						var targetOffset = target.offset().top - self.warning.height() - 20;
						// scroll to the place, there is probably a better way
						setTimeout(function () {
						  $('html,body').scrollTop(targetOffset);
						}, 50);
					}
				});
				// this is the janky thing, there is probably a better way
				setTimeout(function () {
					// is there a hash in the current window's location?
					if (window.location.hash) {
						// again, get the target
						var target = $("[id='" + window.location.hash.slice(1) + "']");
						// calculate the offset
						var targetOffset = target.offset().top - self.warning.height() - 20;
						// scroll to the place
						$('html,body').scrollTop(targetOffset);
					}
				// use a delay that should work on all modern computers. should, not will
				}, 50);
			});
		}
	};

	// Export a single instance of our module:
	return new FloatingWarning('.doc-floating-warning');
});
<commit_msg>Fix #220 - Again, this time by using hashchange event.<commit_after>define([
	'jquery' //requires jquery
], function( $ ) {

	var FloatingWarning = function(warning) {
		this.warning = $(warning); // the floating warning div
		this.init();
	};

	FloatingWarning.prototype = {
		init: function(){
			var self = this;
			$(document).ready(function () {
				// Clone warning to top of document, without fixed positioning,
				// to force correct spacing of header.
				self.warning.clone().prependTo('body').css('position', 'relative');

				setTimeout(function () {
					self.scroll(window.location.hash, self.warning);
				}, 50); // use a delay that should work on all modern computers. should, not will

				// use something nicer
				$(window).on('hashchange', function() {
					self.scroll(window.location.hash, self.warning);
				});
			});
		},
		scroll: function(hash, warning) {
			// is there a hash in the current window's location?
			if (hash) {
				// again, get the target
				var target = $("[id='" + hash.slice(1) + "']");
				if (target.length) {
					// calculate the offset
					var targetOffset = target.offset().top - warning.height() - 20;
					// scroll to the place
					$('html,body').scrollTop(targetOffset);
				}
			}
		}
	};

	// Export a single instance of our module:
	return new FloatingWarning('.doc-floating-warning');
});
<|endoftext|>


<commit_before>const ELECTRON_APP_VERSION = "4.0.0";
const ELECTRON_APP_URL_LINUX = "https://github.com/zulip/zulip-desktop/releases/download/v" + ELECTRON_APP_VERSION + "/Zulip-" + ELECTRON_APP_VERSION + "-x86_64.AppImage";
const ELECTRON_APP_URL_MAC = "https://github.com/zulip/zulip-desktop/releases/download/v" + ELECTRON_APP_VERSION + "/Zulip-" + ELECTRON_APP_VERSION + ".dmg";
const ELECTRON_APP_URL_WINDOWS = "https://github.com/zulip/zulip-desktop/releases/download/v" + ELECTRON_APP_VERSION + "/Zulip-Web-Setup-" + ELECTRON_APP_VERSION + ".exe";

import { detect_user_os } from './tabbed-instructions.js';
import render_tabs from './team.js';

export function path_parts() {
    return window.location.pathname.split('/').filter(function (chunk) {
        return chunk !== '';
    });
}

const hello_events = function () {
    let counter = 0;
    $(window).scroll(function () {
        if (counter % 2 === 0) {
            $(".screen.hero-screen .message-feed").css("transform", "translateY(-" + $(this).scrollTop() / 5 + "px)");
        }
        counter += 1;
    });

    $(".footer").addClass("hello");
};

const apps_events = function () {
    const info = {
        windows: {
            image: "/static/images/landing-page/microsoft.png",
            alt: "Windows",
            description: "Zulip for Windows is even better than Zulip on the web, with a cleaner look, tray integration, native notifications, and support for multiple Zulip accounts.",
            link: ELECTRON_APP_URL_WINDOWS,
            show_instructions: true,
            install_guide: "/help/desktop-app-install-guide",
        },
        mac: {
            image: "/static/images/landing-page/macbook.png",
            alt: "macOS",
            description: "Zulip on macOS is even better than Zulip on the web, with a cleaner look, tray integration, native notifications, and support for multiple Zulip accounts.",
            link: ELECTRON_APP_URL_MAC,
            show_instructions: true,
            install_guide: "/help/desktop-app-install-guide",
        },
        android: {
            image: "/static/images/app-screenshots/zulip-android.png",
            alt: "Android",
            description: "Zulip's native Android app makes it easy to keep up while on the go.",
            show_instructions: false,
            link: "https://play.google.com/store/apps/details?id=com.zulipmobile",
        },
        ios: {
            image: "/static/images/app-screenshots/zulip-iphone-rough.png",
            alt: "iOS",
            description: "Zulip's native iOS app makes it easy to keep up while on the go.",
            show_instructions: false,
            link: "https://itunes.apple.com/us/app/zulip/id1203036395",
        },
        linux: {
            image: "/static/images/landing-page/ubuntu.png",
            alt: "Linux",
            description: "Zulip on the Linux desktop is even better than Zulip on the web, with a cleaner look, tray integration, native notifications, and support for multiple Zulip accounts.",
            link: ELECTRON_APP_URL_LINUX,
            show_instructions: true,
            install_guide: "/help/desktop-app-install-guide",
        },
    };

    let version;

    function get_version_from_path() {
        let result;
        const parts = path_parts();

        Object.keys(info).forEach(function (version) {
            if (parts.includes(version)) {
                result = version;
            }
        });

        result = result || detect_user_os();
        return result;
    }

    function get_path_from_version() {
        return '/apps/' + version;
    }

    function update_path() {
        const next_path = get_path_from_version();
        history.pushState(version, '', next_path);
    }

    const update_page = function () {
        const $download_instructions = $(".download-instructions");
        const $third_party_apps = $("#third-party-apps");
        const $download_android_apk = $("#download-android-apk");
        const version_info = info[version];

        $(".info .platform").text(version_info.alt);
        $(".info .description").text(version_info.description);
        $(".info .link").attr("href", version_info.link);
        $(".image img").attr("src", version_info.image);
        $download_instructions.find("a").attr("href", version_info.install_guide);

        $download_instructions.toggle(version_info.show_instructions);
        $third_party_apps.toggle(version === "mac" ||
                                 version === "windows" ||
                                 version === "linux");
        $download_android_apk.toggle(version === "android");
    };

    $(window).on('popstate', function () {
        version = get_version_from_path();
        update_page();
        $("body").animate({ scrollTop: 0 }, 200);
    });

    $(".apps a .icon").click(function (e) {
        const next_version = $(e.target).closest('a')
            .attr('href')
            .replace('/apps/', '');
        version = next_version;

        update_path();
        update_page();
        $("body").animate({ scrollTop: 0 }, 200);

        return false;
    });

    // init
    version = get_version_from_path();
    history.replaceState(version, '', get_path_from_version());
    update_page();
};

const events = function () {
    // get the location url like `zulipchat.com/features/`, cut off the trailing
    // `/` and then split by `/` to get ["zulipchat.com", "features"], then
    // pop the last element to get the current section (eg. `features`).
    const location = window.location.pathname.replace(/\/#*$/, "").split(/\//).pop();

    $("[data-on-page='" + location + "']").addClass("active");

    $("body").click(function (e) {
        const $e = $(e.target);

        if ($e.is("nav ul .exit")) {
            $("nav ul").removeClass("show");
        }

        if ($("nav ul.show") && !$e.closest("nav ul.show").length && !$e.is("nav ul.show")) {
            $("nav ul").removeClass("show");
        }
    });

    $(".hamburger").click(function (e) {
        $("nav ul").addClass("show");
        e.stopPropagation();
    });

    if (path_parts().includes("apps")) {
        apps_events();
    }

    if (path_parts().includes('hello')) {
        hello_events();
    }
};

// run this callback when the page is determined to have loaded.
const load = function () {

    // Initiate the bootstrap carousel logic
    $('.carousel').carousel({
        interval: false,
    });

    // Move to the next slide on clicking inside the carousel container
    $(".carousel-inner .item-container").click(function (e) {
        const get_tag_name = e.target.tagName.toLowerCase();
        const is_button = get_tag_name === "button";
        const is_link = get_tag_name === "a";
        const is_last_slide = $("#tour-carousel .carousel-inner .item:last-child").hasClass("active");

        // Do not trigger this event if user clicks on a button, link
        // or if it's the last slide
        const move_slide_forward = !is_button && !is_link && !is_last_slide;

        if (move_slide_forward) {
            $(this).closest('.carousel').carousel('next');
        }
    });

    $('.carousel').on('slid', function () {
        const $this = $(this);
        $this.find('.visibility-control').show();
        if ($this.find('.carousel-inner .item').first().hasClass('active')) {
            $this.find('.left.visibility-control').hide();
        } else if ($this.find('.carousel-inner .item').last().hasClass('active')) {
            $this.find('.right.visibility-control').hide();
        }
    });

    // Set up events / categories / search
    events();
};

if (document.readyState === "complete") {
    load();
} else {
    $(load);
}

$(function () {
    if (window.location.pathname === '/team/') {
        render_tabs();
    }
});

// Prevent Firefox from bfcaching the page.
// According to https://developer.mozilla.org/en-US/docs/DOM/window.onunload
// Using this event handler in your page prevents Firefox from caching the
// page in the in-memory bfcache (backward/forward cache).
$(window).on('unload', function () {
    $(window).unbind('unload');
});
<commit_msg>desktop: Update desktop app to v4.0.3.<commit_after>const ELECTRON_APP_VERSION = "4.0.3";
const ELECTRON_APP_URL_LINUX = "https://github.com/zulip/zulip-desktop/releases/download/v" + ELECTRON_APP_VERSION + "/Zulip-" + ELECTRON_APP_VERSION + "-x86_64.AppImage";
const ELECTRON_APP_URL_MAC = "https://github.com/zulip/zulip-desktop/releases/download/v" + ELECTRON_APP_VERSION + "/Zulip-" + ELECTRON_APP_VERSION + ".dmg";
const ELECTRON_APP_URL_WINDOWS = "https://github.com/zulip/zulip-desktop/releases/download/v" + ELECTRON_APP_VERSION + "/Zulip-Web-Setup-" + ELECTRON_APP_VERSION + ".exe";

import { detect_user_os } from './tabbed-instructions.js';
import render_tabs from './team.js';

export function path_parts() {
    return window.location.pathname.split('/').filter(function (chunk) {
        return chunk !== '';
    });
}

const hello_events = function () {
    let counter = 0;
    $(window).scroll(function () {
        if (counter % 2 === 0) {
            $(".screen.hero-screen .message-feed").css("transform", "translateY(-" + $(this).scrollTop() / 5 + "px)");
        }
        counter += 1;
    });

    $(".footer").addClass("hello");
};

const apps_events = function () {
    const info = {
        windows: {
            image: "/static/images/landing-page/microsoft.png",
            alt: "Windows",
            description: "Zulip for Windows is even better than Zulip on the web, with a cleaner look, tray integration, native notifications, and support for multiple Zulip accounts.",
            link: ELECTRON_APP_URL_WINDOWS,
            show_instructions: true,
            install_guide: "/help/desktop-app-install-guide",
        },
        mac: {
            image: "/static/images/landing-page/macbook.png",
            alt: "macOS",
            description: "Zulip on macOS is even better than Zulip on the web, with a cleaner look, tray integration, native notifications, and support for multiple Zulip accounts.",
            link: ELECTRON_APP_URL_MAC,
            show_instructions: true,
            install_guide: "/help/desktop-app-install-guide",
        },
        android: {
            image: "/static/images/app-screenshots/zulip-android.png",
            alt: "Android",
            description: "Zulip's native Android app makes it easy to keep up while on the go.",
            show_instructions: false,
            link: "https://play.google.com/store/apps/details?id=com.zulipmobile",
        },
        ios: {
            image: "/static/images/app-screenshots/zulip-iphone-rough.png",
            alt: "iOS",
            description: "Zulip's native iOS app makes it easy to keep up while on the go.",
            show_instructions: false,
            link: "https://itunes.apple.com/us/app/zulip/id1203036395",
        },
        linux: {
            image: "/static/images/landing-page/ubuntu.png",
            alt: "Linux",
            description: "Zulip on the Linux desktop is even better than Zulip on the web, with a cleaner look, tray integration, native notifications, and support for multiple Zulip accounts.",
            link: ELECTRON_APP_URL_LINUX,
            show_instructions: true,
            install_guide: "/help/desktop-app-install-guide",
        },
    };

    let version;

    function get_version_from_path() {
        let result;
        const parts = path_parts();

        Object.keys(info).forEach(function (version) {
            if (parts.includes(version)) {
                result = version;
            }
        });

        result = result || detect_user_os();
        return result;
    }

    function get_path_from_version() {
        return '/apps/' + version;
    }

    function update_path() {
        const next_path = get_path_from_version();
        history.pushState(version, '', next_path);
    }

    const update_page = function () {
        const $download_instructions = $(".download-instructions");
        const $third_party_apps = $("#third-party-apps");
        const $download_android_apk = $("#download-android-apk");
        const version_info = info[version];

        $(".info .platform").text(version_info.alt);
        $(".info .description").text(version_info.description);
        $(".info .link").attr("href", version_info.link);
        $(".image img").attr("src", version_info.image);
        $download_instructions.find("a").attr("href", version_info.install_guide);

        $download_instructions.toggle(version_info.show_instructions);
        $third_party_apps.toggle(version === "mac" ||
                                 version === "windows" ||
                                 version === "linux");
        $download_android_apk.toggle(version === "android");
    };

    $(window).on('popstate', function () {
        version = get_version_from_path();
        update_page();
        $("body").animate({ scrollTop: 0 }, 200);
    });

    $(".apps a .icon").click(function (e) {
        const next_version = $(e.target).closest('a')
            .attr('href')
            .replace('/apps/', '');
        version = next_version;

        update_path();
        update_page();
        $("body").animate({ scrollTop: 0 }, 200);

        return false;
    });

    // init
    version = get_version_from_path();
    history.replaceState(version, '', get_path_from_version());
    update_page();
};

const events = function () {
    // get the location url like `zulipchat.com/features/`, cut off the trailing
    // `/` and then split by `/` to get ["zulipchat.com", "features"], then
    // pop the last element to get the current section (eg. `features`).
    const location = window.location.pathname.replace(/\/#*$/, "").split(/\//).pop();

    $("[data-on-page='" + location + "']").addClass("active");

    $("body").click(function (e) {
        const $e = $(e.target);

        if ($e.is("nav ul .exit")) {
            $("nav ul").removeClass("show");
        }

        if ($("nav ul.show") && !$e.closest("nav ul.show").length && !$e.is("nav ul.show")) {
            $("nav ul").removeClass("show");
        }
    });

    $(".hamburger").click(function (e) {
        $("nav ul").addClass("show");
        e.stopPropagation();
    });

    if (path_parts().includes("apps")) {
        apps_events();
    }

    if (path_parts().includes('hello')) {
        hello_events();
    }
};

// run this callback when the page is determined to have loaded.
const load = function () {

    // Initiate the bootstrap carousel logic
    $('.carousel').carousel({
        interval: false,
    });

    // Move to the next slide on clicking inside the carousel container
    $(".carousel-inner .item-container").click(function (e) {
        const get_tag_name = e.target.tagName.toLowerCase();
        const is_button = get_tag_name === "button";
        const is_link = get_tag_name === "a";
        const is_last_slide = $("#tour-carousel .carousel-inner .item:last-child").hasClass("active");

        // Do not trigger this event if user clicks on a button, link
        // or if it's the last slide
        const move_slide_forward = !is_button && !is_link && !is_last_slide;

        if (move_slide_forward) {
            $(this).closest('.carousel').carousel('next');
        }
    });

    $('.carousel').on('slid', function () {
        const $this = $(this);
        $this.find('.visibility-control').show();
        if ($this.find('.carousel-inner .item').first().hasClass('active')) {
            $this.find('.left.visibility-control').hide();
        } else if ($this.find('.carousel-inner .item').last().hasClass('active')) {
            $this.find('.right.visibility-control').hide();
        }
    });

    // Set up events / categories / search
    events();
};

if (document.readyState === "complete") {
    load();
} else {
    $(load);
}

$(function () {
    if (window.location.pathname === '/team/') {
        render_tabs();
    }
});

// Prevent Firefox from bfcaching the page.
// According to https://developer.mozilla.org/en-US/docs/DOM/window.onunload
// Using this event handler in your page prevents Firefox from caching the
// page in the in-memory bfcache (backward/forward cache).
$(window).on('unload', function () {
    $(window).unbind('unload');
});
<|endoftext|>


<commit_before>const bsgTable = document.querySelector("#services-bsg")
const bsgRows = bsgTable.querySelectorAll("tbody tr")
const bsgFilterInput = document.querySelector("#bsg-textfilter")
const bsgFilterBtns = document.querySelectorAll("[data-bsgfilterbtn]")
const bsgFilterSets = document.querySelectorAll("#bsg-filtersets fieldset")
const bsgFilterChecks = document.querySelectorAll(".checkitem")
const bsgClearBtn = document.querySelector("#bsg-clearfilters")
const bsgFilterCombo = document.querySelector("#bsg-filtercombo")
const bsgCutoff = document.querySelector("#bsg-cutoff")
const bsgCutoffBtn = bsgCutoff.querySelector("button")
let allFilters

// Initial cutoff
function bsgTableCutoff() {
  bsgRows.forEach((row, i) => {
    if (i > 5) {
      row.hidden = true
    }
  })
}

// Hide cutoff elements
function bsgHideCutoffs() {
  bsgCutoff.hidden = true
}

// Show empty results
function bsgEmptyCheck() {
  const noMatch = document.querySelector("#bsg-nomatch")
  let insMessage = true
  if (noMatch) {
    noMatch.remove()
    bsgTable.classList.remove("dn")
  }
  bsgRows.forEach((row) => {
    if (row.hidden === false) {
      insMessage = false
      return
    }
  })
  if (insMessage) {
    bsgTable.classList.add("dn")
    bsgTable.insertAdjacentHTML(
      "afterend",
      '
No matches
'
    )
  }
}

// Close filter
function bsgCloseFilter() {
  const activeBtn = document.querySelector("[data-bsgfilterbtn].active")
  if (activeBtn) {
    activeBtn.classList.remove("active")
  }
  bsgFilterSets.forEach((set) => {
    set.hidden = true
    set.classList.remove("flex")
  })

  let clearFilters = true
  bsgFilterChecks.forEach((filterCheck) => {
    if (filterCheck.checked == true) {
      clearFilters = false
    }
  })
  if (clearFilters) {
    bsgClearFilters()
  }
}

// Clear all filters
function bsgClearFilters() {
  bsgHideCutoffs()

  bsgFilterChecks.forEach((filterCheck) => {
    filterCheck.checked = false
  })
  bsgRows.forEach((row) => {
    row.hidden = false
  })
  bsgFilterInput.disabled = false
  bsgFilterInput.value = ""

  bsgEmptyCheck()
  allFilters = {}
  bsgFilterCombo.innerHTML = ""
  bsgClearBtn.classList.add("dn")
}

bsgClearBtn.addEventListener("click", function () {
  bsgCloseFilter()
  bsgClearFilters()
})

bsgCutoffBtn.addEventListener("click", function () {
  bsgClearFilters()
})

// Input filter
bsgFilterInput.addEventListener("keyup", function (e) {
  bsgHideCutoffs()
  const query = e.target.value.toLowerCase()
  const notEmpty = query.length >= 1
  if (notEmpty) {
    bsgClearBtn.classList.remove("dn")
  } else {
    bsgClearBtn.classList.add("dn")
  }

  bsgRows.forEach((row) => {
    if (
      row
        .querySelector('[data-filter="name"]')
        .textContent.toLowerCase()
        .includes(query) ||
      row
        .querySelector('[data-filter="servcode"]')
        .textContent.toLowerCase()
        .includes(query) ||
      row
        .querySelector('[data-filter="reqcode"]')
        .textContent.toLowerCase()
        .includes(query)
    ) {
      row.hidden = false
    } else {
      row.hidden = true
    }
  })

  bsgEmptyCheck()
})

// Render combofilter overview
function comboFilter(allFilters) {
  bsgFilterCombo.innerHTML = ""
  for (let activeFilter in allFilters) {
    if (allFilters[activeFilter].length > 0) {
      const filterTitle = document.querySelector(
        '[data-bsgfilterbtn="' + activeFilter + '"]'
      ).textContent
      let appliedFilter =
        '<button type="button" data-bsgtitle="' +
        activeFilter +
        '" class="btn-link btn-link--filter">' +
        filterTitle +
        '</button>
'
      allFilters[activeFilter].forEach((filter) => {
        appliedFilter =
          appliedFilter +
          '<button type="button"' +
          'data-bsgtype="' +
          activeFilter +
          '" data-bsgtitle="' +
          filter +
          '" class="btn-link btn-link--filter">' +
          filter +
          '</button>'
      })
      appliedFilter = appliedFilter + "
"
      bsgFilterCombo.insertAdjacentHTML("beforeend", appliedFilter)
      window.loadMybringIcons()
    }
  }
}

// Remove filter via combo overview
bsgFilterCombo.addEventListener("click", function (e) {
  if (e.target.dataset.bsgtitle) {
    const uncheckEls = document.querySelectorAll(
      'button[data-bsgtype="' + e.target.dataset.bsgtitle + '"]'
    )
    uncheckEls.forEach((uncheckEl) => {
      uncheckEl.click()
    })
  }

  if (e.target.dataset.bsgtype) {
    const uncheckEl = document.querySelector(
      'input[data-filter="' +
        e.target.dataset.bsgtype +
        '"][value="' +
        e.target.dataset.bsgtitle +
        '"'
    )
    uncheckEl.click()
  }
})

// Create object for storing active filters
allFilters = {}
bsgFilterSets.forEach((set) => {
  allFilters[set.id] = []
})

bsgFilterBtns.forEach((filterBtn) => {
  // Toggle filterset
  filterBtn.addEventListener("click", function (e) {
    const currentFilterSet = e.target.dataset.bsgfilterbtn
    if (e.target.classList.contains("active")) {
      bsgCloseFilter()
      return
    }
    bsgCloseFilter()
    bsgClearBtn.classList.remove("dn")

    e.target.classList.add("active")
    bsgFilterInput.disabled = true

    bsgFilterSets.forEach((set) => {
      if (set.id === currentFilterSet) {
        if (set.hidden === false) {
          set.hidden = true
          set.classList.remove("flex")
        } else {
          set.hidden = false
          set.classList.add("flex")
        }
      } else {
        set.hidden = true
        set.classList.remove("flex")
      }
    })

    // Register check clicks from open set
    const bsgSetChecks = document.querySelectorAll(
      'input[data-filter="' + currentFilterSet + '"]'
    )
    bsgSetChecks.forEach((setCheck) => {
      setCheck.addEventListener("click", function (e) {
        const filterKey = e.target.dataset.filter

        // Store active filters
        allFilters[filterKey] = []
        bsgSetChecks.forEach((setCheck) => {
          if (setCheck.checked === true) {
            allFilters[filterKey].push(setCheck.value)
          }
        })

        let activeFilterSets = 0
        for (let activeFilter in allFilters) {
          if (allFilters[activeFilter].length > 0) {
            activeFilterSets++
          }
        }

        comboFilter(allFilters)

        // Toggle table rows
        if (activeFilterSets <= 0) {
          bsgRows.forEach((row) => {
            row.hidden = false
          })
        } else {
          bsgRows.forEach((row) => {
            row.hidden = true

            // Check if row data macthes at least one active filter in each active filterset
            let matches = 0
            for (const activeFilter in allFilters) {
              if (allFilters[activeFilter].length > 0) {
                const currentFilters = allFilters[activeFilter]
                const rowDataAtt = '[data-filter="' + activeFilter + '"]'
                currentFilters.forEach((currentFilter) => {
                  if (row.querySelector(rowDataAtt)) {
                    const currentRowData = row
                      .querySelector(rowDataAtt)
                      .textContent.toLowerCase()
                    if (currentRowData === currentFilter.toLowerCase()) {
                      matches++
                    }
                  }
                })
              }
            }
            if (matches === activeFilterSets) {
              row.hidden = false
            }
          })
        }
        bsgEmptyCheck()
      })
    })
  })
})

// Toggle help
const helpBtn = document.querySelector("#help-country")
const helpTxt = document.querySelector("#info-country")
function toggleHelp() {
  if (helpTxt.hidden == true) {
    helpTxt.hidden = false
  } else {
    helpTxt.hidden = true
  }
}

helpBtn.addEventListener("click", toggleHelp)

document.addEventListener("DOMContentLoaded", function () {
  bsgTableCutoff()
})
<commit_msg>Add functionality to remove the filter using btns<commit_after>const bsgTable = document.querySelector("#services-bsg")
const bsgRows = bsgTable.querySelectorAll("tbody tr")
const bsgFilterInput = document.querySelector("#bsg-textfilter")
const bsgFilterBtns = document.querySelectorAll("[data-bsgfilterbtn]")
const bsgFilterSets = document.querySelectorAll("#bsg-filtersets fieldset")
const bsgFilterChecks = document.querySelectorAll(".checkitem")
const bsgClearBtn = document.querySelector("#bsg-clearfilters")
const bsgFilterCombo = document.querySelector("#bsg-filtercombo")
const bsgCutoff = document.querySelector("#bsg-cutoff")
const bsgCutoffBtn = bsgCutoff.querySelector("button")
let allFilters

// Initial cutoff
function bsgTableCutoff() {
  bsgRows.forEach((row, i) => {
    if (i > 5) {
      row.hidden = true
    }
  })
}

// Hide cutoff elements
function bsgHideCutoffs() {
  bsgCutoff.hidden = true
}

// Show empty results
function bsgEmptyCheck() {
  const noMatch = document.querySelector("#bsg-nomatch")
  let insMessage = true
  if (noMatch) {
    noMatch.remove()
    bsgTable.classList.remove("dn")
  }
  bsgRows.forEach((row) => {
    if (row.hidden === false) {
      insMessage = false
      return
    }
  })
  if (insMessage) {
    bsgTable.classList.add("dn")
    bsgTable.insertAdjacentHTML(
      "afterend",
      '
No matches
'
    )
  }
}

// Close filter
function bsgCloseFilter() {
  const activeBtn = document.querySelector("[data-bsgfilterbtn].active")
  if (activeBtn) {
    activeBtn.classList.remove("active")
  }
  bsgFilterSets.forEach((set) => {
    set.hidden = true
    set.classList.remove("flex")
  })

  let clearFilters = true
  bsgFilterChecks.forEach((filterCheck) => {
    if (filterCheck.checked == true) {
      clearFilters = false
    }
  })
  if (clearFilters) {
    bsgClearFilters()
  }
}

// Clear all filters
function bsgClearFilters() {
  bsgHideCutoffs()

  bsgFilterChecks.forEach((filterCheck) => {
    filterCheck.checked = false
  })
  bsgRows.forEach((row) => {
    row.hidden = false
  })
  bsgFilterInput.disabled = false
  bsgFilterInput.value = ""

  bsgEmptyCheck()
  allFilters = {}
  bsgFilterCombo.innerHTML = ""
  bsgClearBtn.classList.add("dn")
}

bsgClearBtn.addEventListener("click", function () {
  bsgCloseFilter()
  bsgClearFilters()
})

bsgCutoffBtn.addEventListener("click", function () {
  bsgClearFilters()
})

// Input filter
bsgFilterInput.addEventListener("keyup", function (e) {
  bsgHideCutoffs()
  const query = e.target.value.toLowerCase()
  const notEmpty = query.length >= 1
  if (notEmpty) {
    bsgClearBtn.classList.remove("dn")
  } else {
    bsgClearBtn.classList.add("dn")
  }

  bsgRows.forEach((row) => {
    if (
      row
        .querySelector('[data-filter="name"]')
        .textContent.toLowerCase()
        .includes(query) ||
      row
        .querySelector('[data-filter="servcode"]')
        .textContent.toLowerCase()
        .includes(query) ||
      row
        .querySelector('[data-filter="reqcode"]')
        .textContent.toLowerCase()
        .includes(query)
    ) {
      row.hidden = false
    } else {
      row.hidden = true
    }
  })

  bsgEmptyCheck()
})

// Render combofilter overview
function comboFilter(allFilters) {
  bsgFilterCombo.innerHTML = ""
  for (let activeFilter in allFilters) {
    if (allFilters[activeFilter].length > 0) {
      const filterTitle = document.querySelector(
        '[data-bsgfilterbtn="' + activeFilter + '"]'
      ).textContent
      let appliedFilter =
        '<button type="button" data-bsgtitle="' +
        activeFilter +
        '" class="btn-link btn-link--filter">' +
        filterTitle +
        "</button>"
      allFilters[activeFilter].forEach((filter) => {
        appliedFilter =
          appliedFilter +
          '<button type="button"' +
          'data-bsgtype="' +
          activeFilter +
          '" data-bsgtitle="' +
          filter +
          '" class="btn-link btn-link--filter mls">' +
          filter +
          "</button>"
      })
      bsgFilterCombo.insertAdjacentHTML("beforeend", appliedFilter)
      window.loadMybringIcons()
    }
  }
}

function toggleCheckbox(box) {
  const checkEl = document.querySelector(
    'input[data-filter="' +
      box.dataset.bsgtype +
      '"][value="' +
      box.dataset.bsgtitle +
      '"'
  )
  checkEl.click()
}

// Remove filters via combo overview
bsgFilterCombo.addEventListener("click", function (e) {
  // Delete all
  if (e.target.dataset.bsgtitle && !e.target.dataset.bsgtype) {
    let subButtons = document.querySelectorAll(
      'button[data-bsgtype="' + e.target.dataset.bsgtitle + '"]'
    )
    subButtons.forEach((subButton) => {
      toggleCheckbox(subButton)
    })
  }

  // Delete one
  if (e.target.dataset.bsgtype && e.target.dataset.bsgtitle) {
    toggleCheckbox(e.target)
  }
})

// Create object for storing active filters
allFilters = {}
bsgFilterSets.forEach((set) => {
  allFilters[set.id] = []
})

bsgFilterBtns.forEach((filterBtn) => {
  // Toggle filterset
  filterBtn.addEventListener("click", function (e) {
    const currentFilterSet = e.target.dataset.bsgfilterbtn
    if (e.target.classList.contains("active")) {
      bsgCloseFilter()
      return
    }
    bsgCloseFilter()
    bsgClearBtn.classList.remove("dn")

    e.target.classList.add("active")
    bsgFilterInput.disabled = true

    bsgFilterSets.forEach((set) => {
      if (set.id === currentFilterSet) {
        if (set.hidden === false) {
          set.hidden = true
          set.classList.remove("flex")
        } else {
          set.hidden = false
          set.classList.add("flex")
        }
      } else {
        set.hidden = true
        set.classList.remove("flex")
      }
    })

    // Register check clicks from open set
    const bsgSetChecks = document.querySelectorAll(
      'input[data-filter="' + currentFilterSet + '"]'
    )
    bsgSetChecks.forEach((setCheck) => {
      setCheck.addEventListener("click", function (e) {
        const filterKey = e.target.dataset.filter

        // Store active filters
        allFilters[filterKey] = []
        bsgSetChecks.forEach((setCheck) => {
          if (setCheck.checked === true) {
            allFilters[filterKey].push(setCheck.value)
          }
        })

        let activeFilterSets = 0
        for (let activeFilter in allFilters) {
          if (allFilters[activeFilter].length > 0) {
            activeFilterSets++
          }
        }

        comboFilter(allFilters)

        // Toggle table rows
        if (activeFilterSets <= 0) {
          bsgRows.forEach((row) => {
            row.hidden = false
          })
        } else {
          bsgRows.forEach((row) => {
            row.hidden = true

            // Check if row data macthes at least one active filter in each active filterset
            let matches = 0
            for (const activeFilter in allFilters) {
              if (allFilters[activeFilter].length > 0) {
                const currentFilters = allFilters[activeFilter]
                const rowDataAtt = '[data-filter="' + activeFilter + '"]'
                currentFilters.forEach((currentFilter) => {
                  if (row.querySelector(rowDataAtt)) {
                    const currentRowData = row
                      .querySelector(rowDataAtt)
                      .textContent.toLowerCase()
                    if (currentRowData === currentFilter.toLowerCase()) {
                      matches++
                    }
                  }
                })
              }
            }
            if (matches === activeFilterSets) {
              row.hidden = false
            }
          })
        }
        bsgEmptyCheck()
      })
    })
  })
})

// Toggle help
const helpBtn = document.querySelector("#help-country")
const helpTxt = document.querySelector("#info-country")
function toggleHelp() {
  if (helpTxt.hidden == true) {
    helpTxt.hidden = false
  } else {
    helpTxt.hidden = true
  }
}

helpBtn.addEventListener("click", toggleHelp)

document.addEventListener("DOMContentLoaded", function () {
  bsgTableCutoff()
})
<|endoftext|>