<commit_before>var Discord = require('discord.js');

var youtubeStream = require('youtube-audio-stream');
var ytdl = require('ytdl-core');

var shouldDisallowQueue = require('./lib/permission-checks.js');
var VideoFormat = require('./lib/video-format.js');

var Saved = require('./lib/saved.js');
Saved.read();

var client = new Discord.Client();

// Handle discord.js warnings
client.on('warn', (m) => console.log('[warn]', m));

var playQueue = [];
var boundChannel = false;
var currentStream = false;
var currentVideo = false;

var botMention = false;

client.on('ready', () => {
  botMention = `<@${client.internal.user.id}>`;
  console.log(`Bot mention: ${botMention}`);
});

client.on('message', m => {
  if (!botMention) return;

  if (m.content.startsWith(`${botMention} i`)) { // init
    var channelToJoin = spliceArguments(m.content)[1];
    for (var channel of m.channel.server.channels) {
      if (channel instanceof Discord.VoiceChannel) {
        if (!channelToJoin || channel.name === channelToJoin) {
          boundChannel = m.channel;
          client.reply(m, `Binding to text channel <#${boundChannel.id}> and voice channel **${channel.name}** \`(${channel.id})\``);
          client.joinVoiceChannel(channel).catch(error);
          break;
        }
      }
    }
  }

  if (m.content.startsWith(`${botMention} d`)) { // destroy
    if (!boundChannel) return;
    client.reply(m, `Unbinding from <#${boundChannel.id}> and destroying voice connection`);
    playQueue = [];
    currentStream.destroy();
    client.internal.leaveVoiceChannel();
    boundChannel = false;
    return;
  }

  // Only respond to other messages inside the bound channel
  if (!m.channel.equals(boundChannel)) return;

  if (m.content.startsWith(`${botMention} n`)) { // next
    currentStream.destroy();
    playStopped();
  }

  if (m.content.startsWith(`${botMention} y`) // youtube
    || m.content.startsWith(`${botMention} q`)) { // queue
    || m.content.startsWith(`${botMention} p`)) { // play

    var vid = Saved.possiblyRetrieveVideo(spliceArguments(m.content)[1]);
    var requestUrl = 'http://www.youtube.com/watch?v=' + vid;
    ytdl.getInfo(requestUrl, (err, info) => {
      if (err) handleYTError(err);
      else possiblyQueue(info, m.author.id, m);
    });
  }

  if (m.content.startsWith(`${botMention} l`)) { // list
    var formattedList = 'Here are the videos currently in the play queue, from first added to last added: \n';
    formattedList += `Currently playing: ${VideoFormat.prettyPrintWithUser(currentVideo)}\n`;
    playQueue.forEach((video, idx) => {
      formattedList += `${idx + 1}. ${VideoFormat.prettyPrintWithUser(video)}\n`;
    });
    client.reply(m, formattedList);
  }

  if (m.content.startsWith(`${botMention} s`)) { // save
    var argument = spliceArguments(m.content)[1];
    var splitArgs = spliceArguments(argument, 1);
    var requestUrl = 'http://www.youtube.com/watch?v=' + splitArgs[0];
    ytdl.getInfo(requestUrl, (err, info) => {
      if (err) handleYTError(err);
      else saveVideo(info, splitArgs[1], m);
    });
  }
});

function spliceArguments(message, after) {
  after = after || 2;
  var rest = message.split(' ');
  var removed = rest.splice(0, after);
  return [removed.join(' '), rest.join(' ')];
}

function saveVideo(video, keywords, m) {
  simplified = VideoFormat.simplify(video);
  Saved.saved.videos[keywords] = simplified;
  client.reply(m, `Saved video ${VideoFormat.prettyPrint(video)} as **${splitArgs[1]}**`);
  Saved.write();
}

function possiblyQueue(video, userId, m) {
  video.userId = userId;
  reason = shouldDisallowQueue(playQueue, video);
  if (reason) {
    client.reply(m, `You can't queue this video right now! Reason: ${reason}`);
  } else {
    playQueue.push(video);
    client.reply(m, `Queued ${VideoFormat.prettyPrint(video)}`);

    // Start playing if not playing yet
    if (!currentVideo) nextInQueue();
  }
}

function handleYTError(err) {
  if (err.toString().indexOf('Code 150') > -1) {
    // Video unavailable in country
    client.sendMessage(boundChannel, 'This video is unavailable in the country the bot is running in! Please try a different video.');
  } else {
    client.sendMessage(boundChannel, 'An error occurred while getting video information! Please try a different video.');
  }

  console.log(err.toString());
}

function playStopped() {
  if (client.internal.voiceConnection) client.internal.voiceConnection.stopPlaying();

  client.sendMessage(boundChannel, `Finished playing ${VideoFormat.simplePrint(currentVideo)}`);
  currentVideo = false;
  nextInQueue();
}

function play(video) {
  currentVideo = video;
  if (client.internal.voiceConnection) {
    var connection = client.internal.voiceConnection;
    currentStream = youtubeStream(video.loaderUrl);
    currentStream.on('end', () => setTimeout(playStopped, 8000)); // 8 second leeway for bad timing
    connection.playRawStream(currentStream).then(intent => {
      client.sendMessage(boundChannel, `Playing ${VideoFormat.prettyPrint(video)}`);
    });
  }
}

function nextInQueue() {
  if (playQueue.length > 0) {
    next = playQueue.shift();
    play(next);
  }
}

function error(argument) {
  console.log(argument.stack);
}

// Email and password over command line
client.login(process.argv[2], process.argv[3]).catch((e) => console.log(e));
<commit_msg>Add a command to list saved videos<commit_after>var Discord = require('discord.js');

var youtubeStream = require('youtube-audio-stream');
var ytdl = require('ytdl-core');

var shouldDisallowQueue = require('./lib/permission-checks.js');
var VideoFormat = require('./lib/video-format.js');

var Saved = require('./lib/saved.js');
Saved.read();

var client = new Discord.Client();

// Handle discord.js warnings
client.on('warn', (m) => console.log('[warn]', m));

var playQueue = [];
var boundChannel = false;
var currentStream = false;
var currentVideo = false;

var botMention = false;

client.on('ready', () => {
  botMention = `<@${client.internal.user.id}>`;
  console.log(`Bot mention: ${botMention}`);
});

client.on('message', m => {
  if (!botMention) return;

  if (m.content.startsWith(`${botMention} i`)) { // init
    var channelToJoin = spliceArguments(m.content)[1];
    for (var channel of m.channel.server.channels) {
      if (channel instanceof Discord.VoiceChannel) {
        if (!channelToJoin || channel.name === channelToJoin) {
          boundChannel = m.channel;
          client.reply(m, `Binding to text channel <#${boundChannel.id}> and voice channel **${channel.name}** \`(${channel.id})\``);
          client.joinVoiceChannel(channel).catch(error);
          break;
        }
      }
    }
  }

  if (m.content.startsWith(`${botMention} d`)) { // destroy
    if (!boundChannel) return;
    client.reply(m, `Unbinding from <#${boundChannel.id}> and destroying voice connection`);
    playQueue = [];
    currentStream.destroy();
    client.internal.leaveVoiceChannel();
    boundChannel = false;
    return;
  }

  // Only respond to other messages inside the bound channel
  if (!m.channel.equals(boundChannel)) return;

  if (m.content.startsWith(`${botMention} n`)) { // next
    currentStream.destroy();
    playStopped();
  }

  if (m.content.startsWith(`${botMention} y`) // youtube
    || m.content.startsWith(`${botMention} q`)) { // queue
    || m.content.startsWith(`${botMention} p`)) { // play

    var vid = Saved.possiblyRetrieveVideo(spliceArguments(m.content)[1]);
    var requestUrl = 'http://www.youtube.com/watch?v=' + vid;
    ytdl.getInfo(requestUrl, (err, info) => {
      if (err) handleYTError(err);
      else possiblyQueue(info, m.author.id, m);
    });
  }

  if (m.content.startsWith(`${botMention} list s`)) { // list saved
    var formattedList = 'Here are the videos currently saved: \n';
    for(var key in Saved.saved.videos) {
      if(Saved.saved.videos.hasOwnProperty(key)) {
        formattedList += `**${key}**: ${VideoFormat.prettyPrint(Saved.saved.videos[key])}\n`;
      }
    }
    client.reply(m, formattedList);
    return; // so list doesn't get triggered
  }

  if (m.content.startsWith(`${botMention} l`)) { // list
    var formattedList = 'Here are the videos currently in the play queue, from first added to last added: \n';
    formattedList += `Currently playing: ${VideoFormat.prettyPrintWithUser(currentVideo)}\n`;
    playQueue.forEach((video, idx) => {
      formattedList += `${idx + 1}. ${VideoFormat.prettyPrintWithUser(video)}\n`;
    });
    client.reply(m, formattedList);
  }

  if (m.content.startsWith(`${botMention} s`)) { // save
    var argument = spliceArguments(m.content)[1];
    var splitArgs = spliceArguments(argument, 1);
    var requestUrl = 'http://www.youtube.com/watch?v=' + splitArgs[0];
    ytdl.getInfo(requestUrl, (err, info) => {
      if (err) handleYTError(err);
      else saveVideo(info, splitArgs[1], m);
    });
  }
});

function spliceArguments(message, after) {
  after = after || 2;
  var rest = message.split(' ');
  var removed = rest.splice(0, after);
  return [removed.join(' '), rest.join(' ')];
}

function saveVideo(video, keywords, m) {
  simplified = VideoFormat.simplify(video);
  Saved.saved.videos[keywords] = simplified;
  client.reply(m, `Saved video ${VideoFormat.prettyPrint(video)} as **${splitArgs[1]}**`);
  Saved.write();
}

function possiblyQueue(video, userId, m) {
  video.userId = userId;
  reason = shouldDisallowQueue(playQueue, video);
  if (reason) {
    client.reply(m, `You can't queue this video right now! Reason: ${reason}`);
  } else {
    playQueue.push(video);
    client.reply(m, `Queued ${VideoFormat.prettyPrint(video)}`);

    // Start playing if not playing yet
    if (!currentVideo) nextInQueue();
  }
}

function handleYTError(err) {
  if (err.toString().indexOf('Code 150') > -1) {
    // Video unavailable in country
    client.sendMessage(boundChannel, 'This video is unavailable in the country the bot is running in! Please try a different video.');
  } else {
    client.sendMessage(boundChannel, 'An error occurred while getting video information! Please try a different video.');
  }

  console.log(err.toString());
}

function playStopped() {
  if (client.internal.voiceConnection) client.internal.voiceConnection.stopPlaying();

  client.sendMessage(boundChannel, `Finished playing ${VideoFormat.simplePrint(currentVideo)}`);
  currentVideo = false;
  nextInQueue();
}

function play(video) {
  currentVideo = video;
  if (client.internal.voiceConnection) {
    var connection = client.internal.voiceConnection;
    currentStream = youtubeStream(video.loaderUrl);
    currentStream.on('end', () => setTimeout(playStopped, 8000)); // 8 second leeway for bad timing
    connection.playRawStream(currentStream).then(intent => {
      client.sendMessage(boundChannel, `Playing ${VideoFormat.prettyPrint(video)}`);
    });
  }
}

function nextInQueue() {
  if (playQueue.length > 0) {
    next = playQueue.shift();
    play(next);
  }
}

function error(argument) {
  console.log(argument.stack);
}

// Email and password over command line
client.login(process.argv[2], process.argv[3]).catch((e) => console.log(e));
<|endoftext|>


<commit_before>/** @file
 * This module provides several utilities to easier access
 * access the lfapi.
 *
 * This is meant to be used as a module in nodejs, so use
 * @code
 * var lfcli = require('lfcli.js')
 * @endcode
 */

var http = require('http');

/// Define base URL of LQFB backend
// TODO pass this in from outside
var baseurl = {
	host: 'apitest.liquidfeedback.org',
	port: 25520
};

/**
 * Build up the request options for the given path.
 */
var buildRequestOptions = function(path) {
	var options = {
		host: baseurl.host,
		port: baseurl.port
	};
	if(path) {
		if(baseurl.path && typeof(baseurl.path) === 'string') {
			// make sure there is a dash in between
			if(path.charAt(0) === '/' || baseurl.path.charAt(baseurl.path.length-1) === '/') {
				options.path = baseurl.path + path;
			} else {
				options.path = baseurl.path + '/' + path;
			}
		} else {
			options.path = path;
		}
	}
	return options;
}

/**
 * Perform a query against the Liquid Feedback API Server.
 *
 * The function will automaticall check for HTTP-Errors, parse the JSON returned by the server and
 * invoke the given handler function.
 *
 * @param path The query to perform as defined in http://dev.liquidfeedback.org/trac/lf/wiki/API.
 * @param handler The function to handle the JSON object returned by the API Server in response to the query.
 * @return The ClientResponseObject given by http(s).request.
 */
exports.query = function(path, handler) {
	var options = buildRequestOptions(path);

	var extended_handler = function(res) {
		if(res.statusCode != 200) {
			console.error('Request failed: ' + res.statusCode);
			return;
		}

		// aggregate result body
		body = '';
		res.on('data', function(chunck) {
			body += chunck;
		});

		// when everything is aggregated, part body and invoke handlers
		res.on('end', function() {
			// TODO handle parsing errors
			answer = JSON.parse(body);
			if(!answer.status || answer.status !== 'ok') {
				console.warn('STATUS:' + answer.status);
			}
			// TODO different handler in case of errors
			handler(answer);
		});
	}

	return http.get(options, extended_handler).on('error', function(e) {
		console.error("Got error: " + e.message);
	});
};

/**
 * Allow to update the Base URL.
 *
 * @param newbase JSON specification of the base URL as it would be expected by http(s).request().
 */
exports.setBaseURL = function(newbase) {
	baseurl = newbase;
}
<commit_msg>Fix concurrent requests to LFAPI.<commit_after>/** @file
 * This module provides several utilities to easier access
 * access the lfapi.
 *
 * This is meant to be used as a module in nodejs, so use
 * @code
 * var lfcli = require('lfcli.js')
 * @endcode
 */

var http = require('http');

/// Define base URL of LQFB backend
// TODO pass this in from outside
var baseurl = {
	host: 'apitest.liquidfeedback.org',
	port: 25520
};

/**
 * Build up the request options for the given path.
 */
var buildRequestOptions = function(path) {
	var options = {
		host: baseurl.host,
		port: baseurl.port
	};
	if(path) {
		if(baseurl.path && typeof(baseurl.path) === 'string') {
			// make sure there is a dash in between
			if(path.charAt(0) === '/' || baseurl.path.charAt(baseurl.path.length-1) === '/') {
				options.path = baseurl.path + path;
			} else {
				options.path = baseurl.path + '/' + path;
			}
		} else {
			options.path = path;
		}
	}
	return options;
}

/**
 * Perform a query against the Liquid Feedback API Server.
 *
 * The function will automaticall check for HTTP-Errors, parse the JSON returned by the server and
 * invoke the given handler function.
 *
 * @param path The query to perform as defined in http://dev.liquidfeedback.org/trac/lf/wiki/API.
 * @param handler The function to handle the JSON object returned by the API Server in response to the query.
 * @return The ClientResponseObject given by http(s).request.
 */
exports.query = function(path, handler) {
	var options = buildRequestOptions(path);

	var extended_handler = function(res) {
		if(res.statusCode != 200) {
			console.error('Request failed: ' + res.statusCode);
			return;
		}

		// aggregate result body
		var body = '';
		res.on('data', function(chunck) {
			body += chunck;
		});

		// when everything is aggregated, part body and invoke handlers
		res.on('end', function() {
			// TODO handle parsing errors
			var answer = JSON.parse(body);
			if(!answer.status || answer.status !== 'ok') {
				console.warn('STATUS:' + answer.status);
			}
			// TODO different handler in case of errors
			handler(answer);
		});
	}

	return http.get(options, extended_handler).on('error', function(e) {
		console.error("Got error: " + e.message);
	});
};

/**
 * Allow to update the Base URL.
 *
 * @param newbase JSON specification of the base URL as it would be expected by http(s).request().
 */
exports.setBaseURL = function(newbase) {
	baseurl = newbase;
}
<|endoftext|>


<commit_before>const moment = require('moment');
const geezer = require('geezer');
const { ge } = require('ethiopic-calendar');

const lw5Cache = require('./lw5Cache');
const schedule = require('./../schedule');
const { gregorianWeekdayToEthiopicWeekday, ethiopicMonthToFullEthiopicMonth } = require('./ec');

module.exports = moedoo => () => new Promise((resolve, reject) => {
  const todayYYYYMMDD = moment().format('YYYY-MM-DD');

  // no show update for today...
  if (Object.prototype.hasOwnProperty.call(schedule, todayYYYYMMDD) === false) {
    reject('¯\\_(ツ)_/¯');
    return;
  }

  // Gregorian Calendar Moment
  const gCMoment = moment();
  // Ethiopic Object { year, month, date }
  const ethiopicCalendar = ge(gCMoment.year(), gCMoment.month() + 1, gCMoment.date());
  const movies = {}; // { title, indices }

  [1, 2, 3].forEach((cinemaNumber) => {
    schedule[todayYYYYMMDD][`c${cinemaNumber}`].forEach((movie, index) => {
      if (Object.prototype.hasOwnProperty.call(movies, movie.title) === true) {
        movies[movie.title].push({
          cinema: `c${cinemaNumber}`,
          index,
        });
      } else {
        movies[movie.title] = [
          {
            cinema: `c${cinemaNumber}`,
            index,
          },
        ];
      }
    });
  });

  // to be used on lw5 Promise...
  const lw5Mapper = Object.keys(movies).map(movieTitle => ({
    title: movieTitle,
    indices: movies[movieTitle],
  }));

  // build lw5 promise for each movie...
  const lw5MapperPromises = lw5Mapper.map(lw5 => lw5Cache(moedoo, lw5.title));

  Promise
    .all(lw5MapperPromises)
    .then((lw5Data) => {
      lw5Data.forEach((lw5Response, index) => {
        if (lw5Response !== null) {
          // updating movie detail...
          lw5Mapper[index].indices.forEach((lw5MapIndex) => {
            schedule[lw5MapIndex.cinema][lw5MapIndex.index].title = lw5Response.name;
            schedule[lw5MapIndex.cinema][lw5MapIndex.index].detail = lw5Response;
            schedule[lw5MapIndex.cinema][lw5MapIndex.index].posterURL = lw5Response.poster;
          });
        }
      });

      resolve({
        show: schedule,
        meta: {
          gc: `${gCMoment.format('dddd, MMMM D')}`,
          ec: `${gregorianWeekdayToEthiopicWeekday(gCMoment.format('dddd'))}, ${ethiopicMonthToFullEthiopicMonth(ethiopicCalendar.month)} ${geezer(ethiopicCalendar.day)}`,
        },
      });
    }, () => {
      // resolving with all null detail
      resolve({
        show: schedule,
        meta: {
          gc: `${gCMoment.format('dddd, MMMM D')}`,
          ec: `${gregorianWeekdayToEthiopicWeekday(gCMoment.format('dddd'))}, ${ethiopicMonthToFullEthiopicMonth(ethiopicCalendar.month)} ${geezer(ethiopicCalendar.day)}`,
        },
      });
    });
});
<commit_msg>mapping with `todayYYYYMMDD`<commit_after>/* eslint max-len: 0 */

const moment = require('moment');
const geezer = require('geezer');
const { ge } = require('ethiopic-calendar');

const lw5Cache = require('./lw5Cache');
const schedule = require('./../schedule');
const { gregorianWeekdayToEthiopicWeekday, ethiopicMonthToFullEthiopicMonth } = require('./ec');

module.exports = moedoo => () => new Promise((resolve, reject) => {
  const todayYYYYMMDD = moment().format('YYYY-MM-DD');

  // no show update for today...
  if (Object.prototype.hasOwnProperty.call(schedule, todayYYYYMMDD) === false) {
    reject('¯\\_(ツ)_/¯');
    return;
  }

  // Gregorian Calendar Moment
  const gCMoment = moment();
  // Ethiopic Object { year, month, date }
  const ethiopicCalendar = ge(gCMoment.year(), gCMoment.month() + 1, gCMoment.date());
  const movies = {}; // { title, indices }

  [1, 2, 3].forEach((cinemaNumber) => {
    schedule[todayYYYYMMDD][`c${cinemaNumber}`].forEach((movie, index) => {
      if (Object.prototype.hasOwnProperty.call(movies, movie.title) === true) {
        movies[movie.title].push({
          cinema: `c${cinemaNumber}`,
          index,
        });
      } else {
        movies[movie.title] = [
          {
            cinema: `c${cinemaNumber}`,
            index,
          },
        ];
      }
    });
  });

  // to be used on lw5 Promise...
  const lw5Mapper = Object.keys(movies).map(movieTitle => ({
    title: movieTitle,
    indices: movies[movieTitle],
  }));

  // build lw5 promise for each movie...
  const lw5MapperPromises = lw5Mapper.map(lw5 => lw5Cache(moedoo, lw5.title));

  Promise
    .all(lw5MapperPromises)
    .then((lw5Data) => {
      lw5Data.forEach((lw5Response, index) => {
        if (lw5Response !== null) {
          // updating movie detail...
          lw5Mapper[index].indices.forEach((lw5MapIndex) => {
            schedule[todayYYYYMMDD][lw5MapIndex.cinema][lw5MapIndex.index].title = lw5Response.name;
            schedule[todayYYYYMMDD][lw5MapIndex.cinema][lw5MapIndex.index].detail = lw5Response;
            schedule[todayYYYYMMDD][lw5MapIndex.cinema][lw5MapIndex.index].posterURL = lw5Response.poster;
          });
        }
      });

      resolve({
        show: schedule[todayYYYYMMDD],
        meta: {
          gc: `${gCMoment.format('dddd, MMMM D')}`,
          ec: `${gregorianWeekdayToEthiopicWeekday(gCMoment.format('dddd'))}, ${ethiopicMonthToFullEthiopicMonth(ethiopicCalendar.month)} ${geezer(ethiopicCalendar.day)}`,
        },
      });
    }, () => {
      // resolving with all null detail
      resolve({
        show: schedule[todayYYYYMMDD],
        meta: {
          gc: `${gCMoment.format('dddd, MMMM D')}`,
          ec: `${gregorianWeekdayToEthiopicWeekday(gCMoment.format('dddd'))}, ${ethiopicMonthToFullEthiopicMonth(ethiopicCalendar.month)} ${geezer(ethiopicCalendar.day)}`,
        },
      });
    });
});
<|endoftext|>


<commit_before>/* globals define,module */
/*
Using a Universal Module Loader that should be browser, require, and AMD friendly
http://ricostacruz.com/cheatsheets/umdjs.html
*/
;(function(root, factory) {
  if (typeof define === "function" && define.amd) {
    define(factory);
  } else if (typeof exports === "object") {
    module.exports = factory();
  } else {
    root.jsonLogic = factory();
  }
}(this, function() {
  "use strict";
  /* globals console:false */

  if ( ! Array.isArray) {
    Array.isArray = function(arg) {
      return Object.prototype.toString.call(arg) === "[object Array]";
    };
  }

  /**
   * Return an array that contains no duplicates (original not modified)
   * @param  {array} array   Original reference array
   * @return {array}         New array with no duplicates
   */
  function arrayUnique(array) {
    var a = [];
    for (var i=0, l=array.length; i<l; i++) {
      if (a.indexOf(array[i]) === -1) {
        a.push(array[i]);
      }
    }
    return a;
  }

  var jsonLogic = {};
  var operations = {
    "==": function(a, b) {
      return a == b;
    },
    "===": function(a, b) {
      return a === b;
    },
    "!=": function(a, b) {
      return a != b;
    },
    "!==": function(a, b) {
      return a !== b;
    },
    ">": function(a, b) {
      return a > b;
    },
    ">=": function(a, b) {
      return a >= b;
    },
    "<": function(a, b, c) {
      return (c === undefined) ? a < b : (a < b) && (b < c);
    },
    "<=": function(a, b, c) {
      return (c === undefined) ? a <= b : (a <= b) && (b <= c);
    },
    "!!": function(a) {
      return jsonLogic.truthy(a);
    },
    "!": function(a) {
      return !jsonLogic.truthy(a);
    },
    "%": function(a, b) {
      return a % b;
    },
    "log": function(a) {
      console.log(a); return a;
    },
    "in": function(a, b) {
      if(typeof b.indexOf === "undefined") return false;
      return (b.indexOf(a) !== -1);
    },
    "cat": function() {
      return Array.prototype.join.call(arguments, "");
    },
    "substr":function(source, start, end) {
      if(end < 0){
        // JavaScript doesn't support negative end, this emulates PHP behavior
        var temp = String(source).substr(start);
        return temp.substr(0, temp.length + end);
      }
      return String(source).substr(start, end);
    },
    "+": function() {
      return Array.prototype.reduce.call(arguments, function(a, b) {
        return parseFloat(a, 10) + parseFloat(b, 10);
      }, 0);
    },
    "*": function() {
      return Array.prototype.reduce.call(arguments, function(a, b) {
        return parseFloat(a, 10) * parseFloat(b, 10);
      });
    },
    "-": function(a, b) {
      if(b === undefined) {
        return -a;
      }else{
        return a - b;
      }
    },
    "/": function(a, b) {
      return a / b;
    },
    "min": function() {
      return Math.min.apply(this, arguments);
    },
    "max": function() {
      return Math.max.apply(this, arguments);
    },
    "merge": function() {
      return Array.prototype.reduce.call(arguments, function(a, b) {
        return a.concat(b);
      }, []);
    },
    "var": function(a, b) {
      var not_found = (b === undefined) ? null : b;
      var data = this;
      if(typeof a === "undefined" || a==="" || a===null) {
        return data;
      }
      var sub_props = String(a).split(".");
      for(var i = 0; i < sub_props.length; i++) {
        if(data === null) {
          return not_found;
        }
        // Descending into data
        data = data[sub_props[i]];
        if(data === undefined) {
          return not_found;
        }
      }
      return data;
    },
    "missing": function() {
      /*
      Missing can receive many keys as many arguments, like {"missing:[1,2]}
      Missing can also receive *one* argument that is an array of keys,
      which typically happens if it's actually acting on the output of another command
      (like 'if' or 'merge')
      */

      var missing = [];
      var keys = Array.isArray(arguments[0]) ? arguments[0] : arguments;

      for(var i = 0; i < keys.length; i++) {
        var key = keys[i];
        var value = jsonLogic.apply({"var": key}, this);
        if(value === null || value === "") {
          missing.push(key);
        }
      }

      return missing;
    },
    "missing_some": function(need_count, options) {
      // missing_some takes two arguments, how many (minimum) items must be present, and an array of keys (just like 'missing') to check for presence.
      var are_missing = jsonLogic.apply({"missing": options}, this);

      if(options.length - are_missing.length >= need_count) {
        return [];
      }else{
        return are_missing;
      }
    },
    "method": function(obj, method, args) {
      return obj[method].apply(obj, args);
    },

  };

  jsonLogic.is_logic = function(logic) {
    return (
      typeof logic === "object" && // An object
      logic !== null && // but not null
      ! Array.isArray(logic) && // and not an array
      Object.keys(logic).length === 1 // with exactly one key
    );
  };

  /*
  This helper will defer to the JsonLogic spec as a tie-breaker when different language interpreters define different behavior for the truthiness of primitives.  E.g., PHP considers empty arrays to be falsy, but Javascript considers them to be truthy. JsonLogic, as an ecosystem, needs one consistent answer.

  Spec and rationale here: http://jsonlogic.com/truthy
  */
  jsonLogic.truthy = function(value) {
    if(Array.isArray(value) && value.length === 0) {
      return false;
    }
    return !! value;
  };

  jsonLogic.get_operator = function(logic) {
    return Object.keys(logic)[0];
  };

  jsonLogic.get_values = function(logic) {
    return logic[jsonLogic.get_operator(logic)];
  };

  jsonLogic.apply = function(logic, data) {
    // Does this array contain logic? Only one way to find out.
    if(Array.isArray(logic)) {
      return logic.map(function(l) {
        return jsonLogic.apply(l, data);
      });
    }
    // You've recursed to a primitive, stop!
    if( ! jsonLogic.is_logic(logic) ) {
      return logic;
    }

    data = data || {};

    var op = jsonLogic.get_operator(logic);
    var values = logic[op];
    var i;
    var current;
    var scopedLogic, scopedData, filtered, initial;

    // easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]}
    if( ! Array.isArray(values)) {
      values = [values];
    }

    // 'if', 'and', and 'or' violate the normal rule of depth-first calculating consequents, let each manage recursion as needed.
    if(op === "if" || op == "?:") {
      /* 'if' should be called with a odd number of parameters, 3 or greater
      This works on the pattern:
      if( 0 ){ 1 }else{ 2 };
      if( 0 ){ 1 }else if( 2 ){ 3 }else{ 4 };
      if( 0 ){ 1 }else if( 2 ){ 3 }else if( 4 ){ 5 }else{ 6 };

      The implementation is:
      For pairs of values (0,1 then 2,3 then 4,5 etc)
      If the first evaluates truthy, evaluate and return the second
      If the first evaluates falsy, jump to the next pair (e.g, 0,1 to 2,3)
      given one parameter, evaluate and return it. (it's an Else and all the If/ElseIf were false)
      given 0 parameters, return NULL (not great practice, but there was no Else)
      */
      for(i = 0; i < values.length - 1; i += 2) {
        if( jsonLogic.truthy( jsonLogic.apply(values[i], data) ) ) {
          return jsonLogic.apply(values[i+1], data);
        }
      }
      if(values.length === i+1) return jsonLogic.apply(values[i], data);
      return null;
    }else if(op === "and") { // Return first falsy, or last
      for(i=0; i < values.length; i+=1) {
        current = jsonLogic.apply(values[i], data);
        if( ! jsonLogic.truthy(current)) {
          return current;
        }
      }
      return current; // Last
    }else if(op === "or") {// Return first truthy, or last
      for(i=0; i < values.length; i+=1) {
        current = jsonLogic.apply(values[i], data);
        if( jsonLogic.truthy(current) ) {
          return current;
        }
      }
      return current; // Last

    }else if(op === 'filter'){
      scopedData = jsonLogic.apply(values[0], data);
      scopedLogic = values[1];

      if ( ! Array.isArray(scopedData)) {
          return [];
      }
      // Return only the elements from the array in the first argument,
      // that return truthy when passed to the logic in the second argument.
      // For parity with JavaScript, reindex the returned array
      return scopedData.filter(function(datum){
          return jsonLogic.truthy( jsonLogic.apply(scopedLogic, datum));
      });
  }else if(op === 'map'){
      scopedData = jsonLogic.apply(values[0], data);
      scopedLogic = values[1];

      if ( ! Array.isArray(scopedData)) {
          return [];
      }

      return scopedData.map(function(datum){
          return jsonLogic.apply(scopedLogic, datum);
      });

  }else if(op === 'reduce'){
      scopedData = jsonLogic.apply(values[0], data);
      scopedLogic = values[1];
      initial = typeof values[2] !== 'undefined' ? values[2] : null;

      if ( ! Array.isArray(scopedData)) {
          return initial;
      }

      return scopedData.reduce(
          function(accumulator, current){
              return jsonLogic.apply(
                  scopedLogic,
                  {'current':current, 'accumulator':accumulator}
              );
          },
          initial
      );

    }else if(op === "all") {
      scopedData = jsonLogic.apply(values[0], data);
      scopedLogic = values[1];
      // All of an empty set is false. Note, some and none have correct fallback after the for loop
      if( ! scopedData.length) {
        return false;
      }
      for(i=0; i < scopedData.length; i+=1) {
        if( ! jsonLogic.truthy( jsonLogic.apply(scopedLogic, scopedData[i]) )) {
          return false; // First falsy, short circuit
        }
      }
      return true; // All were truthy
    }else if(op === "none") {
      filtered = jsonLogic.apply({'filter' : values}, data);
      return filtered.length === 0;

    }else if(op === "some") {
      filtered = jsonLogic.apply({'filter' : values}, data);
      return filtered.length > 0;
    }

    // Everyone else gets immediate depth-first recursion
    values = values.map(function(val) {
      return jsonLogic.apply(val, data);
    });

    // The operation is called with "data" bound to its "this" and "values" passed as arguments.
    // Structured commands like % or > can name formal arguments while flexible commands (like missing or merge) can operate on the pseudo-array arguments
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments
    if(typeof operations[op] === "function") {
      return operations[op].apply(data, values);
    }else if(op.indexOf(".") > 0) { // Contains a dot, and not in the 0th position
      var sub_ops = String(op).split(".");
      var operation = operations;
      for(i = 0; i < sub_ops.length; i++) {
        // Descending into operations
        operation = operation[sub_ops[i]];
        if(operation === undefined) {
          throw new Error("Unrecognized operation " + op +
          " (failed at " + sub_ops.slice(0, i+1).join(".") + ")");
        }
      }

      return operation.apply(data, values);
    }

    throw new Error("Unrecognized operation " + op );
  };

  jsonLogic.uses_data = function(logic) {
    var collection = [];

    if( jsonLogic.is_logic(logic) ) {
      var op = jsonLogic.get_operator(logic);
      var values = logic[op];

      if( ! Array.isArray(values)) {
        values = [values];
      }

      if(op === "var") {
        // This doesn't cover the case where the arg to var is itself a rule.
        collection.push(values[0]);
      }else{
        // Recursion!
        values.map(function(val) {
          collection.push.apply(collection, jsonLogic.uses_data(val) );
        });
      }
    }

    return arrayUnique(collection);
  };

  jsonLogic.add_operation = function(name, code) {
    operations[name] = code;
  };

  jsonLogic.rm_operation = function(name) {
    delete operations[name];
  };

  jsonLogic.rule_like = function(rule, pattern) {
    // console.log("Is ". JSON.stringify(rule) . " like " . JSON.stringify(pattern) . "?");
    if(pattern === rule) {
      return true;
    } // TODO : Deep object equivalency?
    if(pattern === "@") {
      return true;
    } // Wildcard!
    if(pattern === "number") {
      return (typeof rule === "number");
    }
    if(pattern === "string") {
      return (typeof rule === "string");
    }
    if(pattern === "array") {
      // !logic test might be superfluous in JavaScript
      return Array.isArray(rule) && ! jsonLogic.is_logic(rule);
    }

    if(jsonLogic.is_logic(pattern)) {
      if(jsonLogic.is_logic(rule)) {
        var pattern_op = jsonLogic.get_operator(pattern);
        var rule_op = jsonLogic.get_operator(rule);

        if(pattern_op === "@" || pattern_op === rule_op) {
        // echo "\nOperators match, go deeper\n";
          return jsonLogic.rule_like(
            jsonLogic.get_values(rule, false),
            jsonLogic.get_values(pattern, false)
          );
        }
      }
      return false; // pattern is logic, rule isn't, can't be eq
    }

    if(Array.isArray(pattern)) {
      if(Array.isArray(rule)) {
        if(pattern.length !== rule.length) {
          return false;
        }
        /*
          Note, array order MATTERS, because we're using this array test logic to consider arguments, where order can matter. (e.g., + is commutative, but '-' or 'if' or 'var' are NOT)
        */
        for(var i = 0; i < pattern.length; i += 1) {
          // If any fail, we fail
          if( ! jsonLogic.rule_like(rule[i], pattern[i])) {
            return false;
          }
        }
        return true; // If they *all* passed, we pass
      }else{
        return false; // Pattern is array, rule isn't
      }
    }

    // Not logic, not array, not a === match for rule.
    return false;
  };

  return jsonLogic;
}));
<commit_msg>Fix in rule<commit_after>/* globals define,module */
/*
Using a Universal Module Loader that should be browser, require, and AMD friendly
http://ricostacruz.com/cheatsheets/umdjs.html
*/
;(function(root, factory) {
  if (typeof define === "function" && define.amd) {
    define(factory);
  } else if (typeof exports === "object") {
    module.exports = factory();
  } else {
    root.jsonLogic = factory();
  }
}(this, function() {
  "use strict";
  /* globals console:false */

  if ( ! Array.isArray) {
    Array.isArray = function(arg) {
      return Object.prototype.toString.call(arg) === "[object Array]";
    };
  }

  /**
   * Return an array that contains no duplicates (original not modified)
   * @param  {array} array   Original reference array
   * @return {array}         New array with no duplicates
   */
  function arrayUnique(array) {
    var a = [];
    for (var i=0, l=array.length; i<l; i++) {
      if (a.indexOf(array[i]) === -1) {
        a.push(array[i]);
      }
    }
    return a;
  }

  var jsonLogic = {};
  var operations = {
    "==": function(a, b) {
      return a == b;
    },
    "===": function(a, b) {
      return a === b;
    },
    "!=": function(a, b) {
      return a != b;
    },
    "!==": function(a, b) {
      return a !== b;
    },
    ">": function(a, b) {
      return a > b;
    },
    ">=": function(a, b) {
      return a >= b;
    },
    "<": function(a, b, c) {
      return (c === undefined) ? a < b : (a < b) && (b < c);
    },
    "<=": function(a, b, c) {
      return (c === undefined) ? a <= b : (a <= b) && (b <= c);
    },
    "!!": function(a) {
      return jsonLogic.truthy(a);
    },
    "!": function(a) {
      return !jsonLogic.truthy(a);
    },
    "%": function(a, b) {
      return a % b;
    },
    "log": function(a) {
      console.log(a); return a;
    },
    "in": function(a, b) {
      if(!b || typeof b.indexOf === "undefined") return false;
      return (b.indexOf(a) !== -1);
    },
    "cat": function() {
      return Array.prototype.join.call(arguments, "");
    },
    "substr":function(source, start, end) {
      if(end < 0){
        // JavaScript doesn't support negative end, this emulates PHP behavior
        var temp = String(source).substr(start);
        return temp.substr(0, temp.length + end);
      }
      return String(source).substr(start, end);
    },
    "+": function() {
      return Array.prototype.reduce.call(arguments, function(a, b) {
        return parseFloat(a, 10) + parseFloat(b, 10);
      }, 0);
    },
    "*": function() {
      return Array.prototype.reduce.call(arguments, function(a, b) {
        return parseFloat(a, 10) * parseFloat(b, 10);
      });
    },
    "-": function(a, b) {
      if(b === undefined) {
        return -a;
      }else{
        return a - b;
      }
    },
    "/": function(a, b) {
      return a / b;
    },
    "min": function() {
      return Math.min.apply(this, arguments);
    },
    "max": function() {
      return Math.max.apply(this, arguments);
    },
    "merge": function() {
      return Array.prototype.reduce.call(arguments, function(a, b) {
        return a.concat(b);
      }, []);
    },
    "var": function(a, b) {
      var not_found = (b === undefined) ? null : b;
      var data = this;
      if(typeof a === "undefined" || a==="" || a===null) {
        return data;
      }
      var sub_props = String(a).split(".");
      for(var i = 0; i < sub_props.length; i++) {
        if(data === null) {
          return not_found;
        }
        // Descending into data
        data = data[sub_props[i]];
        if(data === undefined) {
          return not_found;
        }
      }
      return data;
    },
    "missing": function() {
      /*
      Missing can receive many keys as many arguments, like {"missing:[1,2]}
      Missing can also receive *one* argument that is an array of keys,
      which typically happens if it's actually acting on the output of another command
      (like 'if' or 'merge')
      */

      var missing = [];
      var keys = Array.isArray(arguments[0]) ? arguments[0] : arguments;

      for(var i = 0; i < keys.length; i++) {
        var key = keys[i];
        var value = jsonLogic.apply({"var": key}, this);
        if(value === null || value === "") {
          missing.push(key);
        }
      }

      return missing;
    },
    "missing_some": function(need_count, options) {
      // missing_some takes two arguments, how many (minimum) items must be present, and an array of keys (just like 'missing') to check for presence.
      var are_missing = jsonLogic.apply({"missing": options}, this);

      if(options.length - are_missing.length >= need_count) {
        return [];
      }else{
        return are_missing;
      }
    },
    "method": function(obj, method, args) {
      return obj[method].apply(obj, args);
    },

  };

  jsonLogic.is_logic = function(logic) {
    return (
      typeof logic === "object" && // An object
      logic !== null && // but not null
      ! Array.isArray(logic) && // and not an array
      Object.keys(logic).length === 1 // with exactly one key
    );
  };

  /*
  This helper will defer to the JsonLogic spec as a tie-breaker when different language interpreters define different behavior for the truthiness of primitives.  E.g., PHP considers empty arrays to be falsy, but Javascript considers them to be truthy. JsonLogic, as an ecosystem, needs one consistent answer.

  Spec and rationale here: http://jsonlogic.com/truthy
  */
  jsonLogic.truthy = function(value) {
    if(Array.isArray(value) && value.length === 0) {
      return false;
    }
    return !! value;
  };

  jsonLogic.get_operator = function(logic) {
    return Object.keys(logic)[0];
  };

  jsonLogic.get_values = function(logic) {
    return logic[jsonLogic.get_operator(logic)];
  };

  jsonLogic.apply = function(logic, data) {
    // Does this array contain logic? Only one way to find out.
    if(Array.isArray(logic)) {
      return logic.map(function(l) {
        return jsonLogic.apply(l, data);
      });
    }
    // You've recursed to a primitive, stop!
    if( ! jsonLogic.is_logic(logic) ) {
      return logic;
    }

    data = data || {};

    var op = jsonLogic.get_operator(logic);
    var values = logic[op];
    var i;
    var current;
    var scopedLogic, scopedData, filtered, initial;

    // easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]}
    if( ! Array.isArray(values)) {
      values = [values];
    }

    // 'if', 'and', and 'or' violate the normal rule of depth-first calculating consequents, let each manage recursion as needed.
    if(op === "if" || op == "?:") {
      /* 'if' should be called with a odd number of parameters, 3 or greater
      This works on the pattern:
      if( 0 ){ 1 }else{ 2 };
      if( 0 ){ 1 }else if( 2 ){ 3 }else{ 4 };
      if( 0 ){ 1 }else if( 2 ){ 3 }else if( 4 ){ 5 }else{ 6 };

      The implementation is:
      For pairs of values (0,1 then 2,3 then 4,5 etc)
      If the first evaluates truthy, evaluate and return the second
      If the first evaluates falsy, jump to the next pair (e.g, 0,1 to 2,3)
      given one parameter, evaluate and return it. (it's an Else and all the If/ElseIf were false)
      given 0 parameters, return NULL (not great practice, but there was no Else)
      */
      for(i = 0; i < values.length - 1; i += 2) {
        if( jsonLogic.truthy( jsonLogic.apply(values[i], data) ) ) {
          return jsonLogic.apply(values[i+1], data);
        }
      }
      if(values.length === i+1) return jsonLogic.apply(values[i], data);
      return null;
    }else if(op === "and") { // Return first falsy, or last
      for(i=0; i < values.length; i+=1) {
        current = jsonLogic.apply(values[i], data);
        if( ! jsonLogic.truthy(current)) {
          return current;
        }
      }
      return current; // Last
    }else if(op === "or") {// Return first truthy, or last
      for(i=0; i < values.length; i+=1) {
        current = jsonLogic.apply(values[i], data);
        if( jsonLogic.truthy(current) ) {
          return current;
        }
      }
      return current; // Last

    }else if(op === 'filter'){
      scopedData = jsonLogic.apply(values[0], data);
      scopedLogic = values[1];

      if ( ! Array.isArray(scopedData)) {
          return [];
      }
      // Return only the elements from the array in the first argument,
      // that return truthy when passed to the logic in the second argument.
      // For parity with JavaScript, reindex the returned array
      return scopedData.filter(function(datum){
          return jsonLogic.truthy( jsonLogic.apply(scopedLogic, datum));
      });
  }else if(op === 'map'){
      scopedData = jsonLogic.apply(values[0], data);
      scopedLogic = values[1];

      if ( ! Array.isArray(scopedData)) {
          return [];
      }

      return scopedData.map(function(datum){
          return jsonLogic.apply(scopedLogic, datum);
      });

  }else if(op === 'reduce'){
      scopedData = jsonLogic.apply(values[0], data);
      scopedLogic = values[1];
      initial = typeof values[2] !== 'undefined' ? values[2] : null;

      if ( ! Array.isArray(scopedData)) {
          return initial;
      }

      return scopedData.reduce(
          function(accumulator, current){
              return jsonLogic.apply(
                  scopedLogic,
                  {'current':current, 'accumulator':accumulator}
              );
          },
          initial
      );

    }else if(op === "all") {
      scopedData = jsonLogic.apply(values[0], data);
      scopedLogic = values[1];
      // All of an empty set is false. Note, some and none have correct fallback after the for loop
      if( ! scopedData.length) {
        return false;
      }
      for(i=0; i < scopedData.length; i+=1) {
        if( ! jsonLogic.truthy( jsonLogic.apply(scopedLogic, scopedData[i]) )) {
          return false; // First falsy, short circuit
        }
      }
      return true; // All were truthy
    }else if(op === "none") {
      filtered = jsonLogic.apply({'filter' : values}, data);
      return filtered.length === 0;

    }else if(op === "some") {
      filtered = jsonLogic.apply({'filter' : values}, data);
      return filtered.length > 0;
    }

    // Everyone else gets immediate depth-first recursion
    values = values.map(function(val) {
      return jsonLogic.apply(val, data);
    });

    // The operation is called with "data" bound to its "this" and "values" passed as arguments.
    // Structured commands like % or > can name formal arguments while flexible commands (like missing or merge) can operate on the pseudo-array arguments
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments
    if(typeof operations[op] === "function") {
      return operations[op].apply(data, values);
    }else if(op.indexOf(".") > 0) { // Contains a dot, and not in the 0th position
      var sub_ops = String(op).split(".");
      var operation = operations;
      for(i = 0; i < sub_ops.length; i++) {
        // Descending into operations
        operation = operation[sub_ops[i]];
        if(operation === undefined) {
          throw new Error("Unrecognized operation " + op +
          " (failed at " + sub_ops.slice(0, i+1).join(".") + ")");
        }
      }

      return operation.apply(data, values);
    }

    throw new Error("Unrecognized operation " + op );
  };

  jsonLogic.uses_data = function(logic) {
    var collection = [];

    if( jsonLogic.is_logic(logic) ) {
      var op = jsonLogic.get_operator(logic);
      var values = logic[op];

      if( ! Array.isArray(values)) {
        values = [values];
      }

      if(op === "var") {
        // This doesn't cover the case where the arg to var is itself a rule.
        collection.push(values[0]);
      }else{
        // Recursion!
        values.map(function(val) {
          collection.push.apply(collection, jsonLogic.uses_data(val) );
        });
      }
    }

    return arrayUnique(collection);
  };

  jsonLogic.add_operation = function(name, code) {
    operations[name] = code;
  };

  jsonLogic.rm_operation = function(name) {
    delete operations[name];
  };

  jsonLogic.rule_like = function(rule, pattern) {
    // console.log("Is ". JSON.stringify(rule) . " like " . JSON.stringify(pattern) . "?");
    if(pattern === rule) {
      return true;
    } // TODO : Deep object equivalency?
    if(pattern === "@") {
      return true;
    } // Wildcard!
    if(pattern === "number") {
      return (typeof rule === "number");
    }
    if(pattern === "string") {
      return (typeof rule === "string");
    }
    if(pattern === "array") {
      // !logic test might be superfluous in JavaScript
      return Array.isArray(rule) && ! jsonLogic.is_logic(rule);
    }

    if(jsonLogic.is_logic(pattern)) {
      if(jsonLogic.is_logic(rule)) {
        var pattern_op = jsonLogic.get_operator(pattern);
        var rule_op = jsonLogic.get_operator(rule);

        if(pattern_op === "@" || pattern_op === rule_op) {
        // echo "\nOperators match, go deeper\n";
          return jsonLogic.rule_like(
            jsonLogic.get_values(rule, false),
            jsonLogic.get_values(pattern, false)
          );
        }
      }
      return false; // pattern is logic, rule isn't, can't be eq
    }

    if(Array.isArray(pattern)) {
      if(Array.isArray(rule)) {
        if(pattern.length !== rule.length) {
          return false;
        }
        /*
          Note, array order MATTERS, because we're using this array test logic to consider arguments, where order can matter. (e.g., + is commutative, but '-' or 'if' or 'var' are NOT)
        */
        for(var i = 0; i < pattern.length; i += 1) {
          // If any fail, we fail
          if( ! jsonLogic.rule_like(rule[i], pattern[i])) {
            return false;
          }
        }
        return true; // If they *all* passed, we pass
      }else{
        return false; // Pattern is array, rule isn't
      }
    }

    // Not logic, not array, not a === match for rule.
    return false;
  };

  return jsonLogic;
}));
<|endoftext|>


<commit_before>// bind and events dependencies
var Bind = require("github/jillix/bind")
  , Events = require("github/jillix/events")
  ;

/**
 * private: findValue
 *  This function searches a value in an object.
 *
 *  Arguments
 *    @parent: an object
 *    @dotNot: string
 *
 *  Returns
 *    The value that was found in the object or undefined.
 *
 *  Example
 *    findValue({
 *        a: {
 *            b: {
 *                c: 10
 *            }
 *        }
 *    }, "a.b.c") === 10 // true
 *
 * */
function findValue (parent, dotNot) {

    if (!dotNot || !parent) return undefined;

    var splits = String(dotNot).split(".");
    var value;

    for (var i = 0; i < splits.length; i++) {
        value = parent[splits[i]];
        if (value === undefined) { return undefined };
        if (typeof value === "object") {
            parent = value;
        }
    }

    return value;
}

/**
 *
 *  Login
 *  A generic login module for Mono.
 *
 * */
module.exports = function init (conf) {

    // set self and compute config
    var self = this;
    var config = self.config = processConfig.call(self, conf);
    var $login = $(self.config.ui.selectors.login, self.dom);
    var $logout = $(self.config.ui.selectors.logout, self.dom);
    var $forgot = $(self.config.ui.selectors.forgot, self.dom);

    // hide login, logout, and forgot elements
    $login.hide();
    $logout.hide();
    $forgot.hide();

    $forgot.find(".forgot-link").attr("href", config.forgotPage);

    /**
     *
     *  login#getUserInfo
     *
     *  This function returns the user information from session
     *
     * */
    self.getUserInfo = function (callback) {
        self.link("userInfo", callback);
    };

    // call get user info
    self.getUserInfo(function (err, data) {

        // handle error
        if (err) {
            alert(err);
            return;
        }

        // set user info
        self.userInfo = data;

        // emit event
        self.emit("userInfo", data);

        // if this is true, emit ready now
        if (conf.options.readyOnUserInfo) {
            self.emit("ready");
        }

        // the user is logged in
        if (data) {

            // show logout and hide login elements
            $logout.show();
            $login.hide();
            $forgot.hide();

            // get user info elements
            var $userInfo = $(".userInfo", self.dom);

            // each element with data-key attribute
            $userInfo.find("[data-key]").each(function() {

                // the current element
                var $infoElem = $(this);
                // get data-key attribute
                var key = $infoElem.attr("data-key");

                // if the key exists find the value in data and set the text
                if (key) {
                    $infoElem.text(findValue(data, key));
                }
            });

            // show user info
            $userInfo.show();

            // logout button click handler
            $("#logoutButton", self.dom).on("click", function() {

                // call logout operation and redirect to login
                self.link("logout", function(err, data) {
                    window.location = self.config.loginPage;
                });

                // prevent default browser behavior
                return false;
            });

            return;
        }

        // redirect to login page
        if (window.location.pathname !== self.config.loginPage && window.location.pathname !== self.config.forgotPage && self.config.redirect) {
            window.location = self.config.loginPage;
            return;
        }

        // the user is not logged in
        if (window.location.pathname === self.config.forgotPage) {
            $forgot.show();
            $login.hide();
        } else {
            $login.show();
            $forgot.hide();
        }
        $logout.hide();

        // cache the form and add the submit handler
        $("form#login", self.dom).first().submit(function(e) {
            e.preventDefault();
            submitForm.call(self, $(this));
            return false;
        });
        // cache the form and add the submit handler
        $("form#forgot", self.dom).first().submit(function(e) {
            e.preventDefault();
            submitForm.call(self, $(this));
            return false;
        });
    });

    // if this is true, don't emit ready now
    if (!conf.options.readyOnUserInfo) {
        self.emit("ready");
    }

    // call events
    Events.call(self, self.config);
};

/**
 *
 * private: submitForm
 *  This function submits the login form
 *
 *  Arguments
 *    @form: the form jQuery object
 *
 * */
function submitForm(form) {

    // get self
    var self = this;

    // jQuery wrap
    var $form = $(form);

    // hide and empty the error message
    $form.find(self.config.ui.selectors.error).text("").hide();

    var data = {};

    // gather all the data in the form
    $form.find("input, textarea, select").each(function() {
        var input = $(this);
        switch (input.prop("tagName")) {
            case "INPUT":
            case "TEXTAREA":
            case "SELECT":
                if (input.attr("type") !== "checkbox") {
                    data[input.attr("name")] = input.val().trim();
                } else {
                    data[input.attr("name")] = input.prop("checked");
                }
        }
    });

    var isForgot = $form.attr("id") === "forgot";

    // abandon submission if username or password is missing
    if (!data.username || !isForgot && !data.password) {
        return;
    }

    // call the operation
    self.link(isForgot ? "forgot" : "login", { data: data }, function(error, data) {

        // handle error
        if (error) {

            // get alert jQuery element
            var $alertElem = $form.find(self.config.ui.selectors.error);

            // translate error
            self.emit("message", error, function (err, res) {

                // handle error
                if (err) { return; }

                // set error
                var errMsg = res.message || error;

                // alert element exists
                if ($alertElem.length) {
                    $alertElem.text(errMsg).fadeIn();
                } else {
                    alert(errMsg);
                }
            });

            return;
        }

        // get the success page
        var successPage = self.config.successPage;

        // compute its constructor
        switch (successPage.constructor) {

            // success page is a string
            case String:
                // redirect
                window.location = isForgot ? self.config.loginPage : self.config.successPage;
                break;

            // success page is an object
            case Object:

                // compute type
                switch (successPage.type) {

                    // "function" type
                    case "function":

                        // get the function
                        var functionToCall = findValue(window, successPage.value)

                        // does the function exist?
                        if (typeof functionToCall === "function") {

                            // call the function
                            functionToCall.call(self, {
                                data: data
                            }, function (redirectSuccessPage) {

                                // error must be handled in the custom script
                                window.location = redirectSuccessPage;
                            });
                        } else {

                            // error
                            console.error("Function " + successPage.value + "not found.");
                        }
                        break;

                    // we also accept string value
                    case "string":

                        // if value is a string
                        if (typeof successPage.value === "string") {

                            // redirect
                            window.location = successPage.value;
                        } else {

                            // show error
                            console.error("The value of successPage object must be a string");
                        }
                        break;
                }
                break;
            default:
                console.error("Not a valid successPage value. It must be a string or an object containing the type and the value.");
                break;
        }
    });
}

/**
 *
 * private: processConfig
 *  This function sets the config defaults
 *
 *  Arguments
 *    @config: the config object
 *
 *  Returns
 *    config object
 * */
function processConfig (config) {

    // get self
    var self = this;

    // set defaults
    config.options = config.options || {};
    config.loginPage = config.loginPage || "/login";
    config.successPage = config.successPage || "/";
    config.forgotPage = config.forgotPage || "/forgot";
    config.ui = config.ui || {};
    config.ui.selectors = config.ui.selectors || {};
    config.ui.selectors.error = config.ui.selectors.error || ".alert";
    config.ui.selectors.login = config.ui.selectors.login || ".login";
    config.ui.selectors.logout = config.ui.selectors.logout || ".logout";
    config.ui.selectors.forgot = config.ui.selectors.forgot || ".forgot";

    // return
    return config;
}
<commit_msg>Added logout method<commit_after>// bind and events dependencies
var Bind = require("github/jillix/bind")
  , Events = require("github/jillix/events")
  ;

/**
 * private: findValue
 *  This function searches a value in an object.
 *
 *  Arguments
 *    @parent: an object
 *    @dotNot: string
 *
 *  Returns
 *    The value that was found in the object or undefined.
 *
 *  Example
 *    findValue({
 *        a: {
 *            b: {
 *                c: 10
 *            }
 *        }
 *    }, "a.b.c") === 10 // true
 *
 * */
function findValue (parent, dotNot) {

    if (!dotNot || !parent) return undefined;

    var splits = String(dotNot).split(".");
    var value;

    for (var i = 0; i < splits.length; i++) {
        value = parent[splits[i]];
        if (value === undefined) { return undefined };
        if (typeof value === "object") {
            parent = value;
        }
    }

    return value;
}

/**
 *
 *  Login
 *  A generic login module for Mono.
 *
 * */
module.exports = function init (conf) {

    // set self and compute config
    var self = this;
    var config = self.config = processConfig.call(self, conf);
    var $login = $(self.config.ui.selectors.login, self.dom);
    var $logout = $(self.config.ui.selectors.logout, self.dom);
    var $forgot = $(self.config.ui.selectors.forgot, self.dom);

    // hide login, logout, and forgot elements
    $login.hide();
    $logout.hide();
    $forgot.hide();

    $forgot.find(".forgot-link").attr("href", config.forgotPage);

    /**
     *
     *  login#getUserInfo
     *
     *  This function returns the user information from session
     *
     * */
    self.getUserInfo = function (callback) {
        self.link("userInfo", callback);
    };

    /**
     *
     *  login#logout
     *
     *  Logs out the user
     *
     * */
    self.logout = function () {

        // call logout operation and redirect to login
        self.link("logout", function(err, data) {
            window.location = self.config.loginPage;
        });

        // prevent default browser behavior
        return false;
    };

    // call get user info
    self.getUserInfo(function (err, data) {

        // handle error
        if (err) {
            alert(err);
            return;
        }

        // set user info
        self.userInfo = data;

        // emit event
        self.emit("userInfo", data);

        // if this is true, emit ready now
        if (conf.options.readyOnUserInfo) {
            self.emit("ready");
        }

        // the user is logged in
        if (data) {

            // show logout and hide login elements
            $logout.show();
            $login.hide();
            $forgot.hide();

            // get user info elements
            var $userInfo = $(".userInfo", self.dom);

            // each element with data-key attribute
            $userInfo.find("[data-key]").each(function() {

                // the current element
                var $infoElem = $(this);
                // get data-key attribute
                var key = $infoElem.attr("data-key");

                // if the key exists find the value in data and set the text
                if (key) {
                    $infoElem.text(findValue(data, key));
                }
            });

            // show user info
            $userInfo.show();

            // logout button click handler
            $("#logoutButton", self.dom).on("click", self.logout);

            return;
        }

        // redirect to login page
        if (window.location.pathname !== self.config.loginPage && window.location.pathname !== self.config.forgotPage && self.config.redirect) {
            window.location = self.config.loginPage;
            return;
        }

        // the user is not logged in
        if (window.location.pathname === self.config.forgotPage) {
            $forgot.show();
            $login.hide();
        } else {
            $login.show();
            $forgot.hide();
        }
        $logout.hide();

        // cache the form and add the submit handler
        $("form#login", self.dom).first().submit(function(e) {
            e.preventDefault();
            submitForm.call(self, $(this));
            return false;
        });
        // cache the form and add the submit handler
        $("form#forgot", self.dom).first().submit(function(e) {
            e.preventDefault();
            submitForm.call(self, $(this));
            return false;
        });
    });

    // if this is true, don't emit ready now
    if (!conf.options.readyOnUserInfo) {
        self.emit("ready");
    }

    // call events
    Events.call(self, self.config);
};

/**
 *
 * private: submitForm
 *  This function submits the login form
 *
 *  Arguments
 *    @form: the form jQuery object
 *
 * */
function submitForm(form) {

    // get self
    var self = this;

    // jQuery wrap
    var $form = $(form);

    // hide and empty the error message
    $form.find(self.config.ui.selectors.error).text("").hide();

    var data = {};

    // gather all the data in the form
    $form.find("input, textarea, select").each(function() {
        var input = $(this);
        switch (input.prop("tagName")) {
            case "INPUT":
            case "TEXTAREA":
            case "SELECT":
                if (input.attr("type") !== "checkbox") {
                    data[input.attr("name")] = input.val().trim();
                } else {
                    data[input.attr("name")] = input.prop("checked");
                }
        }
    });

    var isForgot = $form.attr("id") === "forgot";

    // abandon submission if username or password is missing
    if (!data.username || !isForgot && !data.password) {
        return;
    }

    // call the operation
    self.link(isForgot ? "forgot" : "login", { data: data }, function(error, data) {

        // handle error
        if (error) {

            // get alert jQuery element
            var $alertElem = $form.find(self.config.ui.selectors.error);

            // translate error
            self.emit("message", error, function (err, res) {

                // handle error
                if (err) { return; }

                // set error
                var errMsg = res.message || error;

                // alert element exists
                if ($alertElem.length) {
                    $alertElem.text(errMsg).fadeIn();
                } else {
                    alert(errMsg);
                }
            });

            return;
        }

        // get the success page
        var successPage = self.config.successPage;

        // compute its constructor
        switch (successPage.constructor) {

            // success page is a string
            case String:
                // redirect
                window.location = isForgot ? self.config.loginPage : self.config.successPage;
                break;

            // success page is an object
            case Object:

                // compute type
                switch (successPage.type) {

                    // "function" type
                    case "function":

                        // get the function
                        var functionToCall = findValue(window, successPage.value)

                        // does the function exist?
                        if (typeof functionToCall === "function") {

                            // call the function
                            functionToCall.call(self, {
                                data: data
                            }, function (redirectSuccessPage) {

                                // error must be handled in the custom script
                                window.location = redirectSuccessPage;
                            });
                        } else {

                            // error
                            console.error("Function " + successPage.value + "not found.");
                        }
                        break;

                    // we also accept string value
                    case "string":

                        // if value is a string
                        if (typeof successPage.value === "string") {

                            // redirect
                            window.location = successPage.value;
                        } else {

                            // show error
                            console.error("The value of successPage object must be a string");
                        }
                        break;
                }
                break;
            default:
                console.error("Not a valid successPage value. It must be a string or an object containing the type and the value.");
                break;
        }
    });
}

/**
 *
 * private: processConfig
 *  This function sets the config defaults
 *
 *  Arguments
 *    @config: the config object
 *
 *  Returns
 *    config object
 * */
function processConfig (config) {

    // get self
    var self = this;

    // set defaults
    config.options = config.options || {};
    config.loginPage = config.loginPage || "/login";
    config.successPage = config.successPage || "/";
    config.forgotPage = config.forgotPage || "/forgot";
    config.ui = config.ui || {};
    config.ui.selectors = config.ui.selectors || {};
    config.ui.selectors.error = config.ui.selectors.error || ".alert";
    config.ui.selectors.login = config.ui.selectors.login || ".login";
    config.ui.selectors.logout = config.ui.selectors.logout || ".logout";
    config.ui.selectors.forgot = config.ui.selectors.forgot || ".forgot";

    // return
    return config;
}
<|endoftext|>


<commit_before>//
// Maoli.js
// JavaScript helper library for common brazilian business rules
//
// https://github.com/aueda/maoli.js
//
// @author Adriano Ueda
// @version 0.2.3

/*globals
    module
*/
(function (window) {

    "use strict";

    var maoli = window.Maoli || {};

    window.Maoli = maoli;

    maoli.version = "0.2.3";

    if (typeof String.prototype.trim !== "function") {
        String.prototype.trim = function () {

            if (!this) {
                return this;
            }

            return this.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
        };
    }

    maoli.Cep = (function () {
        var regexFlags = "gi",

            regexValidations = {
                loose: "^(\\d{5}\\-\\d{3}|\\d{8})$",
                strict: "^(\\d{5}\\-\\d{3})$"
            },

            validate = function (value, punctuation) {
                var regExp = null;

                value = value || "";

                punctuation = punctuation || "loose";

                value = value.trim();

                if (!regexValidations[punctuation]) {
                    return false;
                }

                regExp = new RegExp(regexValidations[punctuation], regexFlags);

                if (!regExp.test(value)) {
                    return false;
                }

                return true;
            };

        return {
            validate: validate
        };
    }());

    maoli.Cpf = (function () {

        var regexFlags = "gi",

            regexValidations = {
                loose: "^(\\d{3}\\.\\d{3}\\.\\d{3}\\-\\d{2})|(\\d{11})$",
                strict: "^\\d{3}\\.\\d{3}\\.\\d{3}\\-\\d{2}$"
            },

            formatIsValid = function (value, punctuation) {
                var regExp = null;

                value = value || "";

                punctuation = punctuation || "loose";

                value = value.trim();

                if (value === "") {
                    return false;
                }

                if (!regexValidations[punctuation]) {
                    return false;
                }

                regExp = new RegExp(regexValidations[punctuation], regexFlags);

                if (!regExp.test(value)) {
                    return false;
                }

                return true;
            },

            sanitize = function (value) {
                var sanitized = value
                    .trim()
                    .toLowerCase()
                    .replace(/\./g, "")
                    .replace(/\-/g, "");

                return sanitized;
            },

            createChecksum = function (text) {
                var i = 0,
                    sum = 0,
                    digit = 0;

                for (i = text.length - 1; i > -1; i -= 1) {
                    sum += parseInt(text.substring(i, i + 1), 10) * (text.length + 1 - i);
                }

                digit = 11 - (sum % 11);

                if (digit === 10 || digit === 11) {
                    digit = 0;
                }

                return digit;
            },

            validate = function (value, punctuation) {

                var inputDigit1 = 0,
                    inputDigit2 = 0,
                    calcDigit1 = 0,
                    calcDigit2 = 0,
                    isValid = false;

                if (!formatIsValid(value, punctuation)) {
                    return isValid;
                }

                value = sanitize(value);

                inputDigit1 = parseInt(value.substring(9, 10), 10);
                inputDigit2 = parseInt(value.substring(10, 11), 10);

                calcDigit1 = createChecksum(value.substring(0, 9));
                calcDigit2 = createChecksum(value.substring(0, 10));

                isValid = inputDigit1 === calcDigit1 && inputDigit2 === calcDigit2;

                return isValid;
            };

        return {
            validate: validate
        };

    }());

    maoli.Cnpj = (function () {

        var multiplier1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2],
            multiplier2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2],

            regexFlags = "gi",

            regexValidations = {
                loose: "^(\\d{2}\\.\\d{3}\\.\\d{3}\\/\\d{4}\\-\\d{2})|(\\d{14})$",
                strict: "^\\d{2}\\.\\d{3}\\.\\d{3}\\/\\d{4}\\-\\d{2}$"
            },

            formatIsValid = function (value, punctuation) {
                var regExp = null;

                value = value || "";

                punctuation = punctuation || "loose";

                value = value.trim();

                if (value === "") {
                    return false;
                }

                if (!regexValidations[punctuation]) {
                    return false;
                }

                regExp = new RegExp(regexValidations[punctuation], regexFlags);

                if (!regExp.test(value)) {
                    return false;
                }

                return true;
            },

            sanitize = function (value) {
                var sanitized = value
                    .trim()
                    .toLowerCase()
                    .replace(/\./g, "")
                    .replace(/\-/g, "")
                    .replace(/\//g, "");

                return sanitized;
            },

            createChecksum = function (text, multiplier) {
                var i = 0,
                    sum = 0,
                    digit = 0,
                    remainder = 0;

                for (i = text.length - 1; i > -1; i -= 1) {
                    sum += parseInt(text.substring(i, i + 1), 10) * multiplier[i];
                }

                remainder = (sum % 11);
                digit = (remainder < 2) ? 0 : 11 - remainder;

                return digit;
            },

            validate = function (value, punctuation) {
                var inputDigit1 = 0,
                    inputDigit2 = 0,
                    calcDigit1 = 0,
                    calcDigit2 = 0,
                    isValid = false;

                if (!formatIsValid(value, punctuation)) {
                    return isValid;
                }

                value = sanitize(value);

                inputDigit1 = parseInt(value.substring(12, 13), 10);
                inputDigit2 = parseInt(value.substring(13, 14), 10);

                calcDigit1 = createChecksum(value.substring(0, 12), multiplier1);
                calcDigit2 = createChecksum(value.substring(0, 13), multiplier2);

                isValid = inputDigit1 === calcDigit1 && inputDigit2 === calcDigit2;

                return isValid;
            };

        return {
            validate: validate
        };
    }());

    if (module !== undefined) {
        module.exports = maoli;
    }
}(this));<commit_msg>Linted the source.<commit_after>//
// Maoli.js
// JavaScript helper library for common brazilian business rules
//
// https://github.com/aueda/maoli.js
//
// @author Adriano Ueda
// @version 0.2.3

/*globals
    module
*/
/*jslint
    this
*/
(function (window) {

    "use strict";

    var maoli = window.Maoli || {},

        trim = function (value) {

            var result = value;

            result = (typeof String.prototype.trim !== "function")
                ? value.replace(/^\s\s*/, "").replace(/\s\s*$/, "")
                : value.trim();

            return result;
        };

    window.Maoli = maoli;

    maoli.version = "0.2.3";

    maoli.Cep = (function () {
        var regexFlags = "gi",

            regexValidations = {
                loose: "^(\\d{5}-\\d{3}|\\d{8})$",
                strict: "^(\\d{5}-\\d{3})$"
            },

            validate = function (value, punctuation) {
                var regExp = null;

                value = value || "";

                punctuation = punctuation || "loose";

                value = trim(value);

                if (!regexValidations[punctuation]) {
                    return false;
                }

                regExp = new RegExp(regexValidations[punctuation], regexFlags);

                if (!regExp.test(value)) {
                    return false;
                }

                return true;
            };

        return {
            validate: validate
        };
    }());

    maoli.Cpf = (function () {

        var regexFlags = "gi",

            regexValidations = {
                loose: "^(\\d{3}\\.\\d{3}\\.\\d{3}-\\d{2})|(\\d{11})$",
                strict: "^\\d{3}\\.\\d{3}\\.\\d{3}-\\d{2}$"
            },

            formatIsValid = function (value, punctuation) {
                var regExp = null;

                value = value || "";

                punctuation = punctuation || "loose";

                value = trim(value);

                if (value === "") {
                    return false;
                }

                if (!regexValidations[punctuation]) {
                    return false;
                }

                regExp = new RegExp(regexValidations[punctuation], regexFlags);

                if (!regExp.test(value)) {
                    return false;
                }

                return true;
            },

            sanitize = function (value) {
                var sanitized = trim(value)
                    .toLowerCase()
                    .replace(/\./g, "")
                    .replace(/-/g, "");

                return sanitized;
            },

            createChecksum = function (text) {
                var i = text.length - 1,
                    sum = 0,
                    digit = 0;

                while (i > -1) {
                    sum += parseInt(text.substring(i, i + 1), 10) * (text.length + 1 - i);

                    i -= 1;
                }

                digit = 11 - (sum % 11);

                if (digit === 10 || digit === 11) {
                    digit = 0;
                }

                return digit;
            },

            validate = function (value, punctuation) {

                var inputDigit1 = 0,
                    inputDigit2 = 0,
                    calcDigit1 = 0,
                    calcDigit2 = 0,
                    isValid = false;

                if (!formatIsValid(value, punctuation)) {
                    return isValid;
                }

                value = sanitize(value);

                inputDigit1 = parseInt(value.substring(9, 10), 10);
                inputDigit2 = parseInt(value.substring(10, 11), 10);

                calcDigit1 = createChecksum(value.substring(0, 9));
                calcDigit2 = createChecksum(value.substring(0, 10));

                isValid = inputDigit1 === calcDigit1 && inputDigit2 === calcDigit2;

                return isValid;
            };

        return {
            validate: validate
        };

    }());

    maoli.Cnpj = (function () {

        var multiplier1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2],
            multiplier2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2],

            regexFlags = "gi",

            regexValidations = {
                loose: "^(\\d{2}\\.\\d{3}\\.\\d{3}\\/\\d{4}-\\d{2})|(\\d{14})$",
                strict: "^\\d{2}\\.\\d{3}\\.\\d{3}\\/\\d{4}-\\d{2}$"
            },

            formatIsValid = function (value, punctuation) {
                var regExp = null;

                value = value || "";

                punctuation = punctuation || "loose";

                value = trim(value);

                if (value === "") {
                    return false;
                }

                if (!regexValidations[punctuation]) {
                    return false;
                }

                regExp = new RegExp(regexValidations[punctuation], regexFlags);

                if (!regExp.test(value)) {
                    return false;
                }

                return true;
            },

            sanitize = function (value) {
                var sanitized = trim(value)
                    .toLowerCase()
                    .replace(/\./g, "")
                    .replace(/-/g, "")
                    .replace(/\//g, "");

                return sanitized;
            },

            createChecksum = function (text, multiplier) {
                var i = text.length - 1,
                    sum = 0,
                    digit = 0,
                    remainder = 0;

                while (i > -1) {
                    sum += parseInt(text.substring(i, i + 1), 10) * multiplier[i];

                    i -= 1;
                }

                remainder = (sum % 11);
                digit = (remainder < 2)
                    ? 0
                    : 11 - remainder;

                return digit;
            },

            validate = function (value, punctuation) {
                var inputDigit1 = 0,
                    inputDigit2 = 0,
                    calcDigit1 = 0,
                    calcDigit2 = 0,
                    isValid = false;

                if (!formatIsValid(value, punctuation)) {
                    return isValid;
                }

                value = sanitize(value);

                inputDigit1 = parseInt(value.substring(12, 13), 10);
                inputDigit2 = parseInt(value.substring(13, 14), 10);

                calcDigit1 = createChecksum(value.substring(0, 12), multiplier1);
                calcDigit2 = createChecksum(value.substring(0, 13), multiplier2);

                isValid = inputDigit1 === calcDigit1 && inputDigit2 === calcDigit2;

                return isValid;
            };

        return {
            validate: validate
        };
    }());

    if (module !== undefined) {
        module.exports = maoli;
    }
}(this));<|endoftext|>


<commit_before>/* Emulate the native <audio> element with Web Audio API */
Media = {
    _context: new(window.AudioContext || window.webkitAudioContext)(),
    _source: null,
    _buffer: null,
    _callbacks: {
        waiting: function(){},
        playing: function(){},
        timeupdate: function(){},
        visualizerupdate: function(){},
        ended: function(){}
    },
    _startTime: 0,
    _position: 0,
    _playing: false,
    _loop: false,
    autoPlay: false,

    init: function() {
        // The _source node has to be recreated each time it's stopped or
        // paused, so we don't create it here.

        // Create the spliter node
        this._chanSplit = this._context.createChannelSplitter(2);

        // Create the gains for left and right
        this._leftGain = this._context.createGain();
        this._rightGain = this._context.createGain();

        // Create channel merge
        this._chanMerge = this._context.createChannelMerger(2);

        // Create the gain node for the volume control
        this._gainNode = this._context.createGain();

        // Create the analyser node for the visualizer
        this._analyser = this._context.createAnalyser();
        this._analyser.fftSize = 2048;

        // Connect all the nodes in the correct way
        // (Note, source is created and connected later)
        //
        //                 <source>
        //                    |\
        //                    | <analyser>
        //                    |
        //    (split using createChannelSplitter)
        //                    |
        //                   / \
        //                  /   \
        //             leftGain rightGain
        //                  \   /
        //                   \ /
        //                    |
        //     (merge using createChannelMerger)
        //                    |
        //                chanMerge
        //                    |
        //                   gain
        //                    |
        //               destination

        // Connect split channels to left / right gains
        this._chanSplit.connect(this._leftGain,0);
        this._chanSplit.connect(this._rightGain,1);

        // Reconnect the left / right gains to the merge node
        this._leftGain.connect(this._chanMerge, 0, 0);
        this._rightGain.connect(this._chanMerge, 0, 1);

        this._chanMerge.connect(this._gainNode);

        this._gainNode.connect(this._context.destination);
        return this;
    },

    // Load from bufferArray
    loadBuffer: function(buffer, loadedCallback) {
        this.stop();
        this._callbacks.waiting();

        var loadAudioBuffer = function(buffer) {
            this._buffer = buffer;
            loadedCallback();
            if(this.autoPlay) {
                this.play(0);
            }
            this._draw(0);
        }

        var error = function (error) {
            //console.error("failed to decode:", error);
        }
        // Decode the target file into an arrayBuffer and pass it to loadBuffer
        this._context.decodeAudioData(buffer, loadAudioBuffer.bind(this), error);
    },

    /* Properties */
    duration: function() {
        return this._buffer.duration;
    },
    timeElapsed: function() {
        return this._context.currentTime - this._startTime;
    },
    timeRemaining: function() {
        return this.duration() - this.timeElapsed();
    },
    percentComplete: function() {
        return (this.timeElapsed() / this.duration()) * 100;
    },
    channels: function() {
        if(!this._buffer) {
            return 0;
        }
        return this._buffer.numberOfChannels;
    },
    sampleRate: function() {
        return this._buffer.sampleRate;
    },

    /* Actions */
    previous: function() {
        // Implement this when we support playlists
    },
    play: function(position) {
        if(this._playing) {
            // So we don't get a race condition with _position getting overwritten
            this.pause();
        }
        if(this._buffer) {
            this._source = this._context.createBufferSource();
            this._source.buffer = this._buffer;
            this._source.connect(this._analyser);
            this._source.connect(this._chanSplit);

            this._position = typeof position !== 'undefined' ? position : this._position;
            this._startTime = this._context.currentTime - this._position;
            this._source.start(0, this._position);
            this._playing = true;
            this._callbacks.playing();
        }
    },
    pause: function() {
        if(!this._playing) {
            return;
        }
        this._silence();
        this._position = this._context.currentTime - this._startTime;
    },

    stop: function() {
        this._silence();
        this._position = 0;
    },

    _silence: function() {
        if(this._source) {
            this._source.stop(0);
            this._source = null;
        }
        this._playing = false;
    },

    /* Actions with arguments */
    seekToPercentComplete: function(percent) {
        var seekTime = this.duration() * (percent / 100);
        this.seekToTime(seekTime);
    },

    // From 0-1
    setVolume: function(volume) {
        this._gainNode.gain.value = volume;
    },

    // From -100 to 100
    setBalance: function(balance) {
        var changeVal = Math.abs(balance) / 100;

        // Hack for Firefox. Having either channel set to 0 seems to revert us
        // to equal balance.
        var changeVal = changeVal - .00000001;

        if(balance > 0) { // Right
            this._leftGain.gain.value = 1 - changeVal;
            this._rightGain.gain.value = 1;
        }
        else if(balance < 0) // Left
        {
            this._leftGain.gain.value = 1;
            this._rightGain.gain.value = 1 - changeVal;
        }
        else // Center
        {
            this._leftGain.gain.value = 1;
            this._rightGain.gain.value = 1;
        }
    },

    toggleRepeat: function() {
        this._loop = !this._loop;
    },

    /* Listeners */
    addEventListener: function(event, callback) {
        this._callbacks[event] = callback;
    },

    seekToTime: function(time) {
        // Make sure we are within range
        time = Math.min(time, this.duration());
        time = Math.max(time, 0);
        this.play(time);
    },

    // There is probably a more reasonable way to do this, rather than having
    // it always running.
    _draw: function() {
        if(this._playing) {
            this._updatePosition();
            this._callbacks.timeupdate();

            // _updatePosition might have stoped the playing
            if(this._playing) {
                this._callbacks.visualizerupdate(this._analyser);
            }
        }
        window.requestAnimationFrame(this._draw.bind(this));
    },

    _updatePosition: function() {
        this._position = this._context.currentTime - this._startTime;
        if(this._position >= this._buffer.duration) {
            // Idealy we could use _source.loop, but it makes updating the position tricky
            if(this._loop) {
                this.play(0);
            } else {
                this.stop();
                this._callbacks.ended();
            }
        }
        return this._position;
    }
}
<commit_msg>Use precalculated postion for timeElapsed<commit_after>/* Emulate the native <audio> element with Web Audio API */
Media = {
    _context: new(window.AudioContext || window.webkitAudioContext)(),
    _source: null,
    _buffer: null,
    _callbacks: {
        waiting: function(){},
        playing: function(){},
        timeupdate: function(){},
        visualizerupdate: function(){},
        ended: function(){}
    },
    _startTime: 0,
    _position: 0,
    _playing: false,
    _loop: false,
    autoPlay: false,

    init: function() {
        // The _source node has to be recreated each time it's stopped or
        // paused, so we don't create it here.

        // Create the spliter node
        this._chanSplit = this._context.createChannelSplitter(2);

        // Create the gains for left and right
        this._leftGain = this._context.createGain();
        this._rightGain = this._context.createGain();

        // Create channel merge
        this._chanMerge = this._context.createChannelMerger(2);

        // Create the gain node for the volume control
        this._gainNode = this._context.createGain();

        // Create the analyser node for the visualizer
        this._analyser = this._context.createAnalyser();
        this._analyser.fftSize = 2048;

        // Connect all the nodes in the correct way
        // (Note, source is created and connected later)
        //
        //                 <source>
        //                    |\
        //                    | <analyser>
        //                    |
        //    (split using createChannelSplitter)
        //                    |
        //                   / \
        //                  /   \
        //             leftGain rightGain
        //                  \   /
        //                   \ /
        //                    |
        //     (merge using createChannelMerger)
        //                    |
        //                chanMerge
        //                    |
        //                   gain
        //                    |
        //               destination

        // Connect split channels to left / right gains
        this._chanSplit.connect(this._leftGain,0);
        this._chanSplit.connect(this._rightGain,1);

        // Reconnect the left / right gains to the merge node
        this._leftGain.connect(this._chanMerge, 0, 0);
        this._rightGain.connect(this._chanMerge, 0, 1);

        this._chanMerge.connect(this._gainNode);

        this._gainNode.connect(this._context.destination);
        return this;
    },

    // Load from bufferArray
    loadBuffer: function(buffer, loadedCallback) {
        this.stop();
        this._callbacks.waiting();

        var loadAudioBuffer = function(buffer) {
            this._buffer = buffer;
            loadedCallback();
            if(this.autoPlay) {
                this.play(0);
            }
            this._draw(0);
        }

        var error = function (error) {
            //console.error("failed to decode:", error);
        }
        // Decode the target file into an arrayBuffer and pass it to loadBuffer
        this._context.decodeAudioData(buffer, loadAudioBuffer.bind(this), error);
    },

    /* Properties */
    duration: function() {
        return this._buffer.duration;
    },
    timeElapsed: function() {
        return this._position;
    },
    timeRemaining: function() {
        return this.duration() - this.timeElapsed();
    },
    percentComplete: function() {
        return (this.timeElapsed() / this.duration()) * 100;
    },
    channels: function() {
        if(!this._buffer) {
            return 0;
        }
        return this._buffer.numberOfChannels;
    },
    sampleRate: function() {
        return this._buffer.sampleRate;
    },

    /* Actions */
    previous: function() {
        // Implement this when we support playlists
    },
    play: function(position) {
        if(this._playing) {
            // So we don't get a race condition with _position getting overwritten
            this.pause();
        }
        if(this._buffer) {
            this._source = this._context.createBufferSource();
            this._source.buffer = this._buffer;
            this._source.connect(this._analyser);
            this._source.connect(this._chanSplit);

            this._position = typeof position !== 'undefined' ? position : this._position;
            this._startTime = this._context.currentTime - this._position;
            this._source.start(0, this._position);
            this._playing = true;
            this._callbacks.playing();
        }
    },
    pause: function() {
        if(!this._playing) {
            return;
        }
        this._silence();
        this._position = this._context.currentTime - this._startTime;
    },

    stop: function() {
        this._silence();
        this._position = 0;
    },

    _silence: function() {
        if(this._source) {
            this._source.stop(0);
            this._source = null;
        }
        this._playing = false;
    },

    /* Actions with arguments */
    seekToPercentComplete: function(percent) {
        var seekTime = this.duration() * (percent / 100);
        this.seekToTime(seekTime);
    },

    // From 0-1
    setVolume: function(volume) {
        this._gainNode.gain.value = volume;
    },

    // From -100 to 100
    setBalance: function(balance) {
        var changeVal = Math.abs(balance) / 100;

        // Hack for Firefox. Having either channel set to 0 seems to revert us
        // to equal balance.
        var changeVal = changeVal - .00000001;

        if(balance > 0) { // Right
            this._leftGain.gain.value = 1 - changeVal;
            this._rightGain.gain.value = 1;
        }
        else if(balance < 0) // Left
        {
            this._leftGain.gain.value = 1;
            this._rightGain.gain.value = 1 - changeVal;
        }
        else // Center
        {
            this._leftGain.gain.value = 1;
            this._rightGain.gain.value = 1;
        }
    },

    toggleRepeat: function() {
        this._loop = !this._loop;
    },

    /* Listeners */
    addEventListener: function(event, callback) {
        this._callbacks[event] = callback;
    },

    seekToTime: function(time) {
        // Make sure we are within range
        time = Math.min(time, this.duration());
        time = Math.max(time, 0);
        this.play(time);
    },

    // There is probably a more reasonable way to do this, rather than having
    // it always running.
    _draw: function() {
        if(this._playing) {
            this._updatePosition();
            this._callbacks.timeupdate();

            // _updatePosition might have stoped the playing
            if(this._playing) {
                this._callbacks.visualizerupdate(this._analyser);
            }
        }
        window.requestAnimationFrame(this._draw.bind(this));
    },

    _updatePosition: function() {
        this._position = this._context.currentTime - this._startTime;
        if(this._position >= this._buffer.duration) {
            // Idealy we could use _source.loop, but it makes updating the position tricky
            if(this._loop) {
                this.play(0);
            } else {
                this.stop();
                this._callbacks.ended();
            }
        }
        return this._position;
    }
}
<|endoftext|>