mirror of
https://github.com/facebook/react.git
synced 2026-02-27 03:07:57 +00:00
289 lines
8.7 KiB
JavaScript
289 lines
8.7 KiB
JavaScript
/**
|
|
* 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 visitReactTag(traverse, object, path, state) {
|
|
var jsxObjIdent = utils.getDocblock(state).jsx;
|
|
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) {
|
|
throw new Error('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.
|
|
throw new Error(
|
|
'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) {
|
|
throw new Error(
|
|
'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) {
|
|
// only run react when react @jsx namespace is specified in docblock
|
|
var jsx = utils.getDocblock(state).jsx;
|
|
return object.type === Syntax.XJSElement && jsx && jsx.length;
|
|
};
|
|
|
|
exports.visitorList = [
|
|
visitReactTag
|
|
];
|