/** * Copyright 2013 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 catchup = require('jstransform/src/utils').catchup; var catchupWhiteSpace = require('jstransform/src/utils').catchupWhiteSpace; var append = require('jstransform/src/utils').append; var move = require('jstransform/src/utils').move; var getDocblock = require('jstransform/src/utils').getDocblock; var FALLBACK_TAGS = require('./xjs').knownTags; var renderXJSExpressionContainer = require('./xjs').renderXJSExpressionContainer; var renderXJSLiteral = require('./xjs').renderXJSLiteral; var quoteAttrName = require('./xjs').quoteAttrName; /** * Customized desugar processor. * * Currently: (Somewhat tailored to React) * => X(null, null) * => X({prop: '1'}, null) * => X({prop:'2'}, Y(null, null)) * => 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) { if (attr.value.type !== Syntax.Literal) { throw new Error("cx only accepts a string literal"); } else { var classNames = attr.value.value.split(/\s+/g); return 'cx(' + classNames.map(JSON.stringify).join(',') + ')'; } } }; function visitReactTag(traverse, object, path, state) { var jsxObjIdent = getDocblock(state).jsx; catchup(object.openingElement.range[0], state); if (object.name.namespace) { throw new Error( 'Namespace tags are not supported. ReactJSX is not XML.'); } var isFallbackTag = FALLBACK_TAGS[object.name.name]; append( (isFallbackTag ? jsxObjIdent + '.' : '') + (object.name.name) + '(', state ); move(object.name.range[1], state); var childrenToRender = object.children.filter(function(child) { return !(child.type === Syntax.Literal && !child.value.match(/\S/)); }); // if we don't have any attributes, pass in null if (object.attributes.length === 0) { append('null', state); } // write attributes object.attributes.forEach(function(attr, index) { catchup(attr.range[0], state); if (attr.name.namespace) { throw new Error( 'Namespace attributes are not supported. ReactJSX is not XML.'); } var name = attr.name.name; var isFirst = index === 0; var isLast = index === object.attributes.length - 1; if (isFirst) { append('{', state); } append(quoteAttrName(name), state); append(':', state); if (!attr.value) { state.g.buffer += 'true'; state.g.position = attr.name.range[1]; if (!isLast) { append(',', state); } } else { move(attr.name.range[1], state); // Use catchupWhiteSpace to skip over the '=' in the attribute catchupWhiteSpace(attr.value.range[0], state); if (JSX_ATTRIBUTE_TRANSFORMS[attr.name.name]) { append(JSX_ATTRIBUTE_TRANSFORMS[attr.name.name](attr), state); move(attr.value.range[1], state); if (!isLast) { append(',', state); } } else if (attr.value.type === Syntax.Literal) { renderXJSLiteral(attr.value, isLast, state); } else { renderXJSExpressionContainer(traverse, attr.value, isLast, path, state); } } if (isLast) { append('}', state); } catchup(attr.range[1], state); }); if (!object.selfClosing) { catchup(object.openingElement.range[1] - 1, state); move(object.openingElement.range[1], state); } // filter out whitespace if (childrenToRender.length > 0) { append(', ', state); object.children.forEach(function(child) { if (child.type === Syntax.Literal && !child.value.match(/\S/)) { return; } catchup(child.range[0], state); var isLast = child === childrenToRender[childrenToRender.length - 1]; 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) { append(',', state); state.g.buffer = state.g.buffer.replace(/(\s*),$/, ',$1'); } } catchup(child.range[1], state); }); } if (object.selfClosing) { // everything up to /> catchup(object.openingElement.range[1] - 2, state); move(object.openingElement.range[1], state); } else { // everything up to catchup(object.closingElement.range[0], state); move(object.closingElement.range[1], state); } append(')', state); return false; } visitReactTag.test = function(object, path, state) { // only run react when react @jsx namespace is specified in docblock var jsx = getDocblock(state).jsx; return object.type === Syntax.XJSElement && jsx && jsx.length; }; exports.visitReactTag = visitReactTag;