Home Reference Source Repository

src/render.js

import {SourceMapGenerator} from 'source-map/lib/source-map-generator'
import * as Ast from './ast'
import {ArrowFunctionExpression, BlockStatement, Identifier, ImportDefaultSpecifier,
	ImportNamespaceSpecifier, Literal} from './ast'
import {Pos, StartColumn, StartLine} from './Loc'

/**
Creates JavaScript source code from a {@link Node}.
@param {Node} ast
@param {object} options
@param {boolean} options.ugly
	If true, will not output any whitespace.
*/
export default function render(ast /* Node */, options) {
	// TODO:ES6 Optional args
	if (options === undefined)
		options = {}

	setUp(options)
	e(ast)
	const res = strOut
	tearDown()
	return res
}

/**
Same as {@link render}, but with a source map as part of the output.
@param {Node} ast
@param {string} inFilePath Name of input file.
@param {string} outFilePath Name of output file.
@param {object} options Same options as for {@link render}.
@return {code: string, sourceMap: string}
*/
export function renderWithSourceMap(ast, inFilePath, outFilePath, options) {
	// TODO:ES6 Optional args
	if (options === undefined)
		options = {}

	setUp(options, inFilePath, outFilePath)
	e(ast)
	const res = {code: strOut, sourceMap: sourceMap.toJSON()}
	tearDown()
	return res
}

// Must init these all before rendering.
let strOut,
	indentAmount, indentStr,
	usingSourceMaps, curAst, inFilePath, sourceMap, outLine, outColumn, lastMappedAst,
	// options
	ugly

const
	setUp = (options, inPath, outPath) => {
		ugly = Boolean(options.ugly)

		indentAmount = 0
		_setIndent()
		strOut = ''
		usingSourceMaps = inPath !== undefined
		if (usingSourceMaps) {
			inFilePath = inPath
			sourceMap = new SourceMapGenerator({file: outPath})
			outLine = StartLine
			outColumn = StartColumn
			lastMappedAst = null
		}
	},

	tearDown = () => {
		strOut = ''
		inFilePath = sourceMap = curAst = lastMappedAst = undefined
	}

const
	// Renders a single expression.
	e = ast => {
		if (usingSourceMaps)
			curAst = ast
		ast.render()
	},

	// Outputs a string.
	// str may not contain newlines.
	o = str => {
		strOut = strOut + str
		if (usingSourceMaps)
			_mapStr(str)
	},

	interleave = (asts, str) => {
		if (!isEmpty(asts)) {
			const maxI = asts.length - 1
			for (let i = 0; i < maxI; i = i + 1) {
				e(asts[i])
				o(str)
			}
			e(asts[maxI])
		}
	},

	paren = asts => {
		o('(')
		interleave(asts, ',')
		o(')')
	},

	block = (blockLines, lineSeparator) => {
		if (isEmpty(blockLines))
			o('{}')
		else {
			o('{')
			indent()
			nl()
			lines(blockLines, lineSeparator)
			unindent()
			nl()
			o('}')
		}
	},

	lines = (lines, lineSeparator) => {
		if (lines.length > 0) {
			const maxI = lines.length - 1
			for (let i = 0; i < maxI; i = i + 1) {
				e(lines[i])
				o(lineSeparator)
				nl()
			}
			e(lines[maxI])
		}
	},

	indentStrs = [''],
	_setIndent = () => {
		indentStr = indentStrs[indentAmount]
		while (indentStr === undefined) {
			indentStrs.push(last(indentStrs) + '\t')
			indentStr = indentStrs[indentAmount]
		}
	},
	indent = () => {
		if (!ugly) {
			indentAmount = indentAmount + 1
			_setIndent()
		}
	},
	unindent = () => {
		if (!ugly) {
			indentAmount = indentAmount - 1
			_setIndent()
		}
	},

	nl = () => {
		if (!ugly) {
			strOut = strOut + '\n' + indentStr
			if (usingSourceMaps)
				_mapNewLine()
		}
	},

	// Private

	_mapStr = str => {
		if (curAst !== lastMappedAst && curAst.loc !== undefined) {
			sourceMap.addMapping({
				source: inFilePath,
				original: curAst.loc.start,
				generated: new Pos(outLine, outColumn)
			})
			lastMappedAst = curAst
		}
		outColumn = outColumn + str.length
	},

	_mapNewLine = () => {
		outLine = outLine + 1
		outColumn = StartColumn + indentAmount
		// Mappings end at end of line. Must begin anew.
		lastMappedAst = null
	}

function fun() {
	o(this.generator ? 'function*' : 'function')
	if (this.id !== null) {
		o(' ')
		e(this.id)
	}
	paren(this.params)
	e(this.body)
}

function arr() {
	if (isEmpty(this.elements))
		o('[]')
	else {
		o('[')
		interleave(this.elements, ',')
		o(']')
	}
}

function rClass() {
	o('class ')
	if (this.id !== null)
		e(this.id)
	if (this.superClass !== null) {
		o(' extends ')
		e(this.superClass)
	}
	e(this.body)
}

const
	forInOf = (_, kind) => {
		o('for(')
		e(_.left)
		o(kind)
		e(_.right)
		o(')')
		e(_.body)
	}

const
	implementMany = (holder, methodName, nameToImpl) => {
		Object.keys(nameToImpl).forEach(name => {
			holder[name].prototype[methodName] = nameToImpl[name]
		})
	},

	isEmpty = arr =>
		arr.length === 0,

	last = arr =>
		arr[arr.length - 1]

implementMany(Ast, 'render', {
	Program() {
		lines(this.body, ';')
	},

	Identifier() {
		o(this.name)
	},

	// Statements
	EmptyStatement() { },
	BlockStatement() {
		block(this.body, ';')
	},
	ExpressionStatement() {
		e(this.expression)
	},
	IfStatement() {
		o('if(')
		e(this.test)
		o(')')
		e(this.consequent)
		if (this.alternate !== null) {
			if (!(this.consequent instanceof BlockStatement))
				o(';')
			o(' else ')
			e(this.alternate)
		}
	},
	LabeledStatement() {
		e(this.label)
		o(':')
		e(this.body)
	},
	BreakStatement() {
		o('break')
		if (this.label !== null) {
			o(' ')
			e(this.label)
		}
	},
	ContinueStatement() {
		o('continue')
		if (this.label !== null) {
			o(' ')
			e(this.label)
		}
	},
	SwitchCase() {
		if (this.test !== null) {
			o('case ')
			e(this.test)
		} else
			o('default')
		o(':')
		switch (this.consequent.length) {
			case 0:
				break
			case 1:
				e(this.consequent[0])
				break
			default:
				indent()
				nl()
				lines(this.consequent, ';')
				unindent()
		}
	},
	SwitchStatement() {
		o('switch(')
		e(this.discriminant)
		o(')')
		block(this.cases, '')
	},
	ReturnStatement() {
		if (this.argument !== null) {
			o('return ')
			e(this.argument)
		} else
			o('return')
	},
	ThrowStatement() {
		o('throw ')
		e(this.argument)
	},
	CatchClause() {
		o('catch(')
		e(this.param)
		o(')')
		e(this.body)
	},
	TryStatement() {
		o('try ')
		e(this.block)
		if (this.handler !== null)
			e(this.handler)
		if (this.finalizer !== null) {
			o('finally')
			e(this.finalizer)
		}
	},
	WhileStatement() {
		o('while(')
		e(this.test)
		o(')')
		e(this.body)
	},
	DoWhileStatement() {
		o('do ')
		e(this.body)
		if (!(this.body instanceof BlockStatement))
			o(';')
		o(' while(')
		e(this.test)
		o(')')
	},
	ForStatement() {
		o('for(')
		if (this.init !== null)
			e(this.init)
		o(';')
		if (this.test !== null)
			e(this.test)
		o(';')
		if (this.update !== null)
			e(this.update)
		o(')')
		e(this.body)
	},
	ForInStatement() { forInOf(this, ' in ') },
	ForOfStatement() { forInOf(this, ' of ') },
	DebuggerStatement() {
		o('debugger')
	},

	// Declarations
	FunctionDeclaration: fun,
	VariableDeclarator() {
		e(this.id)
		if (this.init !== null) {
			o('=')
			e(this.init)
		}
	},
	VariableDeclaration() {
		o(this.kind)
		o(' ')
		interleave(this.declarations, ',')
	},

	// Expressions
	ThisExpression() {
		o('this')
	},
	ArrayExpression: arr,
	ObjectExpression() {
		if (isEmpty(this.properties))
			o('{}')
		else
			block(this.properties, ',')
	},
	Property() {
		if (this.kind === 'init') {
			e(this.key)
			o(':')
			e(this.value)
		} else {
			o(this.kind)
			o(' ')
			e(this.key)
			paren(this.value.params)
			e(this.value.body)
		}
	},
	FunctionExpression: fun,
	ArrowFunctionExpression() {
		if (this.params.length === 1 && this.params[0] instanceof Identifier)
			e(this.params[0])
		else
			paren(this.params)
		o('=>')
		e(this.body)
	},
	SequenceExpression() {
		interleave(this.expressions, ',')
	},
	UnaryExpression() {
		o(this.operator)
		o(' ')
		e(this.argument)
	},
	BinaryExpression() {
		o('(')
		e(this.left)
		o(this.operator === 'instanceof' ? ' instanceof ' : this.operator)
		e(this.right)
		o(')')
	},
	AssignmentExpression() {
		e(this.left)
		o(this.operator)
		e(this.right)
	},
	UpdateExpression() {
		if (this.prefix) {
			o(this.operator)
			e(this.argument)
		} else {
			e(this.argument)
			o(this.operator)
		}
	},
	LogicalExpression() {
		o('(')
		e(this.left)
		o(this.operator)
		e(this.right)
		o(')')
	},
	ConditionalExpression() {
		o('(')
		e(this.test)
		o('?')
		e(this.consequent)
		o(':')
		e(this.alternate)
		o(')')
	},
	NewExpression() {
		o('new (')
		e(this.callee)
		o(')')
		paren(this.arguments)
	},
	CallExpression() {
		if (this.callee instanceof ArrowFunctionExpression) {
			o('(')
			e(this.callee)
			o(')')
		} else
			e(this.callee)
		paren(this.arguments)
	},
	SpreadElement() {
		o('...')
		e(this.argument)
	},
	MemberExpression() {
		e(this.object)
		if (this.computed) {
			o('[')
			e(this.property)
			o(']')
		} else {
			if (this.object instanceof Literal &&
				typeof this.object.value === 'number' &&
				this.object.value === (this.object.value | 0))
				o('..')
			else
				o('.')
			e(this.property)
		}
	},
	YieldExpression() {
		if (this.argument === null)
			o('(yield)')
		else {
			o(this.delegate ? '(yield* ' : '(yield ')
			if (this.argument !== null)
				e(this.argument)
			o(')')
		}
	},
	Literal() {
		if (typeof this.value === 'string') {
			o('"')
			o(escapeStringForLiteral(this.value))
			o('"')
		}
		else
			o(this.value === null ? 'null' : this.value.toString())
	},

	// Templates
	TemplateElement() {
		o(this.value.raw)
	},
	TemplateLiteral() {
		o('`')
		e(this.quasis[0])
		for (let i = 0; i < this.expressions.length; i = i + 1)	 {
			o('${')
			e(this.expressions[i])
			o('}')
			e(this.quasis[i + 1])
		}
		o('`')
	},
	TaggedTemplateExpression() {
		e(this.tag)
		e(this.quasi)
	},

	// Patterns
	AssignmentProperty() {
		e(this.key)
		if (this.key !== this.value) {
			o(':')
			e(this.value)
		}
	},
	ObjectPattern() {
		o('{')
		interleave(this.properties, ',')
		o('}')
	},
	ArrayPattern: arr,
	RestElement() {
		o('...')
		e(this.argument)
	},

	MethodDefinition() {
		if (this.static)
			o('static ')

		const fun = this.value
		const params = fun.params
		const body = fun.body

		const rKey = () => {
			if (this.computed) {
				o('[')
				e(this.key)
				o(']')
			} else
				e(this.key)
		}

		if (fun.generator)
			o('*')
		switch (this.kind) {
			case 'constructor':
				o('constructor')
				break
			case 'method':
				rKey()
				break
			case 'get': case 'set':
				o(this.kind)
				o(' ')
				rKey()
				break
			default:
				throw new Error(this.kind)
		}

		paren(params)
		e(body)
	},

	ClassBody() {
		block(this.body, '')
	},

	ClassDeclaration: rClass,
	ClassExpression: rClass,

	ImportDeclaration() {
		o('import ')

		let def, namespace
		let specifiers = []
		for (const s of this.specifiers) {
			if (s instanceof ImportDefaultSpecifier)
				if (def === undefined)
					def = s
				else
					throw new Error('Multiple default imports')
			else if (s instanceof ImportNamespaceSpecifier)
				if (namespace === undefined)
					namespace = s
				else
					throw new Error('Multiple namespace imports')
			else
				// ImportSpecifier
				specifiers.push(s)
		}

		let needComma = false
		if (def !== undefined) {
			e(def)
			needComma = true
		}
		if (namespace !== undefined) {
			if (needComma)
				o(',')
			e(namespace)
			needComma = true
		}
		if (!isEmpty(specifiers)) {
			if (needComma)
				o(',')
			o('{')
			interleave(specifiers, ',')
			o('}')
		}

		o(' from ')
		e(this.source)
	},
	ImportSpecifier() {
		if (this.imported === this.local)
			e(this.local)
		else {
			e(this.imported)
			o(' as ')
			e(this.local)
		}
	},
	ImportDefaultSpecifier() {
		e(this.local)
	},
	ImportNamespaceSpecifier() {
		o('* as ')
		e(this.local)
	},

	ExportSpecifier() {
		e(this.local)
		if (this.exported !== this.local) {
			o(' as ')
			e(this.exported)
		}
	},
	ExportNamedDeclaration() {
		o('export ')
		if (this.declaration !== null)
			e(this.declaration)
		else {
			o('{')
			interleave(this.specifiers, ',')
			o('}')
			if (this.source !== null) {
				o(' from ')
				e(this.source)
			}
		}
	},
	ExportDefaultDeclaration() {
		o('export default ')
		e(this.declaration)
	},
	ExportAllDeclaration() {
		o('export * from ')
		e(this.source)
	}
})

const
	escapeStringForLiteral = str =>
		str.replace(/[\\"\n\t\b\f\v\r\u2028\u2029]/g, ch => literalEscapes[ch]),
	literalEscapes = {
		'\\': '\\\\',
		'"': '\\"',
		'\n': '\\n',
		'\t': '\\t',
		'\b': '\\b',
		'\f': '\\f',
		'\v': '\\v',
		'\r': '\\r',
		'\u2028': '\\u2028',
		'\u2029': '\\u2029'
	}