snapdragon
Version:
Easy-to-use plugin system for creating powerful, fast and versatile parsers and compilers, with built-in source-map support.
273 lines (241 loc) • 6.15 kB
JavaScript
'use strict';
var use = require('use');
var util = require('snapdragon-util');
var Emitter = require('component-emitter');
var define = require('define-property');
var extend = require('extend-shallow');
var error = require('./error');
/**
* Create a new `Compiler` with the given `options`.
*
* ```js
* var Snapdragon = require('snapdragon');
* var Compiler = Snapdragon.Compiler;
* var compiler = new Compiler();
* ```
* @param {Object} `options`
* @param {Object} `state` Optionally pass a "state" object to use inside visitor functions.
* @api public
*/
function Compiler(options, state) {
this.options = extend({source: 'string'}, options);
this.emitter = new Emitter();
this.on = this.emitter.on.bind(this.emitter);
this.isCompiler = true;
this.state = state || {};
this.state.inside = this.state.inside || {};
this.compilers = {};
this.output = '';
this.indent = '';
this.set('eos', function(node) {
return this.emit(node.val, node);
});
this.set('bos', function(node) {
return this.emit(node.val, node);
});
use(this);
}
/**
* Prototype methods
*/
Compiler.prototype = {
/**
* Throw a formatted error message with details including the cursor position.
*
* ```js
* compiler.set('foo', function(node) {
* if (node.val !== 'foo') {
* throw this.error('expected node.val to be "foo"', node);
* }
* });
* ```
* @name .error
* @param {String} `msg` Message to use in the Error.
* @param {Object} `node`
* @return {undefined}
* @api public
*/
error: function(/*msg, node*/) {
return error.apply(this, arguments);
},
/**
* Concat the given string to `compiler.output`.
*
* ```js
* compiler.set('foo', function(node) {
* this.emit(node.val, node);
* });
* ```
* @name .emit
* @param {String} `string`
* @param {Object} `node` Optionally pass the node to use for position if source maps are enabled.
* @return {String} returns the string
* @api public
*/
emit: function(val, node) {
this.output += val;
return val;
},
/**
* Emit an empty string to effectively "skip" the string for the given `node`,
* but still emit the position and node type.
*
* ```js
* // example: do nothing for beginning-of-string
* snapdragon.compiler.set('bos', compiler.noop);
* ```
* @name .noop
* @param {Object} node
* @api public
*/
noop: function(node) {
this.emit('', node);
},
/**
* Define a non-enumberable property on the `Compiler` instance. This is useful
* in plugins, for exposing methods inside handlers.
*
* ```js
* compiler.define('customMethod', function() {
* // do stuff
* });
* ```
* @name .define
* @param {String} `key` propery name
* @param {any} `val` property value
* @return {Object} Returns the Compiler instance for chaining.
* @api public
*/
define: function(key, val) {
define(this, key, val);
return this;
},
/**
* Add a compiler `fn` for the given `type`. Compilers are called
* when the `.compile` method encounters a node of the given type to
* generate the output string.
*
* ```js
* compiler
* .set('comma', function(node) {
* this.emit(',');
* })
* .set('dot', function(node) {
* this.emit('.');
* })
* .set('slash', function(node) {
* this.emit('/');
* });
* ```
* @name .set
* @param {String} `type`
* @param {Function} `fn`
* @api public
*/
set: function(type, fn) {
this.compilers[type] = fn;
return this;
},
/**
* Get the compiler of the given `type`.
*
* ```js
* var fn = compiler.get('slash');
* ```
* @name .get
* @param {String} `type`
* @api public
*/
get: function(type) {
return this.compilers[type];
},
/**
* Visit `node` using the registered compiler function associated with the
* `node.type`.
*
* ```js
* compiler
* .set('i', function(node) {
* this.visit(node);
* })
* ```
* @name .visit
* @param {Object} `node`
* @return {Object} returns the node
* @api public
*/
visit: function(node) {
if (util.isOpen(node)) {
util.addType(this.state, node);
}
this.emitter.emit('node', node);
var fn = this.compilers[node.type] || this.compilers.unknown;
if (typeof fn !== 'function') {
throw this.error('compiler "' + node.type + '" is not registered', node);
}
var val = fn.call(this, node) || node;
if (util.isNode(val)) {
node = val;
}
if (util.isClose(node)) {
util.removeType(this.state, node);
}
return node;
},
/**
* Iterate over `node.nodes`, calling [visit](#visit) on each node.
*
* ```js
* compiler
* .set('i', function(node) {
* utils.mapVisit(node);
* })
* ```
* @name .mapVisit
* @param {Object} `node`
* @return {Object} returns the node
* @api public
*/
mapVisit: function(parent) {
var nodes = parent.nodes || parent.children;
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (!node.parent) node.parent = parent;
nodes[i] = this.visit(node) || node;
}
},
/**
* Compile the given `AST` and return a string. Iterates over `ast.nodes`
* with [mapVisit](#mapVisit).
*
* ```js
* var ast = parser.parse('foo');
* var str = compiler.compile(ast);
* ```
* @name .compile
* @param {Object} `ast`
* @param {Object} `options` Compiler options
* @return {Object} returns the node
* @api public
*/
compile: function(ast, options) {
var opts = extend({}, this.options, options);
this.ast = ast;
this.output = '';
// source map support
if (opts.sourcemap) {
var sourcemaps = require('./source-maps');
sourcemaps(this);
this.mapVisit(this.ast);
this.applySourceMaps();
this.map = opts.sourcemap === 'generator' ? this.map : this.map.toJSON();
} else {
this.mapVisit(this.ast);
}
return this;
}
};
/**
* Expose `Compiler`
*/
module.exports = Compiler;