<commit_before>/**
 * @file HTML Highlighter module.
 * @copyright 2015 Diffeo
 *
 * Comments:
 *
 */


(function (factory, window) {

  if(typeof window.define === "function" && window.define.amd) {
    window.define([ 'jquery', 'SortingCommon' ], function($, std) {
      return factory(window, $, std);
    } );
  }

} )(/** @lends <global> */ function (window, $, std, undefined) {

  /**
   * @class
   * Main class of the HTML Highlighter module, which exposes an API enabling
   * clients to control all the features supported related to highlighting and
   * text selection.
   *
   * @param {Object} options - Map containing options
   * */
  var Main = function (options)
  {
    /* Assign defaults. */
    options = $.extend(true, $.extend(true, {}, defaults), options);

    Object.defineProperties(this, {
      ui:      { value: new Ui(this, options) },
      options: { value: options               },
      cursor:  { value: new Cursor(this)      }
    } );

    this.queries = { };
    this.stats = {
      queries: 0,
      total: 0,
      highlight: 0
    };

    /* Start by refreshing the internal document's text representation. */
    this.refresh();
    console.info('HTML highlighter instantiated');
  };

  /**
   * Refreshes the internal representation of the text.  Should only be invoked
   * when the HTML structure mutates.
   */
  Main.prototype.refresh = function ()
  {
    this.content = new TextContent(this.options.container.get(0));

    /* TODO: remove */
    this.content.assert();
  };

  /**
   * Create a query set by the name and containing one or more queries.  If the
   * query set already exists, its contents and highlights are first destroyed
   * and new one created.
   *
   * Note that, at this point in time, only string queries are supported.
   *
   * @param {string} name - Name of the query set.
   * @param {string[]} queries - Array containing individual queries to
   * highlight. */
  Main.prototype.add = function (name, queries)
  {
    if(!std.is_arr(queries))
      throw 'Invalid or no queries array specified';

    var q, self = this,
        highlighter = new RangeHighlighter(this.stats.highlight);

    /* Remove query set if it exists. */
    if(name in this.queries)
      this.remove(name);

    q = this.queries[name] = {
      enabled: true,
      set: [ ]
    };

    /* TODO: There is currently no need to save every single highlight id in
     * the `sets´ array since, if I understand it correctly, individual query
     * set highlights will never be removed.  If they're never removed, then
     * one can assume that highlight ids within a particular query set are
     * contiguous, with the only attribute needed to be saved being the *start*
     * highlight id; the end highlight id can be computed by adding set length
     * minus 1 to start id.
     * --------------------------------------------------------------------- */
    /* For each query, perform a lookup in the internal text representation
     * and highlight each hit.  The global id of each highlight is recorded in
     * the `this.queries[name].set´ array. */
    queries.forEach(function (i) {
      var hit,
          finder = new TextFinder(self.content, i);

      while((hit = finder.next(i)) !== false)
        q.set.push(highlighter.do(hit));
    } );


    /* Update global statistics. */
    ++this.stats.queries;
    this.stats.total += q.set.length;

    /* Ensure CSS highlight class rolls over on overflow. */
    ++this.stats.highlight;
    if(this.stats.highlight >= this.options.maxHighlight)
      this.stats.highlight = 0;

    /* Set cursor on the first query of the first query set and update the UI
     * state. */
    this.cursor.clear(false);
    this.ui.update();
  };

  /**
   * Remove a query set by name.
   *
   * An exception is thrown if the query set does not exist.
   *
   * @param {string} name - Name of the query set to remove. */
  Main.prototype.remove = function (name)
  {
    this.remove_(name);

    this.cursor.clear(false);
    this.ui.update();
  };

  /**
   * Enable a query set.
   *
   * An exception is thrown if the query set does not exist.  If the query set
   * is currently already enabled, nothing is done.
   *
   * @param {string} name - Name of the query set to enable. */
  Main.prototype.enable = function (name)
  {
    var q = this.queries[name];

    if(q === undefined)
      throw 'Query set non-existent';
    else if(q.enabled)
      return;

    q.set.forEach(function (i) {
      $('.' + Css.highlight + '-id-' + i).removeClass(Css.disabled);
    } );

    q.enabled = true;
    this.stats.total += q.set.length;
    this.cursor.clear();
  };

  /**
   * Disable a query set.
   *
   * An exception is thrown if the query set does not exist.  If the query set
   * is currently already disabled, nothing is done.
   *
   * @parm {string} name - Name of the query set to disable. */
  Main.prototype.disable = function (name)
  {
    var q = this.queries[name];

    if(q === undefined)
      throw 'Query set non-existent';
    else if(!q.enabled)
      return;

    q.set.forEach(function (i) {
      $('.' + Css.highlight + '-id-' + i).addClass(Css.disabled);
    } );

    q.enabled = false;
    this.stats.total -= q.set.length;
    this.cursor.clear();
  };

  /**
   * Remove <strong>all</strong> query sets.
   * */
  Main.prototype.clear = function ()
  {
    for(var k in this.queries)
      this.remove_(k);

    if(!std.is_obj_empty(this.queries))
      throw "Query set object not empty";

    this.cursor.clear(false);
    this.ui.update();
  };

  /**
   * Convenience method to clear the current cursor state.  The cursor is set
   * to the first query if queries exist, otherwise it is set to an invalid
   * state. */
  Main.prototype.clearCursor = function ()
  {
    this.cursor.clear();
  };

  /**
   * Move cursor position to the next query in the active query set.  If the
   * cursor moves past the last query in the active query set, the active query
   * set moves to the next available one and the cursor position to its first
   * query.  If the current query set is the last in the collection and thus it
   * is not possible to move to the next query set, the first query set is made
   * active instead, thus ensuring that the cursor always rolls over. */
  Main.prototype.next = function ()
  {
    /* Do not worry about overflow; just increment it. */
    this.cursor.set(this.cursor.index + 1);
  };

  /**
   * Move cursor position to the previous query in the active query set.  If
   * the cursor moves past the first query in the active query set, the active
   * query set moves to the previous available one and the cursor position to
   * its last query.  If the current query set is the first in the collection
   * and thus it is not possible to move to the previous query set, the last
   * query set is made active instead, thus ensuring that the cursor always
   * rolls over. */
  Main.prototype.prev = function ()
  {
    this.cursor.set((this.cursor.index === 0
                     ? this.stats.total
                     : this.cursor.index) - 1);
  };

  /**
   * Return the current selected text range in the form of a <code>Range</code>
   * object.  If there is no selected text, <code>null</code> is returned.
   *
   * @returns {Range|null} The current selected text range or <code>null</code>
   * if it could not be computed. */
  Main.prototype.getSelectedRange = function ()
  {
    var sel = window.getSelection(), len;

    /* Note: assigning `len´ in conditional below so as to simplify code;
     * would require additional lines of code otherwise. */
    if(!(sel && sel.anchorNode && (len = sel.toString().length) > 0))
      return null;
    else if(sel.anchorNode.nodeType !== 3 || sel.focusNode.nodeType !== 3) {
      console.info('Selection anchor or focus node(s) not text: ignoring');
      return null;
    }

    /* Determine start and end indices in text offset markers array. */
    var start = this.content.find(sel.anchorNode),
        end = (sel.focusNode === sel.anchorNode
               ? start : this.content.find(sel.focusNode));

    if(start < 0 || end < 0) {
      console.error('Unable to retrieve offset of selection anchor or focus'
                    + 'node(s)', sel.anchorNode, sel.focusNode);
      return null;
    }

    /* Create start and end range descriptors, whilst accounting for inverse
     * selection where the user selects text in a right to left orientation. */
    if(start < end || (start === end && sel.anchorOffset < sel.focusOffset)) {
      start = Range.descriptorRel(this.content.at(start), sel.anchorOffset);

      if(sel.focusNode === sel.anchorNode) {
        end = $.extend(true, { }, start);
        end.offset = start.offset + len - 1;
      } else
        end = Range.descriptorRel(this.content.at(end), sel.focusOffset - 1);
    } else {
      var mi = start;
      start = Range.descriptorRel(this.content.at(end), sel.focusOffset);

      if(sel.focusNode === sel.anchorNode) {
        end = $.extend(true, { }, start);
        end.offset = end.offset + len - 1;
      } else
        end = Range.descriptorRel(this.content.at(mi), sel.anchorOffset - 1);
    }

    return new Range(this.content, start, end);
  };

  /**
   * Return boolean indicative of whether one or more query sets are currently
   * contained.
   *
   * @returns {boolean} <code>false</code> if no query sets currently
   * contained; <code>true</code> otherwise. */
  Main.prototype.empty = function ()
  {
    for(var k in this.queries) {
      if(this.queries[k].set.length > 0)
        return false;
    }

    return true;
  };

  /* Private interface
   * ----------------- */
  /**
   * Remove a query set by name.<commit_msg>module: add method for clearing of selected text<commit_after>/**
 * @file HTML Highlighter module.
 * @copyright 2015 Diffeo
 *
 * Comments:
 *
 */


(function (factory, window) {

  if(typeof window.define === "function" && window.define.amd) {
    window.define([ 'jquery', 'SortingCommon' ], function($, std) {
      return factory(window, $, std);
    } );
  }

} )(/** @lends <global> */ function (window, $, std, undefined) {

  /**
   * @class
   * Main class of the HTML Highlighter module, which exposes an API enabling
   * clients to control all the features supported related to highlighting and
   * text selection.
   *
   * @param {Object} options - Map containing options
   * */
  var Main = function (options)
  {
    /* Assign defaults. */
    options = $.extend(true, $.extend(true, {}, defaults), options);

    Object.defineProperties(this, {
      ui:      { value: new Ui(this, options) },
      options: { value: options               },
      cursor:  { value: new Cursor(this)      }
    } );

    this.queries = { };
    this.stats = {
      queries: 0,
      total: 0,
      highlight: 0
    };

    /* Start by refreshing the internal document's text representation. */
    this.refresh();
    console.info('HTML highlighter instantiated');
  };

  /**
   * Refreshes the internal representation of the text.  Should only be invoked
   * when the HTML structure mutates.
   */
  Main.prototype.refresh = function ()
  {
    this.content = new TextContent(this.options.container.get(0));

    /* TODO: remove */
    this.content.assert();
  };

  /**
   * Create a query set by the name and containing one or more queries.  If the
   * query set already exists, its contents and highlights are first destroyed
   * and new one created.
   *
   * Note that, at this point in time, only string queries are supported.
   *
   * @param {string} name - Name of the query set.
   * @param {string[]} queries - Array containing individual queries to
   * highlight. */
  Main.prototype.add = function (name, queries)
  {
    if(!std.is_arr(queries))
      throw 'Invalid or no queries array specified';

    var q, self = this,
        highlighter = new RangeHighlighter(this.stats.highlight);

    /* Remove query set if it exists. */
    if(name in this.queries)
      this.remove(name);

    q = this.queries[name] = {
      enabled: true,
      set: [ ]
    };

    /* TODO: There is currently no need to save every single highlight id in
     * the `sets´ array since, if I understand it correctly, individual query
     * set highlights will never be removed.  If they're never removed, then
     * one can assume that highlight ids within a particular query set are
     * contiguous, with the only attribute needed to be saved being the *start*
     * highlight id; the end highlight id can be computed by adding set length
     * minus 1 to start id.
     * --------------------------------------------------------------------- */
    /* For each query, perform a lookup in the internal text representation
     * and highlight each hit.  The global id of each highlight is recorded in
     * the `this.queries[name].set´ array. */
    queries.forEach(function (i) {
      var hit,
          finder = new TextFinder(self.content, i);

      while((hit = finder.next(i)) !== false)
        q.set.push(highlighter.do(hit));
    } );


    /* Update global statistics. */
    ++this.stats.queries;
    this.stats.total += q.set.length;

    /* Ensure CSS highlight class rolls over on overflow. */
    ++this.stats.highlight;
    if(this.stats.highlight >= this.options.maxHighlight)
      this.stats.highlight = 0;

    /* Set cursor on the first query of the first query set and update the UI
     * state. */
    this.cursor.clear(false);
    this.ui.update();
  };

  /**
   * Remove a query set by name.
   *
   * An exception is thrown if the query set does not exist.
   *
   * @param {string} name - Name of the query set to remove. */
  Main.prototype.remove = function (name)
  {
    this.remove_(name);

    this.cursor.clear(false);
    this.ui.update();
  };

  /**
   * Enable a query set.
   *
   * An exception is thrown if the query set does not exist.  If the query set
   * is currently already enabled, nothing is done.
   *
   * @param {string} name - Name of the query set to enable. */
  Main.prototype.enable = function (name)
  {
    var q = this.queries[name];

    if(q === undefined)
      throw 'Query set non-existent';
    else if(q.enabled)
      return;

    q.set.forEach(function (i) {
      $('.' + Css.highlight + '-id-' + i).removeClass(Css.disabled);
    } );

    q.enabled = true;
    this.stats.total += q.set.length;
    this.cursor.clear();
  };

  /**
   * Disable a query set.
   *
   * An exception is thrown if the query set does not exist.  If the query set
   * is currently already disabled, nothing is done.
   *
   * @parm {string} name - Name of the query set to disable. */
  Main.prototype.disable = function (name)
  {
    var q = this.queries[name];

    if(q === undefined)
      throw 'Query set non-existent';
    else if(!q.enabled)
      return;

    q.set.forEach(function (i) {
      $('.' + Css.highlight + '-id-' + i).addClass(Css.disabled);
    } );

    q.enabled = false;
    this.stats.total -= q.set.length;
    this.cursor.clear();
  };

  /**
   * Remove <strong>all</strong> query sets.
   * */
  Main.prototype.clear = function ()
  {
    for(var k in this.queries)
      this.remove_(k);

    if(!std.is_obj_empty(this.queries))
      throw "Query set object not empty";

    this.cursor.clear(false);
    this.ui.update();
  };

  /**
   * Convenience method to clear the current cursor state.  The cursor is set
   * to the first query if queries exist, otherwise it is set to an invalid
   * state. */
  Main.prototype.clearCursor = function ()
  {
    this.cursor.clear();
  };

  /**
   * Move cursor position to the next query in the active query set.  If the
   * cursor moves past the last query in the active query set, the active query
   * set moves to the next available one and the cursor position to its first
   * query.  If the current query set is the last in the collection and thus it
   * is not possible to move to the next query set, the first query set is made
   * active instead, thus ensuring that the cursor always rolls over. */
  Main.prototype.next = function ()
  {
    /* Do not worry about overflow; just increment it. */
    this.cursor.set(this.cursor.index + 1);
  };

  /**
   * Move cursor position to the previous query in the active query set.  If
   * the cursor moves past the first query in the active query set, the active
   * query set moves to the previous available one and the cursor position to
   * its last query.  If the current query set is the first in the collection
   * and thus it is not possible to move to the previous query set, the last
   * query set is made active instead, thus ensuring that the cursor always
   * rolls over. */
  Main.prototype.prev = function ()
  {
    this.cursor.set((this.cursor.index === 0
                     ? this.stats.total
                     : this.cursor.index) - 1);
  };

  /**
   * Return the current selected text range in the form of a <code>Range</code>
   * object.  If there is no selected text, <code>null</code> is returned.
   *
   * @returns {Range|null} The current selected text range or <code>null</code>
   * if it could not be computed. */
  Main.prototype.getSelectedRange = function ()
  {
    var sel = window.getSelection(), len;

    /* Note: assigning `len´ in conditional below so as to simplify code;
     * would require additional lines of code otherwise. */
    if(!(sel && sel.anchorNode && (len = sel.toString().length) > 0))
      return null;
    else if(sel.anchorNode.nodeType !== 3 || sel.focusNode.nodeType !== 3) {
      console.info('Selection anchor or focus node(s) not text: ignoring');
      return null;
    }

    /* Determine start and end indices in text offset markers array. */
    var start = this.content.find(sel.anchorNode),
        end = (sel.focusNode === sel.anchorNode
               ? start : this.content.find(sel.focusNode));

    if(start < 0 || end < 0) {
      console.error('Unable to retrieve offset of selection anchor or focus'
                    + 'node(s)', sel.anchorNode, sel.focusNode);
      return null;
    }

    /* Create start and end range descriptors, whilst accounting for inverse
     * selection where the user selects text in a right to left orientation. */
    if(start < end || (start === end && sel.anchorOffset < sel.focusOffset)) {
      start = Range.descriptorRel(this.content.at(start), sel.anchorOffset);

      if(sel.focusNode === sel.anchorNode) {
        end = $.extend(true, { }, start);
        end.offset = start.offset + len - 1;
      } else
        end = Range.descriptorRel(this.content.at(end), sel.focusOffset - 1);
    } else {
      var mi = start;
      start = Range.descriptorRel(this.content.at(end), sel.focusOffset);

      if(sel.focusNode === sel.anchorNode) {
        end = $.extend(true, { }, start);
        end.offset = end.offset + len - 1;
      } else
        end = Range.descriptorRel(this.content.at(mi), sel.anchorOffset - 1);
    }

    return new Range(this.content, start, end);
  };

  Main.prototype.clearSelectedRange = function ()
  {
    /* From: http://stackoverflow.com/a/3169849/3001914
     * Note that we don't support IE at all. */
    if (window.getSelection) {
      if (window.getSelection().empty)                    /* Chrome */
        window.getSelection().empty();
      else if (window.getSelection().removeAllRanges)     /* Firefox */
        window.getSelection().removeAllRanges();
    }
  };

  /**
   * Return boolean indicative of whether one or more query sets are currently
   * contained.
   *
   * @returns {boolean} <code>false</code> if no query sets currently
   * contained; <code>true</code> otherwise. */
  Main.prototype.empty = function ()
  {
    for(var k in this.queries) {
      if(this.queries[k].set.length > 0)
        return false;
    }

    return true;
  };

  /* Private interface
   * ----------------- */
  /**
   * Remove a query set by name.
{'fraction_non_alphanumeric': 0.12648162040510127, 'fraction_numerical': 0.003000750187546887, 'mean_word_length': 2.4117044872888584, 'pattern_counts': {'":': 0, '<': 37, '<?xml version=': 0, '>': 35, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6476437', 'n_tokens_mistral': 5921, 'n_tokens_neox': 5717, 'n_words': 2029}