UNPKG

jsx-transform

Version:

JSX transpiler. Desugar JSX into JavaScript. A standard and configurable implementation of JSX decoupled from React.

399 lines (334 loc) 10.6 kB
var utils = require('jstransform/src/utils'); var Syntax = require('jstransform').Syntax; module.exports = visitNode; /** * Visit tag node and desugar JSX. * * @see {@link https://github.com/facebook/jstransform} * @param {Function} traverse * @param {Object} object * @param {String} path * @param {Object} state * @returns {Boolean} * @private */ function visitNode(traverse, object, path, state) { var options = state.g.opts; var factory = (options.factory); var arrayChildren = options.arrayChildren var openingEl = object.openingElement; var closingEl = object.closingElement; var nameObj = openingEl.name; var attributes = openingEl.attributes; var spreadFn = options.spreadFn; var unknownTagPattern = options.unknownTagPattern; if (!options.renameAttrs) { options.renameAttrs = {}; } utils.catchup(openingEl.range[0], state, trimLeft); var tagName = nameObj.name; var isJSXIdentifier = nameObj.type === Syntax.JSXIdentifier; var knownTag = tagName[0] !== tagName[0].toUpperCase() && isJSXIdentifier; var hasAtLeastOneSpreadAttribute = attributes.some(function (attr) { return attr.type === Syntax.JSXSpreadAttribute; }); var secondArg = false; if (knownTag) { utils.append(factory + "('", state); // DOM('div', ...) } else if (options.passUnknownTagsToFactory) { if (options.unknownTagsAsString) { utils.append(factory + "('", state); } else { utils.append(factory + '(', state); } } utils.move(nameObj.range[0], state); if (knownTag) { // DOM('div', ...) utils.catchup(nameObj.range[1], state); utils.append("'", state); secondArg = true } else if (options.passUnknownTagsToFactory) { // DOM(Component, ...) utils.catchup(nameObj.range[1], state); if (options.unknownTagsAsString) { utils.append("'", state); } secondArg = true } else { // Component(...) tagName = unknownTagPattern.replace('{tag}', nameObj.name); utils.append(tagName, state); utils.move( nameObj.range[1] + (tagName.length - nameObj.name.length), state ); utils.append('(', state); } if (hasAtLeastOneSpreadAttribute) { if (options.passUnknownTagsToFactory || knownTag) { utils.append(', ' + spreadFn + '({', state); } else { utils.append(spreadFn + '({', state); } } else if (attributes.length) { if (secondArg) { utils.append(', ', state); } utils.append('{', state); } var previousWasSpread = false; attributes.forEach(function(attr, index) { var isLast = (index === (attributes.length - 1)); if (attr.type === Syntax.JSXSpreadAttribute) { // close the previous or initial object if (!previousWasSpread) { utils.append('}, ', state); } // Move to the expression start, ignoring everything except parenthesis // and whitespace. utils.catchup(attr.range[0], state, stripNonParen); // Plus 1 to skip `{`. utils.move(attr.range[0] + 1, state); utils.catchup(attr.argument.range[0], state, stripNonParen); 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, stripNonParen); 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 = attributes[index + 1].type === Syntax.JSXSpreadAttribute; } var name if (attr.name.namespace) { name = attr.name.namespace.name + ':' + attr.name.name.name } else { name = attr.name.name; } utils.catchup(attr.range[0], state, trimLeft); if (previousWasSpread) { utils.append('{', state); } utils.append(quoteJSObjKey(name) + ': ', state); if (attr.value) { utils.move(attr.name.range[1], state); utils.catchupNewlines(attr.value.range[0], state); if (attr.value.type === Syntax.Literal) { renderJSXLiteral(attr.value, isLast, state); } else { renderJSXExpressionContainer(traverse, attr.value, isLast, path, state); } } else { state.g.buffer += 'true'; state.g.position = attr.name.range[1]; if (!isLast) { utils.append(', ', state); } } utils.catchup(attr.range[1], state, trimLeft); previousWasSpread = false; }); if (!openingEl.selfClosing) { utils.catchup(openingEl.range[1] - 1, state, trimLeft); utils.move(openingEl.range[1], state); } if (attributes.length && !previousWasSpread) { utils.append('}', state); } if (hasAtLeastOneSpreadAttribute) { utils.append(')', state); } // filter out whitespace var children = object.children.filter(function(child) { return !(child.type === Syntax.Literal && typeof child.value === 'string' && child.value.match(/^[ \t]*[\r\n][ \t\r\n]*$/)); }); if (children.length) { if (!attributes.length) { if (secondArg) { utils.append(', ', state); } utils.append('null', state); } var lastRenderableIndex; children.forEach(function(child, index) { if (child.type !== Syntax.JSXExpressionContainer || child.expression.type !== Syntax.JSXEmptyExpression) { lastRenderableIndex = index; } }); if (lastRenderableIndex !== undefined) { utils.append(', ', state); } if (arrayChildren && children.length) { utils.append('[', state); } children.forEach(function(child, index) { utils.catchup(child.range[0], state, trimLeft); var isFirst = index === 0; var isLast = index >= lastRenderableIndex; if (child.type === Syntax.Literal) { renderJSXLiteral(child, isLast, state); } else if (child.type === Syntax.JSXExpressionContainer) { renderJSXExpressionContainer(traverse, child, isLast, path, state); } else { traverse(child, path, state); if (!isLast) { utils.append(',', state); } } utils.catchup(child.range[1], state, trimLeft); }); } if (openingEl.selfClosing) { // everything up to /> utils.catchup(openingEl.range[1] - 2, state, trimLeft); utils.move(openingEl.range[1], state); } else { // everything up to </close> utils.catchup(closingEl.range[0], state, trimLeft); utils.move(closingEl.range[1], state); } if (arrayChildren && children.length) { utils.append(']', state); } utils.append(')', state); return false; } /** * Returns true if node is JSX tag. * * @param {Object} object * @param {String} path * @param {Object} state * @returns {Boolean} * @private */ visitNode.test = function(object, path, state) { return object.type === Syntax.JSXElement; }; /** * Taken from {@link https://github.com/facebook/react/blob/0.10-stable/vendor/fbtransform/transforms/xjs.js} * * @param {Object} object * @param {Boolean} isLast * @param {Object} state * @param {Number} start * @param {Number} end * @private */ function renderJSXLiteral(object, isLast, state, start, end) { var lines = object.value.split(/\r\n|\n|\r/); if (start) { utils.append(start, state); } var lastNonEmptyLine = 0; lines.forEach(function (line, index) { if (line.match(/[^ \t]/)) { lastNonEmptyLine = index; } }); lines.forEach(function (line, index) { var isFirstLine = index === 0; var isLastLine = index === lines.length - 1; var isLastNonEmptyLine = index === lastNonEmptyLine; // replace rendered whitespace tabs with spaces var trimmedLine = line.replace(/\t/g, ' '); // trim whitespace touching a newline if (!isFirstLine) { trimmedLine = trimmedLine.replace(/^[ ]+/, ''); } if (!isLastLine) { trimmedLine = trimmedLine.replace(/[ ]+$/, ''); } if (!isFirstLine) { utils.append(line.match(/^[ \t]*/)[0], state); } if (trimmedLine || isLastNonEmptyLine) { utils.append( JSON.stringify(trimmedLine) + (!isLastNonEmptyLine ? " + ' ' +" : ''), state); if (isLastNonEmptyLine) { if (end) { utils.append(end, state); } if (!isLast) { utils.append(', ', state); } } // only restore tail whitespace if line had literals if (trimmedLine && !isLastLine) { utils.append(line.match(/[ \t]*$/)[0], state); } } if (!isLastLine) { utils.append('\n', state); } }); utils.move(object.range[1], state); } /** * Taken from {@link https://github.com/facebook/react/blob/0.10-stable/vendor/fbtransform/transforms/xjs.js} * * @param {Function} traverse * @param {Object} object * @param {Boolean} isLast * @param {String} path * @param {Object} state * @returns {Boolean} * @private */ function renderJSXExpressionContainer(traverse, object, isLast, path, state) { // Plus 1 to skip `{`. utils.move(object.range[0] + 1, state); traverse(object.expression, path, state); if (!isLast && object.expression.type !== Syntax.JSXEmptyExpression) { // If we need to append a comma, make sure to do so after the expression. utils.catchup(object.expression.range[1], state, trimLeft); utils.append(', ', state); } // Minus 1 to skip `}`. utils.catchup(object.range[1] - 1, state, trimLeft); utils.move(object.range[1], state); return false; } /** * Quote invalid object literal keys. * * @param {String} name * @returns {String} * @private */ function quoteJSObjKey(name) { if (!/^[a-z_$][a-z\d_$]*$/i.test(name)) { return "'" + name + "'"; } return name; } /** * Trim whitespace left of `val`. * * @param {String} val * @returns {String} * @private */ function trimLeft(val) { return val.replace(/^ +/, ''); } /** * Removes all non-parenthesis characters */ var reNonParen = /([^\(\)])/g; function stripNonParen(value) { return value.replace(reNonParen, ''); }