text: // Copyright 2014 The Chromium Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  /**
   * @fileoverview A utility class for general braille functionality.
   */
  
  
  goog.provide('cvox.BrailleUtil');
  
  goog.require('cvox.ChromeVox');
  goog.require('cvox.DomUtil');
  goog.require('cvox.Focuser');
  goog.require('cvox.NavBraille');
  goog.require('cvox.NodeStateUtil');
  goog.require('cvox.Spannable');
  
  
  /**
   * Trimmable whitespace character that appears between consecutive items in
   * braille.
   * @const {string}
   */
  cvox.BrailleUtil.ITEM_SEPARATOR = ' ';
  
  
  /**
   * Messages considered as containers in braille.
   * Containers are distinguished from roles by their appearance higher up in the
   * DOM tree of a selected node.
   * This list should be very short.
   * @type {!Array.<string>}
   */
  cvox.BrailleUtil.CONTAINER = [
    'tag_h1_brl',
    'tag_h2_brl',
    'tag_h3_brl',
    'tag_h4_brl',
    'tag_h5_brl',
    'tag_h6_brl'
  ];
  
  
  /**
   * Maps a ChromeVox message id to a braille template.
   * The template takes one-character specifiers:
   * n: replaced with braille name.
   * r: replaced with braille role.
   * s: replaced with braille state.
   * c: replaced with braille container role; this potentially returns whitespace,
   * so place at the beginning or end of templates for trimming.
   * v: replaced with braille value.
   * @type {Object.<string, string>}
   */
  cvox.BrailleUtil.TEMPLATE = {
    'base': 'c n v r s',
    'aria_role_alert': 'r: n',
    'aria_role_button': 'n r s',
    'aria_role_checkbox': 'n r (s)',
    'aria_role_menuitemcheckbox': 'n r (s)',
    'aria_role_menuitemradio': 'n r (s)',
    'aria_role_radio': 'n r (s)',
    'aria_role_textbox': 'n: v r s',
    'input_type_button': 'n r s',
    'input_type_checkbox': 'n r (s)',
    'input_type_email': 'n: v r s',
    'input_type_number': 'n: v r s',
    'input_type_password': 'n: v r s',
    'input_type_radio': 'n r (s)',
    'input_type_search': 'n: v r s',
    'input_type_submit': 'n r s',
    'input_type_text': 'n: v r s',
    'input_type_tel': 'n: v r s',
    'input_type_url': 'n: v r s',
    'tag_button': 'n r s',
    'tag_textarea': 'n: v r s'
  };
  
  
  /**
   * Attached to the value region of a braille spannable.
   * @param {number} offset The offset of the span into the value.
   * @constructor
   */
  cvox.BrailleUtil.ValueSpan = function(offset) {
    /**
     * The offset of the span into the value.
     * @type {number}
     */
    this.offset = offset;
  };
  
  
  /**
   * Creates a value span from a json serializable object.
   * @param {!Object} obj The json serializable object to convert.
   * @return {!cvox.BrailleUtil.ValueSpan} The value span.
   */
  cvox.BrailleUtil.ValueSpan.fromJson = function(obj) {
    return new cvox.BrailleUtil.ValueSpan(obj.offset);
  };
  
  
  /**
   * Converts this object to a json serializable object.
   * @return {!Object} The JSON representation.
   */
  cvox.BrailleUtil.ValueSpan.prototype.toJson = function() {
    return this;
  };
  
  
  cvox.Spannable.registerSerializableSpan(
      cvox.BrailleUtil.ValueSpan,
      'cvox.BrailleUtil.ValueSpan',
      cvox.BrailleUtil.ValueSpan.fromJson,
      cvox.BrailleUtil.ValueSpan.prototype.toJson);
  
  
  /**
   * Attached to the selected text within a value.
   * @constructor
   */
  cvox.BrailleUtil.ValueSelectionSpan = function() {
  };
  
  
  cvox.Spannable.registerStatelessSerializableSpan(
      cvox.BrailleUtil.ValueSelectionSpan,
      'cvox.BrailleUtil.ValueSelectionSpan');
  
  
  /**
   * Gets the braille name for a node.
   * See DomUtil for a more precise definition of 'name'.
   * Additionally, whitespace is trimmed.
   * @param {Node} node The node.
   * @return {string} The string representation.
   */
  cvox.BrailleUtil.getName = function(node) {
    if (!node) {
      return '';
    }
    return cvox.DomUtil.getName(node).trim();
  };
  
  
  /**
   * Gets the braille role message id for a node.
   * See DomUtil for a more precise definition of 'role'.
   * @param {Node} node The node.
   * @return {string} The string representation.
   */
  cvox.BrailleUtil.getRoleMsg = function(node) {
    if (!node) {
      return '';
    }
    var roleMsg = cvox.DomUtil.getRoleMsg(node, cvox.VERBOSITY_VERBOSE);
    if (roleMsg) {
      roleMsg = cvox.DomUtil.collapseWhitespace(roleMsg);
    }
    if (roleMsg && (roleMsg.length > 0)) {
      if (cvox.ChromeVox.msgs.getMsg(roleMsg + '_brl')) {
        roleMsg += '_brl';
      }
    }
    return roleMsg;
  };
  
  
  /**
   * Transforms a {@code cvox.NodeState} list of state messages to the
   * corresponding messages for braille and expands them into a localized
   * string suitable for output on a braille display.
   * @param {cvox.NodeState} stateMsgs The states to expand.  The content of this
   *     array is modified.
   * @return {string} The string representation.
   * @private
   */
  cvox.BrailleUtil.expandStateMsgs_ = function(stateMsgs) {
    stateMsgs.forEach(function(state) {
      // Check to see if a variant of the message with '_brl' exists,
      // and use it if so.
      //
      // Note: many messages are templatized, and if we don't pass any
      // argument to substitute, getMsg might throw an error if the
      // resulting string is empty. To avoid this, we pass a dummy
      // substitution string array here.
      var dummySubs = ['dummy', 'dummy', 'dummy'];
      if (cvox.ChromeVox.msgs.getMsg(state[0] + '_brl', dummySubs)) {
        state[0] += '_brl';
      }
    });
    return cvox.NodeStateUtil.expand(stateMsgs);
  };
  
  
  /**
   * Gets the braille container role of a node.
   * @param {Node} prev The previous node in navigation.
   * @param {Node} node The node.
   * @return {string} The string representation.
   */
  cvox.BrailleUtil.getContainer = function(prev, node) {
    if (!prev || !node) {
      return '';
    }
    var ancestors = cvox.DomUtil.getUniqueAncestors(prev, node);
    for (var i = 0, container; container = ancestors[i]; i++) {
      var msg = cvox.BrailleUtil.getRoleMsg(container);
      if (msg && cvox.BrailleUtil.CONTAINER.indexOf(msg) != -1) {
        return cvox.ChromeVox.msgs.getMsg(msg);
      }
    }
    return '';
  };
  
  
  /**
   * Gets the braille value of a node. A cvox.BrailleUtil.ValueSpan will be
   * attached, along with (possibly) a cvox.BrailleUtil.ValueSelectionSpan.
   * @param {Node} node The node.
   * @return {!cvox.Spannable} The value spannable.
   */
  cvox.BrailleUtil.getValue = function(node) {
    if (!node) {
      return new cvox.Spannable();
    }
    var valueSpan = new cvox.BrailleUtil.ValueSpan(0 /* offset */);
    if (cvox.DomUtil.isInputTypeText(node)) {
      var value = node.value;
      if (node.type === 'password') {
        value = value.replace(/./g, '*');
      }
      var spannable = new cvox.Spannable(value, valueSpan);
      if (node === document.activeElement &&
          cvox.DomUtil.doesInputSupportSelection(node)) {
        var selectionStart = cvox.BrailleUtil.clamp_(
            node.selectionStart, 0, spannable.getLength());
        var selectionEnd = cvox.BrailleUtil.clamp_(
            node.selectionEnd, 0, spannable.getLength());
        spannable.setSpan(new cvox.BrailleUtil.ValueSelectionSpan(),
                          Math.min(selectionStart, selectionEnd),
                          Math.max(selectionStart, selectionEnd));
      }
      return spannable;
    } else if (node instanceof HTMLTextAreaElement) {
      var shadow = new cvox.EditableTextAreaShadow();
      shadow.update(node);
      var lineIndex = shadow.getLineIndex(node.selectionEnd);
      var lineStart = shadow.getLineStart(lineIndex);
      var lineEnd = shadow.getLineEnd(lineIndex);
      var lineText = node.value.substring(lineStart, lineEnd);
      valueSpan.offset = lineStart;
      var spannable = new cvox.Spannable(lineText, valueSpan);
      if (node === document.activeElement) {
        var selectionStart = cvox.BrailleUtil.clamp_(
            node.selectionStart - lineStart, 0, spannable.getLength());
        var selectionEnd = cvox.BrailleUtil.clamp_(
            node.selectionEnd - lineStart, 0, spannable.getLength());
        spannable.setSpan(new cvox.BrailleUtil.ValueSelectionSpan(),
                          Math.min(selectionStart, selectionEnd),
                          Math.max(selectionStart, selectionEnd));
      }
      return spannable;
    } else {
      return new cvox.Spannable(cvox.DomUtil.getValue(node), valueSpan);
    }
  };
  
  
  /**
   * Gets the templated representation of braille.
   * @param {Node} prev The previous node (during navigation).
   * @param {Node} node The node.
   * @param {{name:(undefined|string),
   * role:(undefined|string),
   * roleMsg:(undefined|string),
   * state:(undefined|string),
   * container:(undefined|string),
   * value:(undefined|cvox.Spannable)}|Object=} opt_override Override a
   * specific property for the given node.
   * @return {!cvox.Spannable} The string representation.
   */
  cvox.BrailleUtil.getTemplated = function(prev, node, opt_override) {
    opt_override = opt_override ? opt_override : {};
    var roleMsg = opt_override.roleMsg ||
        (node ? cvox.DomUtil.getRoleMsg(node, cvox.VERBOSITY_VERBOSE) : '');
    var template = cvox.BrailleUtil.TEMPLATE[roleMsg] ||
        cvox.BrailleUtil.TEMPLATE['base'];
    var state = opt_override.state;
    if (!state) {
      if (node) {
        state = cvox.BrailleUtil.expandStateMsgs_(
            cvox.DomUtil.getStateMsgs(node, true));
      } else {
        state = '';
      }
    }
    var role = opt_override.role || '';
    if (!role && roleMsg) {
      role = cvox.ChromeVox.msgs.getMsg(roleMsg + '_brl') ||
          cvox.ChromeVox.msgs.getMsg(roleMsg);
    }
  
    var templated = new cvox.Spannable();
    var mapChar = function(c) {
      switch (c) {
        case 'n':
          return opt_override.name || cvox.BrailleUtil.getName(node);
        case 'r':
          return role;
        case 's':
          return state;
        case 'c':
          return opt_override.container ||
              cvox.BrailleUtil.getContainer(prev, node);
        case 'v':
          return opt_override.value || cvox.BrailleUtil.getValue(node);
        default:
          return c;
      }
    };
    for (var i = 0; i < template.length; i++) {
      var component = mapChar(template[i]);
      templated.append(component);
      // Ignore the next whitespace separator if the current component is empty,
      // unless the empty value has a selection, in which case the cursor
      // should be placed on the empty space after the empty value.
      if (!component.toString() && template[i + 1] == ' ' &&
          (!(component instanceof cvox.Spannable) ||
          !/**@type {cvox.Spannable}*/(component).getSpanInstanceOf(
              cvox.BrailleUtil.ValueSelectionSpan))) {
        i++;
      }
    }
    return templated.trimRight();
  };
  
  
  /**
   * Creates a braille value from a string and, optionally, a selection range.
   * A cvox.BrailleUtil.ValueSpan will be
   * attached, along with a cvox.BrailleUtil.ValueSelectionSpan if applicable.
   * @param {string} text The text to display as the value.
   * @param {number=} opt_selStart Selection start.
   * @param {number=} opt_selEnd Selection end if different from selection start.
   * @param {number=} opt_textOffset Start offset of text.
   * @return {!cvox.Spannable} The value spannable.
   */
  cvox.BrailleUtil.createValue = function(text, opt_selStart, opt_selEnd,
                                          opt_textOffset) {
    var spannable = new cvox.Spannable(
        text, new cvox.BrailleUtil.ValueSpan(opt_textOffset || 0));
    if (goog.isDef(opt_selStart)) {
      opt_selEnd = goog.isDef(opt_selEnd) ? opt_selEnd : opt_selStart;
      // TODO(plundblad): This looses the distinction between the selection
      // anchor (start) and focus (end).  We should use that information to
      // decide where to pan the braille display.
      if (opt_selStart > opt_selEnd) {
        var temp = opt_selStart;
        opt_selStart = opt_selEnd;
        opt_selEnd = temp;
      }
  
      spannable.setSpan(new cvox.BrailleUtil.ValueSelectionSpan(),
            opt_selStart, opt_selEnd);
    }
    return spannable;
  };
  
  
  /**
   * Activates a position in a nav braille.  Moves the caret in text fields
   * and simulates a mouse click on the node at the position.
   *
   * @param {!cvox.NavBraille} braille the nav braille representing the display
   *        content that was active when the user issued the key command.
   *        The annotations in the spannable are used to decide what
   *        node to activate and what part of the node value (if any) to
   *        move the caret to.
   * @param {number=} opt_displayPosition position of the display that the user
   *                  activated, relative to the start of braille.
   */
  cvox.BrailleUtil.click = function(braille, opt_displayPosition) {
    var handled = false;
    var spans = braille.text.getSpans(opt_displayPosition || 0);
    var node = spans.filter(function(n) { return n instanceof Node; })[0];
    if (node) {
      if (goog.isDef(opt_displayPosition) &&
          (cvox.DomUtil.isInputTypeText(node) ||
              node instanceof HTMLTextAreaElement)) {
        var valueSpan = spans.filter(
            function(s) {
              return s instanceof cvox.BrailleUtil.ValueSpan;
            })[0];
        if (valueSpan) {
          if (document.activeElement !== node) {
            cvox.Focuser.setFocus(node);
          }
          var cursorPosition = opt_displayPosition -
              braille.text.getSpanStart(valueSpan) +
              valueSpan.offset;
          cvox.ChromeVoxEventWatcher.setUpTextHandler();
          node.selectionStart = node.selectionEnd = cursorPosition;
          cvox.ChromeVoxEventWatcher.handleTextChanged(true);
          handled = true;
        }
      }
    }
    if (!handled) {
      cvox.DomUtil.clickElem(
          node || cvox.ChromeVox.navigationManager.getCurrentNode(),
          false, false, false, true);
    }
  };
  
  
  /**
   * Clamps a number so it is within the given boundaries.
   * @param {number} number The number to clamp.
   * @param {number} min The minimum value to return.
   * @param {number} max The maximum value to return.
   * @return {number} {@code number} if it is within the bounds, or the nearest
   *     number within the bounds otherwise.
   * @private
   */
  cvox.BrailleUtil.clamp_ = function(number, min, max) {
    return Math.min(Math.max(number, min), max);
  };
  