<commit_before>/*
 * CAMXES.JS POSTPROCESSOR
 * Created by Ilmen (ilmen.pokebip <at> gmail.com) on 2013-08-16.
 * Last change: 2016-09-06.
 *
 * Entry point: camxes_postprocessing(input, mode)
 * Arguments:
 *    -- input: [array] camxes' parse tree output
 *    -- mode:  [number] output mode flag
 *         If mode > 7, displaying spaces is enabled and mode's value is
 *         truncated so mode <= 7. Then, depending on the value of mode,
 *         the following options are set:
 *         0 = Raw output (no change)
 *         1 = Condensed
 *         2 = Prettified
 *         3 = Prettified + selma'o
 *         4 = Prettified + selma'o + bridi parts
 *         5 = Prettified - famyma'o
 *         6 = Prettified - famyma'o + selma'o
 *         7 = Prettified - famyma'o + selma'o + bridi parts
 *
 * Return value:
 *       [string] postprocessed version of camxes' output
 */

/*
 * Function list:
 *   -- camxes_postprocessing(text, mode)
 *   -- new_postprocessor(input, no_morpho, with_selmaho, with_terminator)
 *   -- prune_unwanted_nodes(tree, is_wanted_node)
 *   -- prefix_wordclass(tree, replacements)
 *   -- remove_morphology(pt)
 *   -- is_target_node(n)
 *   -- join_expr(n)
 *   -- among(v, s)
 *   -- is_selmaho(v)
 *   -- prettify_brackets(str)
 *   -- str_print_uint(val, charset)
 *   -- str_replace(str, pos, len, sub)
 *   -- chr_check(chr, list)
 *   -- is_string(v)
 *   -- is_array(v)
 */

if (typeof alert !== 'function')
    alert = console.log; // For Node.js

/*
 * Version of transition between the older string processing and the newer
 * parse tree processing.
 * The below function's content is temporary and allows choosing which of the
 * two postprocessor (the older, the newer or even both) to use.
 * If the new postprocessor proves satisfactory, the older postprocessor's code
 * will be entirely removed.
 */
function camxes_postprocessing(input, mode) {
    if (!is_array(input))
        return "Postprocessor error: invalid input type for the first argument. "
             + "It should be of type 'array', but the argument given is of type '" + typeof input + "'."
             + (is_string(input) ? "\n\nThe new postprocessor doesn't allow anymore string inputs. "
             + "Please check the parse tree produced by the parser hasn't been converted to string "
             + "before being passed to the postprocessor." : "");
    var with_spaces = mode & 8;
    var without_morphology = !(mode & 16);
    mode = mode % 8;
    var with_selmaho = (mode != 2 && mode != 5);
    var with_nodes_labels = (mode == 4 || mode == 7);
    var without_terminator = (mode >= 5);
    if (without_morphology)
        input = remove_morphology(input); // Deleting morphology nodes.
    var output;
    if (mode <= 1) {
        output = JSON.stringify(input, undefined, mode == 0 ? 2 : 0);
    } else {
        input = new_postprocessor(input, without_morphology, with_spaces,
                                  with_selmaho, !without_terminator);
        output = JSON.stringify(input);
        output = output.replace(/\"/gm, "");
        output = output.replace(/,/gm, " ");
    }
    // Replacing "spaces" with "_":
    output = output.replace(/([ \[\],])(initial_)?spaces(?=[ \[\],])/gm, "$1_");
    // Bracket prettification:
    output = prettify_brackets(output);
	return output;
}

// ====== NEW POSTPROCESSOR ====== //

function new_postprocessor(input, no_morpho, with_spaces, with_selmaho, with_terminator) {
    var filter;
    var wanted_nodes = with_spaces ? ["initial_spaces", "dot_star"] : ["dot_star"];
    if (no_morpho) {
        filter = function (v,b) { return (with_selmaho ?
                  among(v, wanted_nodes.concat(["cmevla", "gismu", "lujvo", "fuhivla"]))
                  || (is_selmaho(v) && (with_terminator || !b))
                  : among(v, wanted_nodes)
                  || (is_selmaho(v) && b && with_terminator)); };
    } else {
        filter = function (v,b) { return among(v, wanted_nodes) ||
                  (with_selmaho ? (is_selmaho(v) && (with_terminator || !b))
                  : is_selmaho(v) && b && with_terminator); };
    }
    input = prune_unwanted_nodes(input, filter);
    if (input === null) return [];
    if (with_selmaho && no_morpho) {
        var replacements = [["cmene", "C"], ["cmevla", "C"], ["gismu", "G"],
                            ["lujvo", "L"], ["fuhivla", "Z"]];
        input = prefix_wordclass(input, replacements);
        if (is_string(input)) input = [input];
    }
    return input;
}

function prune_unwanted_nodes(tree, is_wanted_node) {
    if (is_string(tree)) return tree;
    if (!is_array(tree)) throw "ERR";
    if (tree.length == 0) return null;
    var no_label = is_array(tree[0]);
    var k = 0;
    var i = no_label ? 0 : 1;
    while (i < tree.length) {
        tree[i] = prune_unwanted_nodes(tree[i], is_wanted_node);
        if (tree[i]) {
            k++;
            i++;
        } else tree.splice(i, 1);
    }
    if (!no_label) {
        if (!is_wanted_node(tree[0], tree.length == 1)) tree.splice(0, 1);
        else k++;
    }
    if (k == 1) return tree[0];
    else return (k > 0) ? tree : null;
}

function prefix_wordclass(tree, replacements) {
    if (tree.length == 2 && is_string(tree[0]) && is_string(tree[1])) {
        var i = 0;
        while (i < replacements.length) {
            if (tree[0] == replacements[i][0]) {
                tree[0] = replacements[i][1];
                break;
            }
            i++;
        }
        return tree[0] + ':' + tree[1];
    }
    var i = 0;
    while (i < tree.length) {
        if (is_array(tree[i]))
            tree[i] = prefix_wordclass(tree[i], replacements);
        i++;
    }
    return tree;
}

// ====== MORPHOLOGY REMOVAL ====== //

/*
 * remove_morphology(parse_tree)
 *
 * This function takes a parse tree, and joins the expressions of the following
 * nodes:
 * "cmevla", "gismu_2", "lujvo", "fuhivla", "spaces"
 * as well as any selmaho node (e.g. "KOhA").
 *
 * (This is essentially a copy of process_parse_tree.js.)
 */

function remove_morphology(pt) {
    if (pt.length < 1) return [];
    var i;
    /* Sometimes nodes have no label and have instead an array as their first
       element. */
    if (is_array(pt[0])) i = 0;
    else { // The first element is a label (node name).
        // Let's check if this node is a candidate for our pruning.
        if (is_target_node(pt)) {
            /* We join recursively all the terminal elements (letters) in this
             * node and its child nodes, and put the resulting string in the #1
             * slot of the array; afterwards we delete all the remaining elements
             * (their terminal values have been concatenated into pt[1]). */
            pt[1] = join_expr(pt);
            // If pt[1] contains an empty string, let's delete it as well:
            pt.splice((pt[1] == "") ? 1 : 2);
            return pt;
        }
        i = 1;
    }
    /* If we've reached here, then this node is not a target for pruning, so let's
       do recursion into its child nodes. */
    while (i < pt.length) {
        remove_morphology(pt[i]);
        i++;
    }
    return pt;
}

/* Checks whether the argument node is a target for pruning. */
function is_target_node(n) {
    return (among(n[0], ["cmevla", "gismu", "lujvo", "fuhivla", "initial_spaces"])
            || is_selmaho(n[0]));
}

/* This function returns the string resulting from the recursive concatenation of
 * all the leaf elements of the parse tree argument (except node names). */
function join_expr(n) {
    if (n.length < 1) return "";
    var s = "";
    var i = is_array(n[0]) ? 0 : 1;
    while (i < n.length) {
        s += is_string(n[i]) ? n[i] : join_expr(n[i]);
        i++;
    }
    return s;
}

function among(v, s) {
    var i = 0;
    while (i < s.length) if (s[i++] == v) return true;
    return false;
}

function is_selmaho(v) {
    if (!is_string(v)) return false;
    return (0 == v.search(/^[IUBCDFGJKLMNPRSTVXZ]?([AEIOUY]|(AI|EI|OI|AU))(h([AEIOUY]|(AI|EI|OI|AU)))*$/g));
}

/* ================== */
/* ===  Routines  === */
/* ================== */

function prettify_brackets(str) {
	var open_brackets = ["(", "[", "{", "<"];
	var close_brackets = [")", "]", "}", ">"];
	var brackets_number = 4;
//	var numset = ['0','1','2','3','4','5','6','7','8','9'];
	var numset = ['\u2070','\u00b9','\u00b2','\u00b3','\u2074',
	              '\u2075','\u2076','\u2077','\u2078','\u2079'];
	var i = 0;
	var floor = 0;
	while (i < str.length) {
		if (str[i] == '[') {
			var n = floor % brackets_number;
			var num = (floor && !n) ?
				str_print_uint(floor / brackets_number, numset) : "";
			str = str_replace(str, i, 1, open_brackets[n] + num);
			floor++;
		} else if (str[i] == ']') {
			floor--;
			var n = floor % brackets_number;
			var num = (floor && !n) ?
				str_print_uint(floor / brackets_number, numset) : "";
			str = str_replace(str, i, 1, num + close_brackets[n]);
		}
		i++;
	}
	return str;
}

function str_print_uint(val, charset) {
	// 'charset' must be a character array.
	var radix = charset.length;
	var str = "";
	val -= val % 1;  // No float allowed
	while (val >= 1) {
		str = charset[val % radix] + str;
		val /= radix;
		val -= val % 1;
	}
	return str;
}

function str_replace(str, pos, len, sub) {
	if (pos < str.length) {
		if (pos + len >= str.length) len -= pos + len - str.length;
		return str.substring(0, pos) + sub + str.substring(pos + len);
	} else return str;
}

function chr_check(chr, list) {
	var i = 0;
	if (!is_string(list)) return false;
	do if (chr == list[i]) return true; while (i++ < list.length);
	return false;
} // Currently unused.

function is_string(v) {
    // return $.type(v) === "string";
    return Object.prototype.toString.call(v) === '[object String]';
}

function is_array(v) {
    // return $.type(v) === "array";
    return Object.prototype.toString.call(v) === '[object Array]';
}

if (typeof module !== 'undefined')
    module.exports.postprocessing = camxes_postprocessing;

<commit_msg>camxes_postproc.js: Adding 'ga_clause' and 'gu_clause' to the morphology pruning target list.<commit_after>/*
 * CAMXES.JS POSTPROCESSOR
 * Created by Ilmen (ilmen.pokebip <at> gmail.com) on 2013-08-16.
 * Last change: 2016-09-06.
 *
 * Entry point: camxes_postprocessing(input, mode)
 * Arguments:
 *    -- input: [array] camxes' parse tree output
 *    -- mode:  [number] output mode flag
 *         If mode > 7, displaying spaces is enabled and mode's value is
 *         truncated so mode <= 7. Then, depending on the value of mode,
 *         the following options are set:
 *         0 = Raw output (no change)
 *         1 = Condensed
 *         2 = Prettified
 *         3 = Prettified + selma'o
 *         4 = Prettified + selma'o + bridi parts
 *         5 = Prettified - famyma'o
 *         6 = Prettified - famyma'o + selma'o
 *         7 = Prettified - famyma'o + selma'o + bridi parts
 *
 * Return value:
 *       [string] postprocessed version of camxes' output
 */

/*
 * Function list:
 *   -- camxes_postprocessing(text, mode)
 *   -- new_postprocessor(input, no_morpho, with_selmaho, with_terminator)
 *   -- prune_unwanted_nodes(tree, is_wanted_node)
 *   -- prefix_wordclass(tree, replacements)
 *   -- remove_morphology(pt)
 *   -- is_target_node(n)
 *   -- join_expr(n)
 *   -- among(v, s)
 *   -- is_selmaho(v)
 *   -- prettify_brackets(str)
 *   -- str_print_uint(val, charset)
 *   -- str_replace(str, pos, len, sub)
 *   -- chr_check(chr, list)
 *   -- is_string(v)
 *   -- is_array(v)
 */

if (typeof alert !== 'function')
    alert = console.log; // For Node.js

/*
 * Version of transition between the older string processing and the newer
 * parse tree processing.
 * The below function's content is temporary and allows choosing which of the
 * two postprocessor (the older, the newer or even both) to use.
 * If the new postprocessor proves satisfactory, the older postprocessor's code
 * will be entirely removed.
 */
function camxes_postprocessing(input, mode) {
    if (!is_array(input))
        return "Postprocessor error: invalid input type for the first argument. "
             + "It should be of type 'array', but the argument given is of type '" + typeof input + "'."
             + (is_string(input) ? "\n\nThe new postprocessor doesn't allow anymore string inputs. "
             + "Please check the parse tree produced by the parser hasn't been converted to string "
             + "before being passed to the postprocessor." : "");
    var with_spaces = mode & 8;
    var without_morphology = !(mode & 16);
    mode = mode % 8;
    var with_selmaho = (mode != 2 && mode != 5);
    var with_nodes_labels = (mode == 4 || mode == 7);
    var without_terminator = (mode >= 5);
    if (without_morphology)
        input = remove_morphology(input); // Deleting morphology nodes.
    var output;
    if (mode <= 1) {
        output = JSON.stringify(input, undefined, mode == 0 ? 2 : 0);
    } else {
        input = new_postprocessor(input, without_morphology, with_spaces,
                                  with_selmaho, !without_terminator);
        output = JSON.stringify(input);
        output = output.replace(/\"/gm, "");
        output = output.replace(/,/gm, " ");
    }
    // Replacing "spaces" with "_":
    output = output.replace(/([ \[\],])(initial_)?spaces(?=[ \[\],])/gm, "$1_");
    // Bracket prettification:
    output = prettify_brackets(output);
	return output;
}

// ====== NEW POSTPROCESSOR ====== //

function new_postprocessor(input, no_morpho, with_spaces, with_selmaho, with_terminator) {
    var filter;
    var wanted_nodes = with_spaces ? ["initial_spaces", "dot_star"] : ["dot_star"];
    if (no_morpho) {
        filter = function (v,b) { return (with_selmaho ?
                  among(v, wanted_nodes.concat(["cmevla", "gismu", "lujvo", "fuhivla"]))
                  || (is_selmaho(v) && (with_terminator || !b))
                  : among(v, wanted_nodes)
                  || (is_selmaho(v) && b && with_terminator)); };
    } else {
        filter = function (v,b) { return among(v, wanted_nodes) ||
                  (with_selmaho ? (is_selmaho(v) && (with_terminator || !b))
                  : is_selmaho(v) && b && with_terminator); };
    }
    input = prune_unwanted_nodes(input, filter);
    if (input === null) return [];
    if (with_selmaho && no_morpho) {
        var replacements = [["cmene", "C"], ["cmevla", "C"], ["gismu", "G"],
                            ["lujvo", "L"], ["fuhivla", "Z"]];
        input = prefix_wordclass(input, replacements);
        if (is_string(input)) input = [input];
    }
    return input;
}

function prune_unwanted_nodes(tree, is_wanted_node) {
    if (is_string(tree)) return tree;
    if (!is_array(tree)) throw "ERR";
    if (tree.length == 0) return null;
    var no_label = is_array(tree[0]);
    var k = 0;
    var i = no_label ? 0 : 1;
    while (i < tree.length) {
        tree[i] = prune_unwanted_nodes(tree[i], is_wanted_node);
        if (tree[i]) {
            k++;
            i++;
        } else tree.splice(i, 1);
    }
    if (!no_label) {
        if (!is_wanted_node(tree[0], tree.length == 1)) tree.splice(0, 1);
        else k++;
    }
    if (k == 1) return tree[0];
    else return (k > 0) ? tree : null;
}

function prefix_wordclass(tree, replacements) {
    if (tree.length == 2 && is_string(tree[0]) && is_string(tree[1])) {
        var i = 0;
        while (i < replacements.length) {
            if (tree[0] == replacements[i][0]) {
                tree[0] = replacements[i][1];
                break;
            }
            i++;
        }
        return tree[0] + ':' + tree[1];
    }
    var i = 0;
    while (i < tree.length) {
        if (is_array(tree[i]))
            tree[i] = prefix_wordclass(tree[i], replacements);
        i++;
    }
    return tree;
}

// ====== MORPHOLOGY REMOVAL ====== //

/*
 * remove_morphology(parse_tree)
 *
 * This function takes a parse tree, and joins the expressions of the following
 * nodes:
 * "cmevla", "gismu_2", "lujvo", "fuhivla", "spaces"
 * as well as any selmaho node (e.g. "KOhA").
 *
 * (This is essentially a copy of process_parse_tree.js.)
 */

function remove_morphology(pt) {
    if (pt.length < 1) return [];
    var i;
    /* Sometimes nodes have no label and have instead an array as their first
       element. */
    if (is_array(pt[0])) i = 0;
    else { // The first element is a label (node name).
        // Let's check if this node is a candidate for our pruning.
        if (is_target_node(pt)) {
            /* We join recursively all the terminal elements (letters) in this
             * node and its child nodes, and put the resulting string in the #1
             * slot of the array; afterwards we delete all the remaining elements
             * (their terminal values have been concatenated into pt[1]). */
            pt[1] = join_expr(pt);
            // If pt[1] contains an empty string, let's delete it as well:
            pt.splice((pt[1] == "") ? 1 : 2);
            return pt;
        }
        i = 1;
    }
    /* If we've reached here, then this node is not a target for pruning, so let's
       do recursion into its child nodes. */
    while (i < pt.length) {
        remove_morphology(pt[i]);
        i++;
    }
    return pt;
}

/* Checks whether the argument node is a target for pruning. */
function is_target_node(n) {
    return (among(n[0], ["cmevla", "gismu", "lujvo", "fuhivla", "initial_spaces", "ga_clause", "gu_clause"])
            || is_selmaho(n[0]));
}

/* This function returns the string resulting from the recursive concatenation of
 * all the leaf elements of the parse tree argument (except node names). */
function join_expr(n) {
    if (n.length < 1) return "";
    var s = "";
    var i = is_array(n[0]) ? 0 : 1;
    while (i < n.length) {
        s += is_string(n[i]) ? n[i] : join_expr(n[i]);
        i++;
    }
    return s;
}

function among(v, s) {
    var i = 0;
    while (i < s.length) if (s[i++] == v) return true;
    return false;
}

function is_selmaho(v) {
    if (!is_string(v)) return false;
    return (0 == v.search(/^[IUBCDFGJKLMNPRSTVXZ]?([AEIOUY]|(AI|EI|OI|AU))(h([AEIOUY]|(AI|EI|OI|AU)))*$/g));
}

/* ================== */
/* ===  Routines  === */
/* ================== */

function prettify_brackets(str) {
	var open_brackets = ["(", "[", "{", "<"];
	var close_brackets = [")", "]", "}", ">"];
	var brackets_number = 4;
//	var numset = ['0','1','2','3','4','5','6','7','8','9'];
	var numset = ['\u2070','\u00b9','\u00b2','\u00b3','\u2074',
	              '\u2075','\u2076','\u2077','\u2078','\u2079'];
	var i = 0;
	var floor = 0;
	while (i < str.length) {
		if (str[i] == '[') {
			var n = floor % brackets_number;
			var num = (floor && !n) ?
				str_print_uint(floor / brackets_number, numset) : "";
			str = str_replace(str, i, 1, open_brackets[n] + num);
			floor++;
		} else if (str[i] == ']') {
			floor--;
			var n = floor % brackets_number;
			var num = (floor && !n) ?
				str_print_uint(floor / brackets_number, numset) : "";
			str = str_replace(str, i, 1, num + close_brackets[n]);
		}
		i++;
	}
	return str;
}

function str_print_uint(val, charset) {
	// 'charset' must be a character array.
	var radix = charset.length;
	var str = "";
	val -= val % 1;  // No float allowed
	while (val >= 1) {
		str = charset[val % radix] + str;
		val /= radix;
		val -= val % 1;
	}
	return str;
}

function str_replace(str, pos, len, sub) {
	if (pos < str.length) {
		if (pos + len >= str.length) len -= pos + len - str.length;
		return str.substring(0, pos) + sub + str.substring(pos + len);
	} else return str;
}

function chr_check(chr, list) {
	var i = 0;
	if (!is_string(list)) return false;
	do if (chr == list[i]) return true; while (i++ < list.length);
	return false;
} // Currently unused.

function is_string(v) {
    // return $.type(v) === "string";
    return Object.prototype.toString.call(v) === '[object String]';
}

function is_array(v) {
    // return $.type(v) === "array";
    return Object.prototype.toString.call(v) === '[object Array]';
}

if (typeof module !== 'undefined')
    module.exports.postprocessing = camxes_postprocessing;

<|endoftext|>


<commit_before>var formatDate, weekDays, formatDateNice, monthNames;

weekDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
monthNames = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ];

// Friday 2/20/2015 20:29:22
formatDate = function(date) {
  var hms;
  hms = date.toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, "$1");
  return weekDays[date.getDay()] + " " + date.getMonth() + "/" + date.getDate() + "/" + date.getFullYear() + " " + hms;
};

// February 7th, 2015
formatDateNice = function(date) {
  var hms;
  hms = date.toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, "$1");
  return monthNames[(date.getMonth())] + " " + date.getDate() + ", " + date.getFullYear();
};

var filters = ['curated', 'trending', 'starred', 'newest'];
Session.setDefault('filterValue', filters[0]); // this must correspond to the first thing in the dropdown

Template.home.helpers({
  user: function() {
    return Meteor.user();
  },
  filterOpen: function() {
    return Session.get("filterOpen");
  },
  filter: function() {
    return Session.get("filter");
  },
  category: function() {
    return Session.get("category");
  }
});

Template.home.events({
  "click .logo-title a": function(e, t) {
    // reset search query
    Session.set('storySearchQuery', null);

    // reset filter
    Session.set('filterValue', filters[0]);
    t.$("select.filters-select").val(filters[0]);
    t.$("select.filters-select").selectOrDie("update");

  }
});

Template.search.onCreated(function() {
  this.autorun(function(){
    if(!Session.get('storySearchQuery')){
      $("input").val(null);
    }
  })

});

Template.search.onRendered(function() {
  if(this.data.slim){
    this.$("button").hide(); // hack to hide button
  } else {
    var storySearchQuery;
    if(storySearchQuery = Session.get('storySearchQuery')){
      this.$("input").val(storySearchQuery);
    }
  }
});

Template.search.helpers({
  showClearSearch: function(){
    return Session.get('storySearchQuery');
  }
});

Template.search.events({
  'click .clear-search': function(e, t){
    // this is the business logic
    Meteor.defer(function(){
      return Session.set('storySearchQuery', null);
    });

    // do this so the ui is snappy
    t.$("input").val(null);
    t.$("button").hide();

  },
  'keydown': function(e, t){
    if(t.data.slim){
      t.$("button").show(); // compensate for hack from above
    }
  }
});

Template.categories.helpers({
  categories: function() {
    return ['all', 'news', 'history', 'art', 'technology', 'politics', 'e-sports', 'music', 'gaming', 'sponsored'];
  },
  selected: function() {
    return Session.equals("category", this.toString());
  }
});

Template.categories.events({
  "click li": function(d) {
    var srcE;
    srcE = d.srcElement ? d.srcElement : d.target;
    return Session.set('category', $(srcE).data('category'));
  }
});

Template.filters.onRendered(function() {
  var options = {};
  if(this.data.slim){
    options.placeholder = "Explore";
  } else {
    var filterValue;
    if(filterValue = Session.get('filterValue')){
      $("select").val(filterValue);
    }
  }
  $("select").selectOrDie(options);

});

Template.filters.helpers({
  filters: function() {
    return _.map(filters, function(filter){
      return {
        value: filter,
        label: filter === 'curated' ? 'FOLD Picks' : _s.capitalize(filter)
      }
    })
  },
  conditionallySelected: function(){
    return Session.equals('filterValue', this.toString()) ? 'selected' : '';
  }
});

Template.filters.events({
  "change select": function(e, t) {
    var filterValue = $(e.target).val();
    Session.set('filterValue', filterValue);
    Session.set('storySearchQuery', null);
    if(t.data.slim){
      Router.go('/');
    }
    trackEvent('Select filter', {
      label: filterValue
    });
  }
});

Template.search.events({
  "keyup input": _.throttle(function(e, t) {
    var text = $(e.target).val().trim();
    Session.set('storySearchQuery', text);
    if(enterPress(e) && t.data.slim){
      Router.go('/');
    }
    $('html, body').scrollTop(0);
  }, 200, {leading: false})
});

var curatedStoriesSub,
  trendingStoriesSub,
  newestStoriesSub,
  starredStoriesSub;

var getSubscriptionPage = function(filterValue){
  return subscriptionsPage.get(filterValue + 'Stories')
};

var setSubscriptionPage = function(filterValue, val){
  return subscriptionsPage.set(filterValue + 'Stories', val);
};

var getCurrentSubscriptionPage = function(){
  var storySearchQuery = Session.get('storySearchQuery');
  return getSubscriptionPage(storySearchQuery ? ('search:' + storySearchQuery) : Session.get('filterValue'));
};

var setCurrentSubscriptionPage = function(val){
  var storySearchQuery = Session.get('storySearchQuery');
  return setSubscriptionPage(storySearchQuery ? ('search:' + storySearchQuery) : Session.get('filterValue'), val);
};

var incrementSubscriptionPage = function(filterValue){
  setSubscriptionPage(filterValue, getSubscriptionPage(filterValue) + 1);
};

var incrementCurrentSubscriptionPage = function(){
  setCurrentSubscriptionPage(getCurrentSubscriptionPage() + 1);
};

var subscriptionsReady = new ReactiveDict();
var subscriptionsPage = new ReactiveDict();
_.each(filters, function(filter){
  setSubscriptionPage(filter, -1);
});

setSubscriptionPage('curated', 0); // curated stories are preloaded

var homeSubs = new SubsManager({
  cacheLimit: 9999,
  expireIn: 99999999
});

var searchUserSubs = new SubsManager({
  cacheLimit: 1,
  expireIn: 60
});

// these methods all keep the subscription open for the lifetime of the window, but can be called again safely
var subscribeToCuratedStories = function(cb){
  if(!curatedStoriesSub){
    curatedStoriesSub = homeSubs.subscribe("curatedStoriesPub", function(){
      var timeToLoadStories = Date.now() - createHomePageDate;
      trackTiming('Subscription', 'Full curated stories ready (time since created template)', timeToLoadStories);
      subscriptionsReady.set('curatedStories', true);
      if(cb){
        cb();
      }
    })
  } else {
    if(cb){
      cb();
    }
  }
};
var subscribeToTrendingStories = function(cb){
  if(!trendingStoriesSub){
    trendingStoriesSub = homeSubs.subscribe("trendingStoriesPub", function(){
      incrementSubscriptionPage('trending');
      subscriptionsReady.set('trendingStories', true);
      if(cb){
        cb();
      }
    })
  } else {
    if(cb){
      cb();
    }
  }
};
var subscribeToNewestStories = function(cb){
  if(!newestStoriesSub){
    newestStoriesSub = homeSubs.subscribe("newestStoriesPub", function(){
      incrementSubscriptionPage('newest');
      subscriptionsReady.set('newestStories', true);
      if(cb){
        cb();
      }
    })
  } else {
    if(cb){
      cb();
    }
  }
};

var subscribeToStarredStories = function(cb){
  if(!starredStoriesSub){
    starredStoriesSub = homeSubs.subscribe("starredStoriesPub", function(){
      incrementSubscriptionPage('starred');
      subscriptionsReady.set('starredStories', true);
      if(cb){
        cb();
      }
    })
  } else {
    if(cb){
      cb();
    }
  }
};

var createHomePageDate;
var whichUserPics = new Tracker.Dependency();

Template.all_stories.onCreated(function(){
  var that = this;
  createHomePageDate = Date.now();
  subscribeToCuratedStories(function(){
    if (!that.view.isDestroyed){ // because this happens asynchronously, the user may have already navigated away
      that.autorun(function(){
        whichUserPics.depend();
        that.subscribe('minimalUsersPub', _.sortBy(Stories.find({ published: true}, {fields: {authorId:1}, reactive: false}).map(function(story){return story.authorId}), _.identity));
      });
    }
    subscribeToTrendingStories(function() {
      subscribeToNewestStories(function(){
        subscribeToStarredStories(function(){
          whichUserPics.changed();
        })
      })
    });
  });

  var notFirstRun = false;
  this.autorun(function(){
    Session.get('filterValue'); // re-run whenever filter value changes
    if (notFirstRun){
      $(window).scrollTop(0)
    }
    notFirstRun = true;
  });

  // first page of search results, or when flip back to query
  this.autorun(function(){
    var storySearchQuery = Session.get('storySearchQuery');
    if(storySearchQuery){
      Tracker.nonreactive(function(){
        var currentPage = getCurrentSubscriptionPage();
        if(typeof currentPage !== 'number'){
          currentPage = 0;
          setCurrentSubscriptionPage(currentPage);
        }
        StorySearch.search(storySearchQuery, {page: currentPage});
        PersonSearch.search(storySearchQuery, {page: 0});
      })
    }
  });

  // further pages of search results
  this.autorun(function(){
    var currentPage = getCurrentSubscriptionPage();
    Tracker.nonreactive(function(){
      var storySearchQuery = Session.get('storySearchQuery');
      if(storySearchQuery && currentPage){
        StorySearch.search(storySearchQuery, {page: currentPage});
      }
    });
  });

  subscribeToStorySearchedMinimalUsers = _.debounce(function(){
    return searchUserSubs.subscribe('minimalUsersPub', _.sortBy(StorySearch.getData({}, true).map(function(story){return story.authorId}), _.identity));
  }, 1000);

  // TODO, we just loaded this data with the search....
  subscribeToPeopleSearchedMinimalUsers = _.debounce(function(){
    return searchUserSubs.subscribe('minimalUsersPub', _.sortBy(PersonSearch.getData({}, true).map(function(person){return person._id}), _.identity));
  }, 1000);

  this.autorun(function(){
    if(StorySearch.getStatus().loaded){
      subscribeToStorySearchedMinimalUsers();
    }
    if(PersonSearch.getStatus().loaded){
      subscribeToPeopleSearchedMinimalUsers();
    }
  });

});

search = null;

var currentHomeStories = function(){

  var limit = (getCurrentSubscriptionPage() + 1) * PUB_SIZE;

  if (limit <= 0){
    return
  }

  if(Session.get('storySearchQuery')){
    var storyResults = StorySearch.getData({
      sort:[
        ["editorsPickAt", "desc"],
        ["favoritedTotal", "desc"],
        ["savedAt", "desc"]
      ],
      docTransform: function(doc){
        return new Story(doc);
      }
    });
    var personResults = PersonSearch.getData({
      sort:[
        ["followersTotal", "desc"],
        ["followingTotal", "desc"],
        ["favoritesTotal", "desc"],
        ["createdAt", "desc"]
      ]
    });
    var searchResults = _.union(personResults, storyResults)
    searchResults.count = function(){
      return storyResults.length
    }
    return searchResults;
  }

  switch (Session.get('filterValue')) {
    case 'curated': // preview versions of all these stories come from fast-render so we can show them right away
      return Stories.find({ published: true, editorsPick: true}, {sort: {'editorsPickAt': -1}, limit: limit, reactive: true}); // .fetch() prevents a weird "Bad index" error
      break;
    case 'newest':
      return Stories.find({published: true}, {sort: {'publishedAt': -1}, limit: limit, reactive: true});
      break;
    case 'trending':
      return Stories.find({published: true}, {sort: {'analytics.views.total': -1}, limit: limit, reactive: true});
      break;
    case 'starred':
      return Stories.find({published: true}, {sort: {'favoritedTotal': -1}, limit: limit, reactive: true});
      break;
  }
};

Template.all_stories.events({
  'click .show-more' : function(e,t){

    var storySearchQuery = Session.get('storySearchQuery');
    if(storySearchQuery){
      incrementCurrentSubscriptionPage();
    } else {
      var filterValue = Session.get('filterValue');
      subscriptionsReady.set(filterValue + 'Stories', false);
      homeSubs.subscribe(filterValue + 'StoriesPub', {page: getCurrentSubscriptionPage() + 1}, function(){
        incrementCurrentSubscriptionPage();
        whichUserPics.changed();
        subscriptionsReady.set(filterValue + 'Stories', true);
      })
    }

  },
  'click .dismiss-box': function (e,t) {
    Session.set('boxDismissed', true);
  }
});

Template.all_stories.helpers({ // most of these are reactive false, but they will react when switch back and forth due to nesting inside ifs (so they rerun when switching between filters)
  stories: currentHomeStories,
  storiesLoading: function(){
    return(!(subscriptionsReady.get(Session.get('filterValue') + 'Stories')) || PersonSearch.getStatus().loading
    || StorySearch.getStatus().loading)
  },
  moreToShow: function(){
    var stories = currentHomeStories();
    if (!stories){
      return false
    }
    return currentHomeStories().count() >= (getCurrentSubscriptionPage() + 1) * PUB_SIZE
  },
  boxDismissed: function(){
    return Session.get('boxDismissed')
  }
});

Template.story_preview.helpers({
  story: function(){
    return Template.instance().data.story || Stories.findOne(this._id);
  }
});

Template._story_preview_content.onCreated(function(){
  this.showProfileInfoVariable = new ReactiveVar();
  var that = this;
  var timer = null;
  this.hideProfileInfo = function(){
    timer = Meteor.setTimeout(function(){
      that.showProfileInfoVariable.set(false);
      timer = null;
    }, 300)
  };
  this.cancelHideProfileInfo = function(){
    if(timer){
      Meteor.clearTimeout(timer);
    }
  };
  this.showProfileInfo = function(){
    that.cancelHideProfileInfo();
    that.showProfileInfoVariable.set(true);
  }
});

Template._story_preview_content.helpers({
  showProfileInfo: function(){
    return Template.instance().showProfileInfoVariable.get()
  }
});
Template._story_preview_content.events({
  "mouseenter .byline": function(d) {
    Template.instance().showProfileInfo();
  },
  "mouseleave .byline": function(d) {
    Template.instance().hideProfileInfo();
  },
});

Template._story_preview_content.helpers({
  lastPublishDate: function() {
    if(this.publishedAt) {
      return formatDateNice(this.publishedAt);
    }
  },
  story: function(){
    if (Template.instance().data.useDraftStory){
      return this.draftStory;
    } else {
      return this;
    }
  },
  linkRoute: function(){
    return Template.instance().data.useDraftStory ? 'edit' : 'read';
  },
  author: function(){
    return Meteor.users.findOne(this.authorId)
  },
  profileUrl: function(){
    return '/profile/' + (this.authorDisplayUsername || this.authorUsername); // TODO migrate drafts and only use authorDisplayUsername
  },
  contextCountOfType: function(type){
    return this.contextBlockTypeCount ? this.contextBlockTypeCount[type] : this.contextCountOfType(type);
  }
});

Template.login_buttons.helpers({
  showUserInfo: function() {
    return Template.instance().showUserInfo.get();
  }
});

Template.login_buttons.onCreated(function() {
  return this.showUserInfo = new ReactiveVar(false);
});

Template.login_buttons.events({
  "mouseenter .user-action": function(d) {
    Template.instance().showUserInfo.set(true);
  },
  "mouseleave .user-action": function(d) {
    Template.instance().showUserInfo.set(false);
  },
  "click .signin": function(d) {
    openSignInOverlay();
  },
  "click .logout" : function(e) {
    e.preventDefault();
    Template.instance().showUserInfo.set(false);
    Meteor.logout();
  }
});
<commit_msg>fix user subs on search<commit_after>var formatDate, weekDays, formatDateNice, monthNames;

weekDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
monthNames = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ];

// Friday 2/20/2015 20:29:22
formatDate = function(date) {
  var hms;
  hms = date.toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, "$1");
  return weekDays[date.getDay()] + " " + date.getMonth() + "/" + date.getDate() + "/" + date.getFullYear() + " " + hms;
};

// February 7th, 2015
formatDateNice = function(date) {
  var hms;
  hms = date.toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, "$1");
  return monthNames[(date.getMonth())] + " " + date.getDate() + ", " + date.getFullYear();
};

var filters = ['curated', 'trending', 'starred', 'newest'];
Session.setDefault('filterValue', filters[0]); // this must correspond to the first thing in the dropdown

Template.home.helpers({
  user: function() {
    return Meteor.user();
  },
  filterOpen: function() {
    return Session.get("filterOpen");
  },
  filter: function() {
    return Session.get("filter");
  },
  category: function() {
    return Session.get("category");
  }
});

Template.home.events({
  "click .logo-title a": function(e, t) {
    // reset search query
    Session.set('storySearchQuery', null);

    // reset filter
    Session.set('filterValue', filters[0]);
    t.$("select.filters-select").val(filters[0]);
    t.$("select.filters-select").selectOrDie("update");

  }
});

Template.search.onCreated(function() {
  this.autorun(function(){
    if(!Session.get('storySearchQuery')){
      $("input").val(null);
    }
  })

});

Template.search.onRendered(function() {
  if(this.data.slim){
    this.$("button").hide(); // hack to hide button
  } else {
    var storySearchQuery;
    if(storySearchQuery = Session.get('storySearchQuery')){
      this.$("input").val(storySearchQuery);
    }
  }
});

Template.search.helpers({
  showClearSearch: function(){
    return Session.get('storySearchQuery');
  }
});

Template.search.events({
  'click .clear-search': function(e, t){
    // this is the business logic
    Meteor.defer(function(){
      return Session.set('storySearchQuery', null);
    });

    // do this so the ui is snappy
    t.$("input").val(null);
    t.$("button").hide();

  },
  'keydown': function(e, t){
    if(t.data.slim){
      t.$("button").show(); // compensate for hack from above
    }
  }
});

Template.categories.helpers({
  categories: function() {
    return ['all', 'news', 'history', 'art', 'technology', 'politics', 'e-sports', 'music', 'gaming', 'sponsored'];
  },
  selected: function() {
    return Session.equals("category", this.toString());
  }
});

Template.categories.events({
  "click li": function(d) {
    var srcE;
    srcE = d.srcElement ? d.srcElement : d.target;
    return Session.set('category', $(srcE).data('category'));
  }
});

Template.filters.onRendered(function() {
  var options = {};
  if(this.data.slim){
    options.placeholder = "Explore";
  } else {
    var filterValue;
    if(filterValue = Session.get('filterValue')){
      $("select").val(filterValue);
    }
  }
  $("select").selectOrDie(options);

});

Template.filters.helpers({
  filters: function() {
    return _.map(filters, function(filter){
      return {
        value: filter,
        label: filter === 'curated' ? 'FOLD Picks' : _s.capitalize(filter)
      }
    })
  },
  conditionallySelected: function(){
    return Session.equals('filterValue', this.toString()) ? 'selected' : '';
  }
});

Template.filters.events({
  "change select": function(e, t) {
    var filterValue = $(e.target).val();
    Session.set('filterValue', filterValue);
    Session.set('storySearchQuery', null);
    if(t.data.slim){
      Router.go('/');
    }
    trackEvent('Select filter', {
      label: filterValue
    });
  }
});

Template.search.events({
  "keyup input": _.throttle(function(e, t) {
    var text = $(e.target).val().trim();
    Session.set('storySearchQuery', text);
    if(enterPress(e) && t.data.slim){
      Router.go('/');
    }
    $('html, body').scrollTop(0);
  }, 200, {leading: false})
});

var curatedStoriesSub,
  trendingStoriesSub,
  newestStoriesSub,
  starredStoriesSub;

var getSubscriptionPage = function(filterValue){
  return subscriptionsPage.get(filterValue + 'Stories')
};

var setSubscriptionPage = function(filterValue, val){
  return subscriptionsPage.set(filterValue + 'Stories', val);
};

var getCurrentSubscriptionPage = function(){
  var storySearchQuery = Session.get('storySearchQuery');
  return getSubscriptionPage(storySearchQuery ? ('search:' + storySearchQuery) : Session.get('filterValue'));
};

var setCurrentSubscriptionPage = function(val){
  var storySearchQuery = Session.get('storySearchQuery');
  return setSubscriptionPage(storySearchQuery ? ('search:' + storySearchQuery) : Session.get('filterValue'), val);
};

var incrementSubscriptionPage = function(filterValue){
  setSubscriptionPage(filterValue, getSubscriptionPage(filterValue) + 1);
};

var incrementCurrentSubscriptionPage = function(){
  setCurrentSubscriptionPage(getCurrentSubscriptionPage() + 1);
};

var subscriptionsReady = new ReactiveDict();
var subscriptionsPage = new ReactiveDict();
_.each(filters, function(filter){
  setSubscriptionPage(filter, -1);
});

setSubscriptionPage('curated', 0); // curated stories are preloaded

var homeSubs = new SubsManager({
  cacheLimit: 9999,
  expireIn: 99999999
});

var storySearchUserSubs = new SubsManager({
  cacheLimit: 1,
  expireIn: 60
});

var peopleSearchUserSubs = new SubsManager({
  cacheLimit: 1,
  expireIn: 60
});

// these methods all keep the subscription open for the lifetime of the window, but can be called again safely
var subscribeToCuratedStories = function(cb){
  if(!curatedStoriesSub){
    curatedStoriesSub = homeSubs.subscribe("curatedStoriesPub", function(){
      var timeToLoadStories = Date.now() - createHomePageDate;
      trackTiming('Subscription', 'Full curated stories ready (time since created template)', timeToLoadStories);
      subscriptionsReady.set('curatedStories', true);
      if(cb){
        cb();
      }
    })
  } else {
    if(cb){
      cb();
    }
  }
};
var subscribeToTrendingStories = function(cb){
  if(!trendingStoriesSub){
    trendingStoriesSub = homeSubs.subscribe("trendingStoriesPub", function(){
      incrementSubscriptionPage('trending');
      subscriptionsReady.set('trendingStories', true);
      if(cb){
        cb();
      }
    })
  } else {
    if(cb){
      cb();
    }
  }
};
var subscribeToNewestStories = function(cb){
  if(!newestStoriesSub){
    newestStoriesSub = homeSubs.subscribe("newestStoriesPub", function(){
      incrementSubscriptionPage('newest');
      subscriptionsReady.set('newestStories', true);
      if(cb){
        cb();
      }
    })
  } else {
    if(cb){
      cb();
    }
  }
};

var subscribeToStarredStories = function(cb){
  if(!starredStoriesSub){
    starredStoriesSub = homeSubs.subscribe("starredStoriesPub", function(){
      incrementSubscriptionPage('starred');
      subscriptionsReady.set('starredStories', true);
      if(cb){
        cb();
      }
    })
  } else {
    if(cb){
      cb();
    }
  }
};

var createHomePageDate;
var whichUserPics = new Tracker.Dependency();

Template.all_stories.onCreated(function(){
  var that = this;
  createHomePageDate = Date.now();
  subscribeToCuratedStories(function(){
    if (!that.view.isDestroyed){ // because this happens asynchronously, the user may have already navigated away
      that.autorun(function(){
        whichUserPics.depend();
        that.subscribe('minimalUsersPub', _.sortBy(Stories.find({ published: true}, {fields: {authorId:1}, reactive: false}).map(function(story){return story.authorId}), _.identity));
      });
    }
    subscribeToTrendingStories(function() {
      subscribeToNewestStories(function(){
        subscribeToStarredStories(function(){
          whichUserPics.changed();
        })
      })
    });
  });

  var notFirstRun = false;
  this.autorun(function(){
    Session.get('filterValue'); // re-run whenever filter value changes
    if (notFirstRun){
      $(window).scrollTop(0)
    }
    notFirstRun = true;
  });

  // first page of search results, or when flip back to query
  this.autorun(function(){
    var storySearchQuery = Session.get('storySearchQuery');
    if(storySearchQuery){
      Tracker.nonreactive(function(){
        var currentPage = getCurrentSubscriptionPage();
        if(typeof currentPage !== 'number'){
          currentPage = 0;
          setCurrentSubscriptionPage(currentPage);
        }
        StorySearch.search(storySearchQuery, {page: currentPage});
        PersonSearch.search(storySearchQuery, {page: 0});
      })
    }
  });

  // further pages of search results
  this.autorun(function(){
    var currentPage = getCurrentSubscriptionPage();
    Tracker.nonreactive(function(){
      var storySearchQuery = Session.get('storySearchQuery');
      if(storySearchQuery && currentPage){
        StorySearch.search(storySearchQuery, {page: currentPage});
      }
    });
  });

  subscribeToStorySearchedMinimalUsers = _.debounce(function(){
    return storySearchUserSubs.subscribe('minimalUsersPub', _.sortBy(StorySearch.getData({}, true).map(function(story){return story.authorId}), _.identity));
  }, 1000);

  // TODO, we just loaded this data with the search....
  subscribeToPeopleSearchedMinimalUsers = _.debounce(function(){
    return peopleSearchUserSubs.subscribe('minimalUsersPub', _.sortBy(PersonSearch.getData({}, true).map(function(person){return person._id}), _.identity));
  }, 1000);

  this.autorun(function(){
    if(StorySearch.getStatus().loaded){
      subscribeToStorySearchedMinimalUsers();
    }
    if(PersonSearch.getStatus().loaded){
      subscribeToPeopleSearchedMinimalUsers();
    }
  });

});

search = null;

var currentHomeStories = function(){

  var limit = (getCurrentSubscriptionPage() + 1) * PUB_SIZE;

  if (limit <= 0){
    return
  }

  if(Session.get('storySearchQuery')){
    var storyResults = StorySearch.getData({
      sort:[
        ["editorsPickAt", "desc"],
        ["favoritedTotal", "desc"],
        ["savedAt", "desc"]
      ],
      docTransform: function(doc){
        return new Story(doc);
      }
    });
    var personResults = PersonSearch.getData({
      sort:[
        ["followersTotal", "desc"],
        ["followingTotal", "desc"],
        ["favoritesTotal", "desc"],
        ["createdAt", "desc"]
      ]
    });
    var searchResults = _.union(personResults, storyResults)
    searchResults.count = function(){
      return storyResults.length
    }
    return searchResults;
  }

  switch (Session.get('filterValue')) {
    case 'curated': // preview versions of all these stories come from fast-render so we can show them right away
      return Stories.find({ published: true, editorsPick: true}, {sort: {'editorsPickAt': -1}, limit: limit, reactive: true}); // .fetch() prevents a weird "Bad index" error
      break;
    case 'newest':
      return Stories.find({published: true}, {sort: {'publishedAt': -1}, limit: limit, reactive: true});
      break;
    case 'trending':
      return Stories.find({published: true}, {sort: {'analytics.views.total': -1}, limit: limit, reactive: true});
      break;
    case 'starred':
      return Stories.find({published: true}, {sort: {'favoritedTotal': -1}, limit: limit, reactive: true});
      break;
  }
};

Template.all_stories.events({
  'click .show-more' : function(e,t){

    var storySearchQuery = Session.get('storySearchQuery');
    if(storySearchQuery){
      incrementCurrentSubscriptionPage();
    } else {
      var filterValue = Session.get('filterValue');
      subscriptionsReady.set(filterValue + 'Stories', false);
      homeSubs.subscribe(filterValue + 'StoriesPub', {page: getCurrentSubscriptionPage() + 1}, function(){
        incrementCurrentSubscriptionPage();
        whichUserPics.changed();
        subscriptionsReady.set(filterValue + 'Stories', true);
      })
    }

  },
  'click .dismiss-box': function (e,t) {
    Session.set('boxDismissed', true);
  }
});

Template.all_stories.helpers({ // most of these are reactive false, but they will react when switch back and forth due to nesting inside ifs (so they rerun when switching between filters)
  stories: currentHomeStories,
  storiesLoading: function(){
    return(!(subscriptionsReady.get(Session.get('filterValue') + 'Stories')) || PersonSearch.getStatus().loading
    || StorySearch.getStatus().loading)
  },
  moreToShow: function(){
    var stories = currentHomeStories();
    if (!stories){
      return false
    }
    return currentHomeStories().count() >= (getCurrentSubscriptionPage() + 1) * PUB_SIZE
  },
  boxDismissed: function(){
    return Session.get('boxDismissed')
  }
});

Template.story_preview.helpers({
  story: function(){
    return Template.instance().data.story || Stories.findOne(this._id);
  }
});

Template._story_preview_content.onCreated(function(){
  this.showProfileInfoVariable = new ReactiveVar();
  var that = this;
  var timer = null;
  this.hideProfileInfo = function(){
    timer = Meteor.setTimeout(function(){
      that.showProfileInfoVariable.set(false);
      timer = null;
    }, 300)
  };
  this.cancelHideProfileInfo = function(){
    if(timer){
      Meteor.clearTimeout(timer);
    }
  };
  this.showProfileInfo = function(){
    that.cancelHideProfileInfo();
    that.showProfileInfoVariable.set(true);
  }
});

Template._story_preview_content.helpers({
  showProfileInfo: function(){
    return Template.instance().showProfileInfoVariable.get()
  }
});
Template._story_preview_content.events({
  "mouseenter .byline": function(d) {
    Template.instance().showProfileInfo();
  },
  "mouseleave .byline": function(d) {
    Template.instance().hideProfileInfo();
  },
});

Template._story_preview_content.helpers({
  lastPublishDate: function() {
    if(this.publishedAt) {
      return formatDateNice(this.publishedAt);
    }
  },
  story: function(){
    if (Template.instance().data.useDraftStory){
      return this.draftStory;
    } else {
      return this;
    }
  },
  linkRoute: function(){
    return Template.instance().data.useDraftStory ? 'edit' : 'read';
  },
  author: function(){
    return Meteor.users.findOne(this.authorId)
  },
  profileUrl: function(){
    return '/profile/' + (this.authorDisplayUsername || this.authorUsername); // TODO migrate drafts and only use authorDisplayUsername
  },
  contextCountOfType: function(type){
    return this.contextBlockTypeCount ? this.contextBlockTypeCount[type] : this.contextCountOfType(type);
  }
});

Template.login_buttons.helpers({
  showUserInfo: function() {
    return Template.instance().showUserInfo.get();
  }
});

Template.login_buttons.onCreated(function() {
  return this.showUserInfo = new ReactiveVar(false);
});

Template.login_buttons.events({
  "mouseenter .user-action": function(d) {
    Template.instance().showUserInfo.set(true);
  },
  "mouseleave .user-action": function(d) {
    Template.instance().showUserInfo.set(false);
  },
  "click .signin": function(d) {
    openSignInOverlay();
  },
  "click .logout" : function(e) {
    e.preventDefault();
    Template.instance().showUserInfo.set(false);
    Meteor.logout();
  }
});
<|endoftext|>


<commit_before>/**
 * @fileOverview HistoryCtrl angular controller
 */

define(['./module', 'bitcoinjs-lib', 'util/btc', 'darkwallet', 'dwutil/multisig', 'frontend/port'],
function (controllers, Bitcoin, BtcUtils, DarkWallet, MultisigFund, Port) {
  'use strict';
  controllers.controller('HistoryCtrl', ['$scope', 'notify', function($scope, notify) {

  // Start some structures
  $scope.pocket = {index: undefined, name: 'All Pockets', mpk: undefined, addresses: $scope.allAddresses, changeAddresses: []};
  $scope.pocketName = "All Pockets";
  $scope.selectedPocket = 'pocket:all';
  $scope.historyRows = [];

  $scope.isAll = true;
  $scope.isFund = false;

  function calculateBalance(pocket, isFund, isAll) {
        var balance;
        var wallet = DarkWallet.getIdentity().wallet;
        if (isFund) {
            balance = wallet.getBalance(pocket.fund.multisig.seq[0]);
        } else if (isAll) {
            balance = wallet.getBalance();
        } else {
            var mainBalance = wallet.getBalance(pocket.index);
            var changeBalance = wallet.getBalance(pocket.index+1);

            var confirmed = mainBalance.confirmed + changeBalance.confirmed;
            var unconfirmed = mainBalance.unconfirmed + changeBalance.unconfirmed;
            balance = {confirmed: confirmed, unconfirmed: unconfirmed};
        }
        return balance;
  }

  function isCurrentPocket(pocketId) {
      if ($scope.isAll) {
          return true;
      } else if ($scope.isFund && $scope.pocket.index == pocketId) {
          return true;
      } else if (typeof $scope.pocket.index == 'number' && pocketId == $scope.pocket.index/2) {
      }
  }

  Port.connectNg('gui', $scope, function(data) {
    // Check on gui balance updates to recalculate pocket balance so it shows properly
    if (data.type == 'balance') {
        if (isCurrentPocket(data.pocketId)) {
            var balance = calculateBalance($scope.pocket, $scope.isFund, $scope.isAll);
            $scope.balance = balance.confirmed;
            $scope.unconfirmed = balance.unconfirmed;
            $scope.chooseRows();
            if (!$scope.$$phase) {
                $scope.$apply();
            }
        }
    }
  });

  Port.connectNg('wallet', $scope, function(data) {
    if (data.type == 'ready') {
        var balance = calculateBalance($scope.pocket, $scope.isFund, $scope.isAll);
        $scope.balance = balance.confirmed;
        $scope.unconfirmed = balance.unconfirmed;
        $scope.chooseRows();
        if (!$scope.$$phase) {
            $scope.$apply();
        }
    }
  });

  // History Listing
  $scope.selectFund = function(fund, rowIndex) {
      $scope.pocket.name = fund.name;
      $scope.pocket.index = fund.seq[0];
      var address = $scope.identity.wallet.getAddress(fund.seq);

      $scope.pocket.changeAddresses = [];
      $scope.pocket.addresses = [address];
      $scope.pocket.fund = new MultisigFund(fund);
      $scope.pocket.tasks = $scope.pocket.fund.tasks;

      $scope.isAll = false;
      $scope.isFund = true;
      $scope.pocket.mpk = undefined;
      $scope.pocket.stealth = undefined;
      $scope.selectedPocket = 'fund:' + rowIndex;

      var balance = $scope.identity.wallet.getBalance(fund.seq[0]);

      $scope.balance = balance.confirmed;
      $scope.unconfirmed = balance.unconfirmed;
      $scope.chooseRows();
  };
  $scope.selectPocket = function(pocketName, rowIndex, form) {
      var pocketIndex;
      if (pocketName === undefined) {
          $scope.pocket.name = "All Pockets";
          $scope.pocket.index = undefined;
          $scope.pocket.mpk = undefined;
          $scope.pocket.stealth = undefined;
          $scope.pocket.fund = null;
          $scope.pocket.addresses = $scope.allAddresses;
          $scope.pocket.changeAddresses = [];
          $scope.isAll = true;
          $scope.isFund = false;
          var balance = $scope.identity.wallet.getBalance();
          $scope.balance = balance.confirmed;
          $scope.unconfirmed = balance.unconfirmed;
          $scope.pocket.tasks = [];
          rowIndex = 'all';
      } else {
          pocketIndex = rowIndex*2;
          $scope.pocket.index = pocketIndex;
          $scope.pocket.name = pocketName;
          $scope.pocket.fund = null;
          var walletAddress = $scope.identity.wallet.getAddress([$scope.pocket.index]);
          $scope.pocket.mpk = walletAddress.mpk;
          $scope.pocket.stealth = walletAddress.stealth;
          $scope.pocket.addresses = $scope.addresses[$scope.pocket.index];
          $scope.pocket.changeAddresses = $scope.addresses[$scope.pocket.index+1];
          var walletPocket = $scope.identity.wallet.pockets.getPocket(pocketName);
          $scope.pocket.mixing = walletPocket.mixing;
          $scope.pocket.tasks = [];
          $scope.isAll = false;
          $scope.isFund = false;
          // balance is sum of public and change branches
          var mainBalance = $scope.identity.wallet.getBalance(pocketIndex);
          var changeBalance = $scope.identity.wallet.getBalance(pocketIndex+1);
          $scope.balance = mainBalance.confirmed + changeBalance.confirmed;
          $scope.unconfirmed = mainBalance.unconfirmed + changeBalance.unconfirmed;
          $scope.forms.pocketLabelForm = form;
      }
      $scope.selectedPocket = 'pocket:' + rowIndex;
      $scope.chooseRows();
  };

  $scope.newMultiSig = function() {
    $scope.selectedPocket = 'newMultisig';
  };

  // Filters

  // Filter the rows we want to show
  var chooseRows = function() {
    var history = $scope.identity.history.history;
    var rows = history.filter($scope.pocketFilter);
    rows = rows.sort(function(a, b) {
       if (!a.height) {
          return -10000000;
       }
       if (!b.height) {
          return 10000000;
       }
       return b.height - a.height;
    });
    var shownRows = [];
    rows = rows.filter(function(row) { return $scope.historyFilter(row, shownRows) } );
    if (!rows.length) {
        return [];
    }
    // Now calculate balances
    var prevRow = rows[0];
    prevRow.confirmed = $scope.balance;
    prevRow.unconfirmed = $scope.unconfirmed;
    var idx = 1;
    while(idx<rows.length) {
        var row = rows[idx];
        var value = prevRow.myOutValue - prevRow.myInValue;
        if (prevRow.height) {
            row.confirmed = prevRow.confirmed-value;
            row.unconfirmed = prevRow.unconfirmed;
        } else {
            row.confirmed = prevRow.confirmed;
            row.unconfirmed = prevRow.unconfirmed-value;
        }
        prevRow = row;
        idx++;
    }
    return rows;
  }

  $scope.txFilter = 'last10';

  var pocketFilter = function(row) {
      // Making sure shownRows is reset before historyFilter stage is reached.
      if ($scope.isAll) {
          // only add pocket transactions for now
          return typeof row.pocket === 'number';
      }
      else {
          // row pocket here is just 1st element in index, pocket can be pocket/2
          if (typeof row.pocket === 'number') {
              // row.pocket here is the branch id, pocket.index is 2*pocketId
              var pocketBranch = (row.pocket%2) ? row.pocket-1 : row.pocket;
              return pocketBranch == $scope.pocket.index;
          } else {
              return row.pocket == $scope.pocket.index;
          }
      }
  };

  // Get date 30 days ago
  var prevmonth = new Date();
  prevmonth.setDate(prevmonth.getDate()-30);
  // Get date 7 days ago
  var prevweek = new Date();
  prevweek.setDate(prevweek.getDate()-7);

  // Set the history filter
  $scope.setHistoryFilter = function(name) {
      $scope.txFilter = name;
      $scope.chooseRows();
  };

  // History filter, run for every row to see if we should show it
  $scope.unusedAddressFilter = function(address) {
      return address.nOutputs == 0;
  };
  $scope.usedAddressFilter = function(address) {
      return address.nOutputs;
  };
  $scope.pocketFilter = function(row) {
      return pocketFilter(row);
  };
  $scope.historyFilter = function(row, shownRows) {
      if (!row.height) {
          shownRows.push(row.hash);
          return true;
      }
      switch($scope.txFilter) {
          case 'all':
              return true;
          case 'lastWeek':
              var ts = BtcUtils.heightToTimestamp(row.height);
              if (ts > prevweek.getTime()) {
                  return true;
              }
              break;
          case 'lastMonth':
              var ts = BtcUtils.heightToTimestamp(row.height);
              if (ts > prevmonth.getTime()) {
                  return true;
              }
              break;
          case 'last10':
          default:
              if (shownRows.indexOf(row.hash) != -1) {
                  return true;
              } else if (shownRows.length < 10) {
                  shownRows.push(row.hash);
                  return true;
              }
      }
      return false;
  };

  $scope.chooseRows = function() {
      $scope.historyRows = chooseRows();
  }

  $scope.copyClipboardPublic = function(walletAddress) {
      var pubKey = new Bitcoin.ECPubKey(walletAddress.pubKey, true);
      var publicHex = pubKey.toHex();
      $scope.copyClipboard(publicHex, 'Copied public key to clipboard');
  }

}]);
});
<commit_msg>outgoing unconfirmed should be discounted from balance.<commit_after>/**
 * @fileOverview HistoryCtrl angular controller
 */

define(['./module', 'bitcoinjs-lib', 'util/btc', 'darkwallet', 'dwutil/multisig', 'frontend/port'],
function (controllers, Bitcoin, BtcUtils, DarkWallet, MultisigFund, Port) {
  'use strict';
  controllers.controller('HistoryCtrl', ['$scope', 'notify', function($scope, notify) {

  // Start some structures
  $scope.pocket = {index: undefined, name: 'All Pockets', mpk: undefined, addresses: $scope.allAddresses, changeAddresses: []};
  $scope.pocketName = "All Pockets";
  $scope.selectedPocket = 'pocket:all';
  $scope.historyRows = [];

  $scope.isAll = true;
  $scope.isFund = false;

  function calculateBalance(pocket, isFund, isAll) {
        var balance;
        var wallet = DarkWallet.getIdentity().wallet;
        if (isFund) {
            balance = wallet.getBalance(pocket.fund.multisig.seq[0]);
        } else if (isAll) {
            balance = wallet.getBalance();
        } else {
            var mainBalance = wallet.getBalance(pocket.index);
            var changeBalance = wallet.getBalance(pocket.index+1);

            var confirmed = mainBalance.confirmed + changeBalance.confirmed;
            var unconfirmed = mainBalance.unconfirmed + changeBalance.unconfirmed;
            balance = {confirmed: confirmed, unconfirmed: unconfirmed};
        }
        return balance;
  }

  function isCurrentPocket(pocketId) {
      if ($scope.isAll) {
          return true;
      } else if ($scope.isFund && $scope.pocket.index == pocketId) {
          return true;
      } else if (typeof $scope.pocket.index == 'number' && pocketId == $scope.pocket.index/2) {
      }
  }

  Port.connectNg('gui', $scope, function(data) {
    // Check on gui balance updates to recalculate pocket balance so it shows properly
    if (data.type == 'balance') {
        if (isCurrentPocket(data.pocketId)) {
            var balance = calculateBalance($scope.pocket, $scope.isFund, $scope.isAll);
            $scope.balance = balance.confirmed;
            $scope.unconfirmed = balance.unconfirmed;
            $scope.chooseRows();
            if (!$scope.$$phase) {
                $scope.$apply();
            }
        }
    }
  });

  Port.connectNg('wallet', $scope, function(data) {
    if (data.type == 'ready') {
        var balance = calculateBalance($scope.pocket, $scope.isFund, $scope.isAll);
        $scope.balance = balance.confirmed;
        $scope.unconfirmed = balance.unconfirmed;
        $scope.chooseRows();
        if (!$scope.$$phase) {
            $scope.$apply();
        }
    }
  });

  // History Listing
  $scope.selectFund = function(fund, rowIndex) {
      $scope.pocket.name = fund.name;
      $scope.pocket.index = fund.seq[0];
      var address = $scope.identity.wallet.getAddress(fund.seq);

      $scope.pocket.changeAddresses = [];
      $scope.pocket.addresses = [address];
      $scope.pocket.fund = new MultisigFund(fund);
      $scope.pocket.tasks = $scope.pocket.fund.tasks;

      $scope.isAll = false;
      $scope.isFund = true;
      $scope.pocket.mpk = undefined;
      $scope.pocket.stealth = undefined;
      $scope.selectedPocket = 'fund:' + rowIndex;

      var balance = $scope.identity.wallet.getBalance(fund.seq[0]);

      $scope.balance = balance.confirmed;
      $scope.unconfirmed = balance.unconfirmed;
      $scope.chooseRows();
  };
  $scope.selectPocket = function(pocketName, rowIndex, form) {
      var pocketIndex;
      if (pocketName === undefined) {
          $scope.pocket.name = "All Pockets";
          $scope.pocket.index = undefined;
          $scope.pocket.mpk = undefined;
          $scope.pocket.stealth = undefined;
          $scope.pocket.fund = null;
          $scope.pocket.addresses = $scope.allAddresses;
          $scope.pocket.changeAddresses = [];
          $scope.isAll = true;
          $scope.isFund = false;
          var balance = $scope.identity.wallet.getBalance();
          $scope.balance = balance.confirmed;
          $scope.unconfirmed = balance.unconfirmed;
          $scope.pocket.tasks = [];
          rowIndex = 'all';
      } else {
          pocketIndex = rowIndex*2;
          $scope.pocket.index = pocketIndex;
          $scope.pocket.name = pocketName;
          $scope.pocket.fund = null;
          var walletAddress = $scope.identity.wallet.getAddress([$scope.pocket.index]);
          $scope.pocket.mpk = walletAddress.mpk;
          $scope.pocket.stealth = walletAddress.stealth;
          $scope.pocket.addresses = $scope.addresses[$scope.pocket.index];
          $scope.pocket.changeAddresses = $scope.addresses[$scope.pocket.index+1];
          var walletPocket = $scope.identity.wallet.pockets.getPocket(pocketName);
          $scope.pocket.mixing = walletPocket.mixing;
          $scope.pocket.tasks = [];
          $scope.isAll = false;
          $scope.isFund = false;
          // balance is sum of public and change branches
          var mainBalance = $scope.identity.wallet.getBalance(pocketIndex);
          var changeBalance = $scope.identity.wallet.getBalance(pocketIndex+1);
          $scope.balance = mainBalance.confirmed + changeBalance.confirmed;
          $scope.unconfirmed = mainBalance.unconfirmed + changeBalance.unconfirmed;
          $scope.forms.pocketLabelForm = form;
      }
      $scope.selectedPocket = 'pocket:' + rowIndex;
      $scope.chooseRows();
  };

  $scope.newMultiSig = function() {
    $scope.selectedPocket = 'newMultisig';
  };

  // Filters

  // Filter the rows we want to show
  var chooseRows = function() {
    var history = $scope.identity.history.history;
    var rows = history.filter($scope.pocketFilter);
    rows = rows.sort(function(a, b) {
       if (!a.height) {
          return -10000000;
       }
       if (!b.height) {
          return 10000000;
       }
       return b.height - a.height;
    });
    var shownRows = [];
    rows = rows.filter(function(row) { return $scope.historyFilter(row, shownRows) } );
    if (!rows.length) {
        return [];
    }
    // Now calculate balances
    var prevRow = rows[0];
    prevRow.confirmed = $scope.balance;
    prevRow.unconfirmed = $scope.unconfirmed;

    var idx = 1;
    while(idx<rows.length) {
        var row = rows[idx];
        var value = prevRow.myOutValue - prevRow.myInValue;
        if (prevRow.height) {
            row.confirmed = prevRow.confirmed-value;
            row.unconfirmed = prevRow.unconfirmed;
        } else {
            row.confirmed = prevRow.confirmed-value;
            // Outgoing unconfirmed are credited straight away
            if (value < 0) {
                row.confirmed -= value;
            }
            row.unconfirmed = prevRow.unconfirmed-value;
        }
        prevRow = row;
        idx++;
    }
    return rows;
  }

  $scope.txFilter = 'last10';

  var pocketFilter = function(row) {
      // Making sure shownRows is reset before historyFilter stage is reached.
      if ($scope.isAll) {
          // only add pocket transactions for now
          return typeof row.pocket === 'number';
      }
      else {
          // row pocket here is just 1st element in index, pocket can be pocket/2
          if (typeof row.pocket === 'number') {
              // row.pocket here is the branch id, pocket.index is 2*pocketId
              var pocketBranch = (row.pocket%2) ? row.pocket-1 : row.pocket;
              return pocketBranch == $scope.pocket.index;
          } else {
              return row.pocket == $scope.pocket.index;
          }
      }
  };

  // Get date 30 days ago
  var prevmonth = new Date();
  prevmonth.setDate(prevmonth.getDate()-30);
  // Get date 7 days ago
  var prevweek = new Date();
  prevweek.setDate(prevweek.getDate()-7);

  // Set the history filter
  $scope.setHistoryFilter = function(name) {
      $scope.txFilter = name;
      $scope.chooseRows();
  };

  // History filter, run for every row to see if we should show it
  $scope.unusedAddressFilter = function(address) {
      return address.nOutputs == 0;
  };
  $scope.usedAddressFilter = function(address) {
      return address.nOutputs;
  };
  $scope.pocketFilter = function(row) {
      return pocketFilter(row);
  };
  $scope.historyFilter = function(row, shownRows) {
      if (!row.height) {
          shownRows.push(row.hash);
          return true;
      }
      switch($scope.txFilter) {
          case 'all':
              return true;
          case 'lastWeek':
              var ts = BtcUtils.heightToTimestamp(row.height);
              if (ts > prevweek.getTime()) {
                  return true;
              }
              break;
          case 'lastMonth':
              var ts = BtcUtils.heightToTimestamp(row.height);
              if (ts > prevmonth.getTime()) {
                  return true;
              }
              break;
          case 'last10':
          default:
              if (shownRows.indexOf(row.hash) != -1) {
                  return true;
              } else if (shownRows.length < 10) {
                  shownRows.push(row.hash);
                  return true;
              }
      }
      return false;
  };

  $scope.chooseRows = function() {
      $scope.historyRows = chooseRows();
  }

  $scope.copyClipboardPublic = function(walletAddress) {
      var pubKey = new Bitcoin.ECPubKey(walletAddress.pubKey, true);
      var publicHex = pubKey.toHex();
      $scope.copyClipboard(publicHex, 'Copied public key to clipboard');
  }

}]);
});
<|endoftext|>


<commit_before>/*global expect*/
describe('expect.it', () => {
  it('returns an expectation function that when applyed runs the assertion on the given subject', () => {
    var expectation = expect.it('to be greater than', 14);
    expectation(20);
    expect(
      function() {
        expectation(10);
      },
      'to throw',
      'expected 10 to be greater than 14'
    );
  });

  it('is inspected as it is written', () => {
    var expectation = expect
      .it('to be a number')
      .and('to be less than', 14)
      .and('to be negative')
      .or('to be a string')
      .and('to have length', 6);
    expect(
      expect.inspect(expectation).toString(),
      'to equal',
      "expect.it('to be a number')\n" +
        "        .and('to be less than', 14)\n" +
        "        .and('to be negative')\n" +
        "      .or('to be a string')\n" +
        "        .and('to have length', 6)"
    );
  });

  it('does not catch errors that are not thrown by unexpected', () => {
    var clonedExpect = expect
      .clone()
      .addAssertion('explode', function(expect, subject) {
        throw new Error('Explosion');
      });

    expect(clonedExpect.it('explode'), 'to throw', 'Explosion');
  });

  describe('with chained and', () => {
    it('all assertions has to be satisfied', () => {
      var expectation = expect
        .it('to be a number')
        .and('to be less than', 14)
        .and('to be negative');
      expect(
        function() {
          expectation(20);
        },
        'to throw',
        '✓ expected 20 to be a number and\n' +
          '⨯ expected 20 to be less than 14 and\n' +
          '⨯ expected 20 to be negative'
      );
    });

    it('returns a new function', () => {
      var expectation = expect.it('to be a number');
      var compositeExpectation = expectation.and('to be less than', 14);
      expect(compositeExpectation, 'not to be', expectation);

      expectation(20);
      expect(
        function() {
          compositeExpectation(20);
        },
        'to throw',
        '✓ expected 20 to be a number and\n' +
          '⨯ expected 20 to be less than 14'
      );
    });

    it('outputs one failing assertion correctly', () => {
      var expectation = expect
        .it('to be a number')
        .and('to be less than', 14)
        .and('to be negative');
      expect(
        function() {
          expectation(8);
        },
        'to throw',
        '✓ expected 8 to be a number and\n' +
          '✓ expected 8 to be less than 14 and\n' +
          '⨯ expected 8 to be negative'
      );
    });
  });

  describe('with chained or', () => {
    it('succeeds if any expectations succeeds', () => {
      var expectation = expect
        .it('to be a number')
        .or('to be a string')
        .or('to be an array');
      expect(function() {
        expectation('success');
      }, 'not to throw');
    });

    it('fails if all the expectations fails', () => {
      var expectation = expect
        .it('to be a number')
        .and('to be greater than', 6)
        .or('to be a string')
        .and('to have length', 6)
        .or('to be an array');
      expect(
        function() {
          expectation('foobarbaz');
        },
        'to throw',
        "⨯ expected 'foobarbaz' to be a number and\n" +
          "⨯ expected 'foobarbaz' to be greater than 6\n" +
          'or\n' +
          "✓ expected 'foobarbaz' to be a string and\n" +
          "⨯ expected 'foobarbaz' to have length 6\n" +
          '    expected 9 to be 6\n' +
          'or\n' +
          "⨯ expected 'foobarbaz' to be an array"
      );
    });

    it('if there are no and-clauses it writes the failure output more compactly', () => {
      var expectation = expect
        .it('to be a number')
        .or('to be a string')
        .or('to be an array');
      expect(
        function() {
          expectation(true);
        },
        'to throw',
        '⨯ expected true to be a number or\n' +
          '⨯ expected true to be a string or\n' +
          '⨯ expected true to be an array'
      );
    });

    it('returns a new function', () => {
      var expectation = expect.it('to be a number');
      var compositeExpectation = expectation.or('to be a string');
      expect(compositeExpectation, 'not to be', expectation);

      expectation(20);
      expect(
        function() {
          compositeExpectation(20);
          compositeExpectation(true);
        },
        'to throw',
        '⨯ expected true to be a number or\n' + '⨯ expected true to be a string'
      );
    });
  });

  describe('with async assertions', () => {
    var clonedExpect = expect
      .clone()
      .addAssertion('to be a number after a short delay', function(
        expect,
        subject
      ) {
        expect.errorMode = 'nested';

        return expect.promise(function(run) {
          setTimeout(
            run(function() {
              expect(subject, 'to be a number');
            }),
            1
          );
        });
      })
      .addAssertion('to be finite after a short delay', function(
        expect,
        subject
      ) {
        expect.errorMode = 'nested';

        return expect.promise(function(run) {
          setTimeout(
            run(function() {
              expect(subject, 'to be finite');
            }),
            1
          );
        });
      })
      .addAssertion('to be a string after a short delay', function(
        expect,
        subject
      ) {
        expect.errorMode = 'nested';

        return expect.promise(function(run) {
          setTimeout(
            run(function() {
              expect(subject, 'to be a string');
            }),
            1
          );
        });
      });

    it('should succeed', () => {
      return clonedExpect.it('to be a number after a short delay')(123);
    });

    it('should fail with a diff', () => {
      return expect(
        clonedExpect.it('to be a number after a short delay')(false),
        'to be rejected with',
        'expected false to be a number after a short delay\n' +
          '  expected false to be a number'
      );
    });

    describe('with a chained "and" construct', () => {
      it('should succeed', () => {
        return clonedExpect
          .it('to be a number after a short delay')
          .and('to be finite after a short delay')(123);
      });

      it('should fail with a diff', () => {
        return expect(
          clonedExpect
            .it('to be a number after a short delay')
            .and('to be finite after a short delay')(false),
          'to be rejected with',
          '⨯ expected false to be a number after a short delay and\n' +
            '    expected false to be a number\n' +
            '⨯ expected false to be finite after a short delay'
        );
      });
    });

    describe('with a chained "or" construct', () => {
      it('should succeed', () => {
        return clonedExpect
          .it('to be a number after a short delay')
          .and('to be finite after a short delay')
          .or('to be a string after a short delay')('abc');
      });

      it('should fail with a diff', () => {
        return expect(
          clonedExpect
            .it('to be a number after a short delay')
            .and('to be finite after a short delay')
            .or('to be a string after a short delay')(false),
          'to be rejected with',
          '⨯ expected false to be a number after a short delay and\n' +
            '    expected false to be a number\n' +
            '⨯ expected false to be finite after a short delay\n' +
            'or\n' +
            '⨯ expected false to be a string after a short delay\n' +
            '    expected false to be a string'
        );
      });
    });
  });

  it('should not swallow a "missing assertion" error when using an expect.it(...).or(...) construct', () => {
    expect(
      function() {
        expect(
          'foo',
          'to satisfy',
          expect.it('this is misspelled', 1).or('to be a string')
        );
      },
      'to throw',
      "expected 'foo' to satisfy\n" +
        "expect.it('this is misspelled', 1)\n" +
        "      .or('to be a string')\n" +
        '\n' +
        "Unknown assertion 'this is misspelled', did you mean: 'to be fulfilled'"
    );
  });

  it('should fail with a "missing assertion" error even when it is not the first failing one in an "and" group', () => {
    expect(
      function() {
        expect(
          'foo',
          'to satisfy',
          expect
            .it('to begin with', 'bar')
            .and('misspelled')
            .or('to be a string')
        );
      },
      'to throw',
      "expected 'foo' to satisfy\n" +
        "expect.it('to begin with', 'bar')\n" +
        "        .and('misspelled')\n" +
        "      .or('to be a string')\n" +
        '\n' +
        "Unknown assertion 'misspelled', did you mean: 'called'"
    );
  });

  it('should not fail when the first clause in an or group specifies an assertion that is not defined for the given arguments', () => {
    expect(
      [false, 'foo', 'bar'],
      'to have items satisfying',
      expect.it('to be false').or('to be a string')
    );
  });

  describe('with forwarding of flags', () => {
    var clonedExpect = expect
      .clone()
      .addAssertion('<object> [not] to have a foo property of bar', function(
        expect,
        subject
      ) {
        return expect(subject, 'to satisfy', {
          foo: expect.it('[not] to equal', 'bar')
        });
      });

    describe('when the flag is not being forwarded', () => {
      it('should succeed', () => {
        clonedExpect({ foo: 'bar' }, 'to have a foo property of bar');
      });

      it('should fail with a diff', () => {
        return expect(
          function() {
            clonedExpect({ quux: 123 }, 'to have a foo property of bar');
          },
          'to throw',
          'expected { quux: 123 } to have a foo property of bar\n' +
            '\n' +
            '{\n' +
            '  quux: 123\n' +
            "  // missing: foo: should equal 'bar'\n" +
            '}'
        );
      });
    });

    describe('when the flag is being forwarded', () => {
      it('should succeed', () => {
        clonedExpect({ quux: 123 }, 'not to have a foo property of bar');
      });

      it('should fail with a diff', () => {
        return expect(
          function() {
            clonedExpect({ foo: 'bar' }, 'not to have a foo property of bar');
          },
          'to throw',
          "expected { foo: 'bar' } not to have a foo property of bar\n" +
            '\n' +
            '{\n' +
            "  foo: 'bar' // should not equal 'bar'\n" +
            '}'
        );
      });
    });
  });

  describe('when passed a function', () => {
    it('should succeed', () => {
      expect.it(function(value) {
        expect(value, 'to equal', 'foo');
      })('foo');
    });

    it('should fail with a diff', () => {
      expect(
        function() {
          expect.it(function(value) {
            expect(value, 'to equal', 'bar');
          })('foo');
        },
        'to throw',
        "expected 'foo' to equal 'bar'\n" + '\n' + '-foo\n' + '+bar'
      );
    });

    it('should fail when passed more than two arguments', () => {
      expect(
        function() {
          expect.it(function(value) {
            expect(value, 'to equal', 'bar');
          }, 'yadda')('foo');
        },
        'to throw',
        'expect.it(<function>) does not accept additional arguments'
      );
    });
  });
});
<commit_msg>Fix typo in test description<commit_after>/*global expect*/
describe('expect.it', () => {
  it('returns an expectation function that when applied runs the assertion on the given subject', () => {
    var expectation = expect.it('to be greater than', 14);
    expectation(20);
    expect(
      function() {
        expectation(10);
      },
      'to throw',
      'expected 10 to be greater than 14'
    );
  });

  it('is inspected as it is written', () => {
    var expectation = expect
      .it('to be a number')
      .and('to be less than', 14)
      .and('to be negative')
      .or('to be a string')
      .and('to have length', 6);
    expect(
      expect.inspect(expectation).toString(),
      'to equal',
      "expect.it('to be a number')\n" +
        "        .and('to be less than', 14)\n" +
        "        .and('to be negative')\n" +
        "      .or('to be a string')\n" +
        "        .and('to have length', 6)"
    );
  });

  it('does not catch errors that are not thrown by unexpected', () => {
    var clonedExpect = expect
      .clone()
      .addAssertion('explode', function(expect, subject) {
        throw new Error('Explosion');
      });

    expect(clonedExpect.it('explode'), 'to throw', 'Explosion');
  });

  describe('with chained and', () => {
    it('all assertions has to be satisfied', () => {
      var expectation = expect
        .it('to be a number')
        .and('to be less than', 14)
        .and('to be negative');
      expect(
        function() {
          expectation(20);
        },
        'to throw',
        '✓ expected 20 to be a number and\n' +
          '⨯ expected 20 to be less than 14 and\n' +
          '⨯ expected 20 to be negative'
      );
    });

    it('returns a new function', () => {
      var expectation = expect.it('to be a number');
      var compositeExpectation = expectation.and('to be less than', 14);
      expect(compositeExpectation, 'not to be', expectation);

      expectation(20);
      expect(
        function() {
          compositeExpectation(20);
        },
        'to throw',
        '✓ expected 20 to be a number and\n' +
          '⨯ expected 20 to be less than 14'
      );
    });

    it('outputs one failing assertion correctly', () => {
      var expectation = expect
        .it('to be a number')
        .and('to be less than', 14)
        .and('to be negative');
      expect(
        function() {
          expectation(8);
        },
        'to throw',
        '✓ expected 8 to be a number and\n' +
          '✓ expected 8 to be less than 14 and\n' +
          '⨯ expected 8 to be negative'
      );
    });
  });

  describe('with chained or', () => {
    it('succeeds if any expectations succeeds', () => {
      var expectation = expect
        .it('to be a number')
        .or('to be a string')
        .or('to be an array');
      expect(function() {
        expectation('success');
      }, 'not to throw');
    });

    it('fails if all the expectations fails', () => {
      var expectation = expect
        .it('to be a number')
        .and('to be greater than', 6)
        .or('to be a string')
        .and('to have length', 6)
        .or('to be an array');
      expect(
        function() {
          expectation('foobarbaz');
        },
        'to throw',
        "⨯ expected 'foobarbaz' to be a number and\n" +
          "⨯ expected 'foobarbaz' to be greater than 6\n" +
          'or\n' +
          "✓ expected 'foobarbaz' to be a string and\n" +
          "⨯ expected 'foobarbaz' to have length 6\n" +
          '    expected 9 to be 6\n' +
          'or\n' +
          "⨯ expected 'foobarbaz' to be an array"
      );
    });

    it('if there are no and-clauses it writes the failure output more compactly', () => {
      var expectation = expect
        .it('to be a number')
        .or('to be a string')
        .or('to be an array');
      expect(
        function() {
          expectation(true);
        },
        'to throw',
        '⨯ expected true to be a number or\n' +
          '⨯ expected true to be a string or\n' +
          '⨯ expected true to be an array'
      );
    });

    it('returns a new function', () => {
      var expectation = expect.it('to be a number');
      var compositeExpectation = expectation.or('to be a string');
      expect(compositeExpectation, 'not to be', expectation);

      expectation(20);
      expect(
        function() {
          compositeExpectation(20);
          compositeExpectation(true);
        },
        'to throw',
        '⨯ expected true to be a number or\n' + '⨯ expected true to be a string'
      );
    });
  });

  describe('with async assertions', () => {
    var clonedExpect = expect
      .clone()
      .addAssertion('to be a number after a short delay', function(
        expect,
        subject
      ) {
        expect.errorMode = 'nested';

        return expect.promise(function(run) {
          setTimeout(
            run(function() {
              expect(subject, 'to be a number');
            }),
            1
          );
        });
      })
      .addAssertion('to be finite after a short delay', function(
        expect,
        subject
      ) {
        expect.errorMode = 'nested';

        return expect.promise(function(run) {
          setTimeout(
            run(function() {
              expect(subject, 'to be finite');
            }),
            1
          );
        });
      })
      .addAssertion('to be a string after a short delay', function(
        expect,
        subject
      ) {
        expect.errorMode = 'nested';

        return expect.promise(function(run) {
          setTimeout(
            run(function() {
              expect(subject, 'to be a string');
            }),
            1
          );
        });
      });

    it('should succeed', () => {
      return clonedExpect.it('to be a number after a short delay')(123);
    });

    it('should fail with a diff', () => {
      return expect(
        clonedExpect.it('to be a number after a short delay')(false),
        'to be rejected with',
        'expected false to be a number after a short delay\n' +
          '  expected false to be a number'
      );
    });

    describe('with a chained "and" construct', () => {
      it('should succeed', () => {
        return clonedExpect
          .it('to be a number after a short delay')
          .and('to be finite after a short delay')(123);
      });

      it('should fail with a diff', () => {
        return expect(
          clonedExpect
            .it('to be a number after a short delay')
            .and('to be finite after a short delay')(false),
          'to be rejected with',
          '⨯ expected false to be a number after a short delay and\n' +
            '    expected false to be a number\n' +
            '⨯ expected false to be finite after a short delay'
        );
      });
    });

    describe('with a chained "or" construct', () => {
      it('should succeed', () => {
        return clonedExpect
          .it('to be a number after a short delay')
          .and('to be finite after a short delay')
          .or('to be a string after a short delay')('abc');
      });

      it('should fail with a diff', () => {
        return expect(
          clonedExpect
            .it('to be a number after a short delay')
            .and('to be finite after a short delay')
            .or('to be a string after a short delay')(false),
          'to be rejected with',
          '⨯ expected false to be a number after a short delay and\n' +
            '    expected false to be a number\n' +
            '⨯ expected false to be finite after a short delay\n' +
            'or\n' +
            '⨯ expected false to be a string after a short delay\n' +
            '    expected false to be a string'
        );
      });
    });
  });

  it('should not swallow a "missing assertion" error when using an expect.it(...).or(...) construct', () => {
    expect(
      function() {
        expect(
          'foo',
          'to satisfy',
          expect.it('this is misspelled', 1).or('to be a string')
        );
      },
      'to throw',
      "expected 'foo' to satisfy\n" +
        "expect.it('this is misspelled', 1)\n" +
        "      .or('to be a string')\n" +
        '\n' +
        "Unknown assertion 'this is misspelled', did you mean: 'to be fulfilled'"
    );
  });

  it('should fail with a "missing assertion" error even when it is not the first failing one in an "and" group', () => {
    expect(
      function() {
        expect(
          'foo',
          'to satisfy',
          expect
            .it('to begin with', 'bar')
            .and('misspelled')
            .or('to be a string')
        );
      },
      'to throw',
      "expected 'foo' to satisfy\n" +
        "expect.it('to begin with', 'bar')\n" +
        "        .and('misspelled')\n" +
        "      .or('to be a string')\n" +
        '\n' +
        "Unknown assertion 'misspelled', did you mean: 'called'"
    );
  });

  it('should not fail when the first clause in an or group specifies an assertion that is not defined for the given arguments', () => {
    expect(
      [false, 'foo', 'bar'],
      'to have items satisfying',
      expect.it('to be false').or('to be a string')
    );
  });

  describe('with forwarding of flags', () => {
    var clonedExpect = expect
      .clone()
      .addAssertion('<object> [not] to have a foo property of bar', function(
        expect,
        subject
      ) {
        return expect(subject, 'to satisfy', {
          foo: expect.it('[not] to equal', 'bar')
        });
      });

    describe('when the flag is not being forwarded', () => {
      it('should succeed', () => {
        clonedExpect({ foo: 'bar' }, 'to have a foo property of bar');
      });

      it('should fail with a diff', () => {
        return expect(
          function() {
            clonedExpect({ quux: 123 }, 'to have a foo property of bar');
          },
          'to throw',
          'expected { quux: 123 } to have a foo property of bar\n' +
            '\n' +
            '{\n' +
            '  quux: 123\n' +
            "  // missing: foo: should equal 'bar'\n" +
            '}'
        );
      });
    });

    describe('when the flag is being forwarded', () => {
      it('should succeed', () => {
        clonedExpect({ quux: 123 }, 'not to have a foo property of bar');
      });

      it('should fail with a diff', () => {
        return expect(
          function() {
            clonedExpect({ foo: 'bar' }, 'not to have a foo property of bar');
          },
          'to throw',
          "expected { foo: 'bar' } not to have a foo property of bar\n" +
            '\n' +
            '{\n' +
            "  foo: 'bar' // should not equal 'bar'\n" +
            '}'
        );
      });
    });
  });

  describe('when passed a function', () => {
    it('should succeed', () => {
      expect.it(function(value) {
        expect(value, 'to equal', 'foo');
      })('foo');
    });

    it('should fail with a diff', () => {
      expect(
        function() {
          expect.it(function(value) {
            expect(value, 'to equal', 'bar');
          })('foo');
        },
        'to throw',
        "expected 'foo' to equal 'bar'\n" + '\n' + '-foo\n' + '+bar'
      );
    });

    it('should fail when passed more than two arguments', () => {
      expect(
        function() {
          expect.it(function(value) {
            expect(value, 'to equal', 'bar');
          }, 'yadda')('foo');
        },
        'to throw',
        'expect.it(<function>) does not accept additional arguments'
      );
    });
  });
});
<|endoftext|>