text: /**
   * Copyright 2013-2014 Facebook, Inc.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   * http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  /*global exports:true*/
  "use strict";
  
  var Syntax = require('esprima-fb').Syntax;
  var utils = require('jstransform/src/utils');
  
  var FALLBACK_TAGS = require('./xjs').knownTags;
  var renderXJSExpressionContainer =
    require('./xjs').renderXJSExpressionContainer;
  var renderXJSLiteral = require('./xjs').renderXJSLiteral;
  var quoteAttrName = require('./xjs').quoteAttrName;
  
  var trimLeft = require('./xjs').trimLeft;
  
  /**
   * Customized desugar processor.
   *
   * Currently: (Somewhat tailored to React)
   * <X> </X> => X(null, null)
   * <X prop="1" /> => X({prop: '1'}, null)
   * <X prop="2"><Y /></X> => X({prop:'2'}, Y(null, null))
   * <X prop="2"><Y /><Z /></X> => X({prop:'2'}, [Y(null, null), Z(null, null)])
   *
   * Exceptions to the simple rules above:
   * if a property is named "class" it will be changed to "className" in the
   * javascript since "class" is not a valid object key in javascript.
   */
  
  var JSX_ATTRIBUTE_TRANSFORMS = {
    cxName: function(attr) {
      throw new Error(
        "cxName is no longer supported, use className={cx(...)} instead"
      );
    }
  };
  
  /**
   * Removes all non-whitespace/parenthesis characters
   */
  var reNonWhiteParen = /([^\s\(\)])/g;
  function stripNonWhiteParen(value) {
    return value.replace(reNonWhiteParen, '');
  }
  
  function throwSyntaxError(object, message) {
    var error = new SyntaxError(message);
    error.lineNumber = object.loc.start.line;
    error.column = object.loc.start.column;
    throw error;
  }
  
  function visitReactTag(traverse, object, path, state) {
    var jsxObjIdent = utils.getDocblock(state).jsx;
  
    // only run react when react @jsx namespace is specified in docblock
    if (!jsxObjIdent) {
      throwSyntaxError(object,
        '/** @jsx */ namespace was not found, ' +
        'not sure how to compile ' +
        utils.getNodeSourceText(object, state) +
        ' to JavaScript.'
      );
    }
  
    var openingElement = object.openingElement;
    var nameObject = openingElement.name;
    var attributesObject = openingElement.attributes;
  
    utils.catchup(openingElement.range[0], state, trimLeft);
  
    if (nameObject.type === Syntax.XJSNamespacedName && nameObject.namespace) {
      throwSyntaxError(nameObject, 'Namespace tags are not supported. ReactJSX is not XML.');
    }
  
    // Only identifiers can be fallback tags or need quoting. We don't need to
    // handle quoting for other types.
    var didAddTag = false;
  
    // Only identifiers can be fallback tags. XJSMemberExpressions are not.
    if (nameObject.type === Syntax.XJSIdentifier) {
      var tagName = nameObject.name;
      var quotedTagName = quoteAttrName(tagName);
  
      if (FALLBACK_TAGS.hasOwnProperty(tagName)) {
        // "Properly" handle invalid identifiers, like <font-face>, which needs to
        // be enclosed in quotes.
        var predicate =
          tagName === quotedTagName ?
            ('.' + tagName) :
            ('[' + quotedTagName + ']');
        utils.append(jsxObjIdent + predicate, state);
        utils.move(nameObject.range[1], state);
        didAddTag = true;
      } else if (tagName !== quotedTagName) {
        // If we're in the case where we need to quote and but don't recognize the
        // tag, throw.
        throwSyntaxError(tagName,
          'Tags must be valid JS identifiers or a recognized special case. `<' +
          tagName + '>` is not one of them.'
        );
      }
    }
  
    // Use utils.catchup in this case so we can easily handle XJSMemberExpressions
    // which look like Foo.Bar.Baz. This also handles unhyphenated XJSIdentifiers
    // that aren't fallback tags.
    if (!didAddTag) {
      utils.move(nameObject.range[0], state);
      utils.catchup(nameObject.range[1], state);
    }
  
    utils.append('(', state);
  
    var hasAttributes = attributesObject.length;
  
    var hasAtLeastOneSpreadProperty = attributesObject.some(function(attr) {
      return attr.type === Syntax.XJSSpreadAttribute;
    });
  
    // if we don't have any attributes, pass in null
    if (hasAtLeastOneSpreadProperty) {
      utils.append('Object.assign({', state);
    } else if (hasAttributes) {
      utils.append('{', state);
    } else {
      utils.append('null', state);
    }
  
    // keep track of if the previous attribute was a spread attribute
    var previousWasSpread = false;
  
    // write attributes
    attributesObject.forEach(function(attr, index) {
      var isLast = index === attributesObject.length - 1;
  
      if (attr.type === Syntax.XJSSpreadAttribute) {
        // Plus 1 to skip `{`.
        utils.move(attr.range[0] + 1, state);
  
        // Close the previous object or initial object
        if (!previousWasSpread) {
          utils.append('}, ', state);
        }
  
        // Move to the expression start, ignoring everything except parenthesis
        // and whitespace.
        utils.catchup(attr.argument.range[0], state, stripNonWhiteParen);
  
        traverse(attr.argument, path, state);
  
        utils.catchup(attr.argument.range[1], state);
  
        // Move to the end, ignoring parenthesis and the closing `}`
        utils.catchup(attr.range[1] - 1, state, stripNonWhiteParen);
  
        if (!isLast) {
          utils.append(', ', state);
        }
  
        utils.move(attr.range[1], state);
  
        previousWasSpread = true;
  
        return;
      }
  
      // If the next attribute is a spread, we're effective last in this object
      if (!isLast) {
        isLast = attributesObject[index + 1].type === Syntax.XJSSpreadAttribute;
      }
  
      if (attr.name.namespace) {
        throwSyntaxError(attr.name.namespace,
           'Namespace attributes are not supported. ReactJSX is not XML.');
      }
      var name = attr.name.name;
  
      utils.catchup(attr.range[0], state, trimLeft);
  
      if (previousWasSpread) {
        utils.append('{', state);
      }
  
      utils.append(quoteAttrName(name), state);
      utils.append(': ', state);
  
      if (!attr.value) {
        state.g.buffer += 'true';
        state.g.position = attr.name.range[1];
        if (!isLast) {
          utils.append(', ', state);
        }
      } else {
        utils.move(attr.name.range[1], state);
        // Use catchupNewlines to skip over the '=' in the attribute
        utils.catchupNewlines(attr.value.range[0], state);
        if (JSX_ATTRIBUTE_TRANSFORMS.hasOwnProperty(attr.name.name)) {
          utils.append(JSX_ATTRIBUTE_TRANSFORMS[attr.name.name](attr), state);
          utils.move(attr.value.range[1], state);
          if (!isLast) {
            utils.append(', ', state);
          }
        } else if (attr.value.type === Syntax.Literal) {
          renderXJSLiteral(attr.value, isLast, state);
        } else {
          renderXJSExpressionContainer(traverse, attr.value, isLast, path, state);
        }
      }
  
      utils.catchup(attr.range[1], state, trimLeft);
  
      previousWasSpread = false;
  
    });
  
    if (!openingElement.selfClosing) {
      utils.catchup(openingElement.range[1] - 1, state, trimLeft);
      utils.move(openingElement.range[1], state);
    }
  
    if (hasAttributes && !previousWasSpread) {
      utils.append('}', state);
    }
  
    if (hasAtLeastOneSpreadProperty) {
      utils.append(')', state);
    }
  
    // filter out whitespace
    var childrenToRender = object.children.filter(function(child) {
      return !(child.type === Syntax.Literal
               && typeof child.value === 'string'
               && child.value.match(/^[ \t]*[\r\n][ \t\r\n]*$/));
    });
    if (childrenToRender.length > 0) {
      var lastRenderableIndex;
  
      childrenToRender.forEach(function(child, index) {
        if (child.type !== Syntax.XJSExpressionContainer ||
            child.expression.type !== Syntax.XJSEmptyExpression) {
          lastRenderableIndex = index;
        }
      });
  
      if (lastRenderableIndex !== undefined) {
        utils.append(', ', state);
      }
  
      childrenToRender.forEach(function(child, index) {
        utils.catchup(child.range[0], state, trimLeft);
  
        var isLast = index >= lastRenderableIndex;
  
        if (child.type === Syntax.Literal) {
          renderXJSLiteral(child, isLast, state);
        } else if (child.type === Syntax.XJSExpressionContainer) {
          renderXJSExpressionContainer(traverse, child, isLast, path, state);
        } else {
          traverse(child, path, state);
          if (!isLast) {
            utils.append(', ', state);
          }
        }
  
        utils.catchup(child.range[1], state, trimLeft);
      });
    }
  
    if (openingElement.selfClosing) {
      // everything up to />
      utils.catchup(openingElement.range[1] - 2, state, trimLeft);
      utils.move(openingElement.range[1], state);
    } else {
      // everything up to </ sdflksjfd>
      utils.catchup(object.closingElement.range[0], state, trimLeft);
      utils.move(object.closingElement.range[1], state);
    }
  
    utils.append(')', state);
    return false;
  }
  
  visitReactTag.test = function(object, path, state) {
    return object.type === Syntax.XJSElement;
  };
  
  exports.visitorList = [
    visitReactTag
  ];
  