text: /*
   * Copyright (c) 2015 Adobe Systems Incorporated. All rights reserved.
   *  
   * Permission is hereby granted, free of charge, to any person obtaining a
   * copy of this software and associated documentation files (the "Software"), 
   * to deal in the Software without restriction, including without limitation 
   * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
   * and/or sell copies of the Software, and to permit persons to whom the 
   * Software is furnished to do so, subject to the following conditions:
   *  
   * The above copyright notice and this permission notice shall be included in
   * all copies or substantial portions of the Software.
   *  
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
   * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
   * DEALINGS IN THE SOFTWARE.
   * 
   */
  
  /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
  /*global define, $ */
  
  define(function (require, exports, module) {
      "use strict";
      
      // Load dependencies.
      var TokenUtils  = require("utils/TokenUtils");
      
      // Enums of token types.
      var TOKEN_TAG    = 1,
          TOKEN_ATTR   = 2,
          TOKEN_VALUE  = 3;
      
      // Regex to find whitespace.
      var regexWhitespace = /^\s+$/;
      
      /**
       * Returns an object that represents all its params.
       *
       * @param {!Token} token CodeMirror token at the current pos
       * @param {number} tokenType Type of current token
       * @param {number} offset Offset in current token
       * @param {Array.<string>} exclusionList List of attributes of a tag or attribute options used by an attribute
       * @param {string} tagName Name of the current tag
       * @param {string} attrName Name of the curent attribute
       * @param {boolean} shouldReplace true if we don't want to append ="" to an attribute
       * @return {!{token: Token, tokenType: int, offset: int, exclusionList: Array.<string>, tagName: string, attrName: string, shouldReplace: boolean}}
       */
      function _createTagInfo(token, tokenType, offset, exclusionList, tagName, attrName, shouldReplace) {
          return {
              token: token || null,
              tokenType: tokenType || null,
              offset: offset || 0,
              exclusionList: exclusionList || [],
              tagName: tagName || "",
              attrName: attrName || "",
              shouldReplace: shouldReplace || false
          };
      }
      
      /**
       * Return the tagName and a list of attributes used by the tag.
       *
       * @param {!Editor} editor An instance of active editor
       * @param {!{line: number, ch: number}} constPos The position of cursor in the active editor
       * @return {!{tagName: string, exclusionList: Array.<string>, shouldReplace: boolean}}
       */
      function _getTagAttributes(editor, constPos) {
          var pos, ctx, ctxPrev, ctxNext, ctxTemp, tagName, exclusionList = [], shouldReplace;
          
          pos = $.extend({}, constPos);
          ctx = TokenUtils.getInitialContext(editor._codeMirror, pos);
          
          // Stop if the cursor is before = or an attribute value.
          ctxTemp = $.extend(true, {}, ctx);
          if (ctxTemp.token.type === null && regexWhitespace.test(ctxTemp.token.string)) {
              if (TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctxTemp)) {
                  if ((ctxTemp.token.type === null && ctxTemp.token.string === "=") ||
                          ctxTemp.token.type === "string") {
                      return null;
                  }
                  TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctxTemp);
              }
          }
          
          // Incase an attribute is followed by an equal sign, shouldReplace will be used
          // to prevent from appending ="" again.
          if (ctxTemp.token.type === "attribute") {
              if (TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctxTemp)) {
                  if (ctxTemp.token.type === null && ctxTemp.token.string === "=") {
                      shouldReplace = true;
                  }
              }
          }
          
          // Look-Back and get the attributes and tag name.
          pos = $.extend({}, constPos);
          ctxPrev = TokenUtils.getInitialContext(editor._codeMirror, pos);
          while (TokenUtils.movePrevToken(ctxPrev)) {
              if (ctxPrev.token.type && ctxPrev.token.type.indexOf("tag bracket") >= 0) {
                  // Disallow hints in closed tag and inside tag content
                  if (ctxPrev.token.string === "</" || ctxPrev.token.string.indexOf(">") !== -1) {
                      return null;
                  }
              }
              
              // Get attributes.
              if (ctxPrev.token.type === "attribute") {
                  exclusionList.push(ctxPrev.token.string);
              }
              
              // Get tag.
              if (ctxPrev.token.type === "tag") {
                  tagName = ctxPrev.token.string;
                  if (TokenUtils.movePrevToken(ctxPrev)) {
                      if (ctxPrev.token.type === "tag bracket" && ctxPrev.token.string === "<") {
                          break;
                      }
                      return null;
                  }
              }
          }
          
          // Look-Ahead and find rest of the attributes.
          pos = $.extend({}, constPos);
          ctxNext = TokenUtils.getInitialContext(editor._codeMirror, pos);
          while (TokenUtils.moveNextToken(ctxNext)) {
              if (ctxNext.token.type === "string" && ctxNext.token.string === "\"") {
                  return null;
              }
              
              // Stop on closing bracket of its own tag or opening bracket of next tag.
              if (ctxNext.token.type === "tag bracket" &&
                      (ctxNext.token.string.indexOf(">") >= 0 || ctxNext.token.string === "<")) {
                  break;
              }
              if (ctxNext.token.type === "attribute" && exclusionList.indexOf(ctxNext.token.string) === -1) {
                  exclusionList.push(ctxNext.token.string);
              }
          }
          return {
              tagName: tagName,
              exclusionList: exclusionList,
              shouldReplace: shouldReplace
          };
      }
      
      /**
       * Return the tag name, attribute name and a list of options used by the attribute
       *
       * @param {!Editor} editor An instance of active editor
       * @param {!{line: number, ch: number}} pos Position of cursor in the editor
       * @return {!{tagName: string, attrName: string, exclusionList: Array.<string>}}
       */
      function _getTagAttributeValue(editor, pos) {
          var ctx, tagName, attrName, exclusionList = [], offset, textBefore, textAfter;
          
          ctx = TokenUtils.getInitialContext(editor._codeMirror, pos);
          offset = TokenUtils.offsetInToken(ctx);
          
          // To support multiple options on the same attribute, we have
          // to break the value, these values will not be available then.
          if (ctx.token.type === "string" && /\s+/.test(ctx.token.string)) {
              textBefore = ctx.token.string.substr(1, offset);
              textAfter = ctx.token.string.substr(offset);
              
              // Remove quote from end of the string.
              if (/^['"]$/.test(ctx.token.string.substr(-1, 1))) {
                  textAfter = textAfter.substr(0, textAfter.length - 1);
              }
              
              // Split the text before and after the offset, skipping the current query.
              exclusionList = exclusionList.concat(textBefore.split(/\s+/).slice(0, -1));
              exclusionList = exclusionList.concat(textAfter.split(/\s+/));
  
              // Filter through the list removing empty strings.
              exclusionList = exclusionList.filter(function (value) {
                  if (value.length > 0) {
                      return true;
                  }
              });
          }
          
          // Look-back and find tag and attributes.
          while (TokenUtils.movePrevToken(ctx)) {
              if (ctx.token.type === "tag bracket") {
                  // Disallow hints in closing tags.
                  if (ctx.token.string === "</") {
                      return null;
                  }
                  // Stop when closing bracket of another tag or opening bracket of its own in encountered.
                  if (ctx.token.string.indexOf(">") >= 0 || ctx.token.string === "<") {
                      break;
                  }
              }
              
              // Get the first previous attribute.
              if (ctx.token.type === "attribute" && !attrName) {
                  attrName = ctx.token.string;
              }
              
              // Stop if we get a bracket after tag.
              if (ctx.token.type === "tag") {
                  tagName = ctx.token.string;
                  if (TokenUtils.movePrevToken(ctx)) {
                      if (ctx.token.type === "tag bracket" && ctx.token.string === "<") {
                          break;
                      }
                      return null;
                  }
              }
          }
          
          return {
              tagName: tagName,
              attrName: attrName,
              exclusionList: exclusionList
          };
      }
      
      /**
       * Return the tag info at a given position in the active editor
       *
       * @param {!Editor} editor Instance of active editor
       * @param {!{line: number, ch: number}} pos Position of cursor in the editor
       * @return {!{token: Object, tokenType: number, offset: number, exclusionList: Array.<string>, tagName: string, attrName: string, shouldReplace: boolean}}
       */
      function getTagInfo(editor, pos) {
          var ctx, offset, tagAttrs, tagAttrValue;
          
          ctx = TokenUtils.getInitialContext(editor._codeMirror, pos);
          offset = TokenUtils.offsetInToken(ctx);
          
          if (ctx.token && ctx.token.type === "tag bracket" && ctx.token.string === "<") {
              // Returns tagInfo when an angle bracket is created.
              return _createTagInfo(ctx.token, TOKEN_TAG);
          } else if (ctx.token && ctx.token.type === "tag") {
              // Return tagInfo when a tag is created.
              if (TokenUtils.movePrevToken(ctx)) {
                  if (ctx.token.type === "tag bracket" && ctx.token.string === "<") {
                      TokenUtils.moveNextToken(ctx);
                      return _createTagInfo(ctx.token, TOKEN_TAG, offset);
                  }
              }
          } else if (ctx.token && (ctx.token.type === "attribute" ||
                                   (ctx.token.type === null && regexWhitespace.test(ctx.token.string)))) {
              // Return tagInfo when an attribute is created.
              tagAttrs = _getTagAttributes(editor, pos);
              if (tagAttrs && tagAttrs.tagName) {
                  return _createTagInfo(ctx.token, TOKEN_ATTR, offset, tagAttrs.exclusionList, tagAttrs.tagName, null, tagAttrs.shouldReplace);
              }
          } else if (ctx.token && ((ctx.token.type === null && ctx.token.string === "=") ||
                                   (ctx.token.type === "string" && /^['"]$/.test(ctx.token.string.charAt(0))))) {
              // Return tag info when an attribute value is created.
              // Allow no hints if the cursor is outside the value.
              if (ctx.token.type === "string" &&
                      /^['"]$/.test(ctx.token.string.substr(-1, 1)) &&
                      ctx.token.string.length !== 1 &&
                      ctx.token.end === pos.ch) {
                  return _createTagInfo();
              }
              
              tagAttrValue = _getTagAttributeValue(editor, pos);
              if (tagAttrValue && tagAttrValue.tagName && tagAttrValue.attrName) {
                  return _createTagInfo(ctx.token, TOKEN_VALUE, offset, tagAttrValue.exclusionList, tagAttrValue.tagName, tagAttrValue.attrName);
              }
          }
          return _createTagInfo();
      }
      
      /**
       * Return the query text of a value.
       * 
       * @param {!{token: Object, tokenType: number, offset: number, exclusionList: Array.<string>, tagName: string, attrName: string, shouldReplace: boolean}}
       * @return {string}  The query to use to matching hints.
       */
      function getValueQuery(tagInfo) {
          var query;
          if (tagInfo.token.string === "=") {
              return "";
          }
          // Remove quotation marks in query.
          query = tagInfo.token.string.substr(1, tagInfo.offset - 1);
          
          // Get the last option to use as a query to support multiple options.
          return query.split(/\s+/).slice(-1)[0];
      }
      
      // Expose public API.
      exports.getTagInfo      = getTagInfo;
      exports.getValueQuery   = getValueQuery;
      exports.regexWhitespace = regexWhitespace;
      exports.TOKEN_TAG       = TOKEN_TAG;
      exports.TOKEN_ATTR      = TOKEN_ATTR;
      exports.TOKEN_VALUE     = TOKEN_VALUE;
  });
  