/** * 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*/ /** * State represents the given parser state. It has a local and global parts. * Global contains parser position, source, etc. Local contains scope based * properties, like current class name. State should contain all the info * required for transformation. It's the only mandatory object that is being * passed to every function in transform chain. * * @param {String} source * @param {Object} transformOptions * @return {Object} */ function createState(source, transformOptions) { return { /** * Name of the super class variable * @type {String} */ superVar: '', /** * Name of the enclosing class scope * @type {String} */ scopeName: '', /** * Global state (not affected by updateState) * @type {Object} */ g: { /** * A set of general options that transformations can consider while doing * a transformation: * * - minify * Specifies that transformation steps should do their best to minify * the output source when possible. This is useful for places where * minification optimizations are possible with higher-level context * info than what jsxmin can provide. * * For example, the ES6 class transform will minify munged private * variables if this flag is set. */ opts: transformOptions, /** * Current position in the source code * @type {Number} */ position: 0, /** * Buffer containing the result * @type {String} */ buffer: '', /** * Indentation offset (only negative offset is supported now) * @type {Number} */ indentBy: 0, /** * Source that is being transformed * @type {String} */ source: source, /** * Cached parsed docblock (see getDocblock) * @type {object} */ docblock: null, /** * Whether the thing was used * @type {Boolean} */ tagNamespaceUsed: false, /** * If using bolt xjs transformation * @type {Boolean} */ isBolt: undefined, /** * Whether to record source map (expensive) or not * @type {SourceMapGenerator|null} */ sourceMap: null, /** * Filename of the file being processed. Will be returned as a source * attribute in the source map */ sourceMapFilename: 'source.js', /** * Only when source map is used: last line in the source for which * source map was generated * @type {Number} */ sourceLine: 1, /** * Only when source map is used: last line in the buffer for which * source map was generated * @type {Number} */ bufferLine: 1, /** * The top-level Program AST for the original file. */ originalProgramAST: null, sourceColumn: 0, bufferColumn: 0 } }; } /** * Updates a copy of a given state with "update" and returns an updated state. * * @param {Object} state * @param {Object} update * @return {Object} */ function updateState(state, update) { return { g: state.g, superVar: update.superVar || state.superVar, scopeName: update.scopeName || state.scopeName }; } /** * Given a state fill the resulting buffer from the original source up to * the end * @param {Number} end * @param {Object} state * @param {Function?} contentTransformer Optional callback to transform newly * added content. */ function catchup(end, state, contentTransformer) { if (end < state.g.position) { // cannot move backwards return; } var source = state.g.source.substring(state.g.position, end); var transformed = updateIndent(source, state); if (state.g.sourceMap && transformed) { // record where we are state.g.sourceMap.addMapping({ generated: { line: state.g.bufferLine, column: state.g.bufferColumn }, original: { line: state.g.sourceLine, column: state.g.sourceColumn }, source: state.g.sourceMapFilename }); // record line breaks in transformed source var sourceLines = source.split('\n'); var transformedLines = transformed.split('\n'); // Add line break mappings between last known mapping and the end of the // added piece. So for the code piece // (foo, bar); // > var x = 2; // > var b = 3; // var c = // only add lines marked with ">": 2, 3. for (var i = 1; i < sourceLines.length - 1; i++) { state.g.sourceMap.addMapping({ generated: { line: state.g.bufferLine, column: 0 }, original: { line: state.g.sourceLine, column: 0 }, source: state.g.sourceMapFilename }); state.g.sourceLine++; state.g.bufferLine++; } // offset for the last piece if (sourceLines.length > 1) { state.g.sourceLine++; state.g.bufferLine++; state.g.sourceColumn = 0; state.g.bufferColumn = 0; } state.g.sourceColumn += sourceLines[sourceLines.length - 1].length; state.g.bufferColumn += transformedLines[transformedLines.length - 1].length; } state.g.buffer += contentTransformer ? contentTransformer(transformed) : transformed; state.g.position = end; } /** * Applies `catchup` but passing in a function that removes any non-whitespace * characters. */ var re = /(\S)/g; function stripNonWhite(value) { return value.replace(re, function() { return ''; }); } /** * Catches up as `catchup` but turns each non-white character into a space. */ function catchupWhiteSpace(end, state) { catchup(end, state, stripNonWhite); } /** * Same as catchup but does not touch the buffer * @param {Number} end * @param {Object} state */ function move(end, state) { // move the internal cursors if (state.g.sourceMap) { if (end < state.g.position) { state.g.position = 0; state.g.sourceLine = 1; state.g.sourceColumn = 0; } var source = state.g.source.substring(state.g.position, end); var sourceLines = source.split('\n'); if (sourceLines.length > 1) { state.g.sourceLine += sourceLines.length - 1; state.g.sourceColumn = 0; } state.g.sourceColumn += sourceLines[sourceLines.length - 1].length; } state.g.position = end; } /** * Appends a string of text to the buffer * @param {String} string * @param {Object} state */ function append(string, state) { if (state.g.sourceMap && string) { state.g.sourceMap.addMapping({ generated: { line: state.g.bufferLine, column: state.g.bufferColumn }, original: { line: state.g.sourceLine, column: state.g.sourceColumn }, source: state.g.sourceMapFilename }); var transformedLines = string.split('\n'); if (transformedLines.length > 1) { state.g.bufferLine += transformedLines.length - 1; state.g.bufferColumn = 0; } state.g.bufferColumn += transformedLines[transformedLines.length - 1].length; } state.g.buffer += string; } /** * Update indent using state.indentBy property. Indent is measured in * double spaces. Updates a single line only. * * @param {String} str * @param {Object} state * @return {String} */ function updateIndent(str, state) { for (var i = 0; i < -state.g.indentBy; i++) { str = str.replace(/(^|\n)( {2}|\t)/g, '$1'); } return str; } /** * Calculates indent from the beginning of the line until "start" or the first * character before start. * @example * " foo.bar()" * ^ * start * indent will be 2 * * @param {Number} start * @param {Object} state * @return {Number} */ function indentBefore(start, state) { var end = start; start = start - 1; while (start > 0 && state.g.source[start] != '\n') { if (!state.g.source[start].match(/[ \t]/)) { end = start; } start--; } return state.g.source.substring(start + 1, end); } function getDocblock(state) { if (!state.g.docblock) { var docblock = require('./docblock'); state.g.docblock = docblock.parseAsObject(docblock.extract(state.g.source)); } return state.g.docblock; } exports.catchup = catchup; exports.catchupWhiteSpace = catchupWhiteSpace; exports.append = append; exports.move = move; exports.updateIndent = updateIndent; exports.indentBefore = indentBefore; exports.updateState = updateState; exports.createState = createState; exports.getDocblock = getDocblock;