Skip to content

Instantly share code, notes, and snippets.

@eventualbuddha
Created September 3, 2015 16:50
Show Gist options
  • Save eventualbuddha/490105cc38619cedf139 to your computer and use it in GitHub Desktop.
Save eventualbuddha/490105cc38619cedf139 to your computer and use it in GitHub Desktop.
import { nodes } from 'coffee-script';
import { createReadStream } from 'fs';
import MagicString from 'magic-string';
class LineColumnMap {
constructor(source) {
this.offsets = this.constructor.getLineOffsets(source);
}
getOffsetForLineAndColumn(line, column) {
return this.offsets[line] + column;
}
getLineAndColumnForOffset(offset) {
for (let line = this.offsets.length; line >= 0; line--) {
let lineOffset = this.offsets[line];
if (offset >= lineOffset) {
return { line, column: offset - lineOffset };
}
}
return null;
}
static getLineOffsets(source) {
const offsets = [];
let offset = 0;
for (;;) {
offsets.push(offset);
let newline = source.indexOf('\n', offset);
if (newline < 0) break;
offset = newline + '\n'.length;
}
return offsets;
}
}
class Node {
constructor(node, source, lineColumnMap, patcher) {
this.node = node;
this.source = source;
this.lineColumnMap = lineColumnMap;
this.patcher = patcher;
}
overwrite(string) {
this.patcher.overwrite(this.start, this.end, string);
}
peek(offset) {
if (offset > 0) {
return this.source[this.end + offset - 1];
} else {
return this.source[this.stat + offset];
}
}
append(string) {
this.patcher.insert(this.end, string);
}
overwriteUntil(other, string) {
this.patcher.overwrite(this.end, other.start, string);
}
get indent() {
const source = this.source;
const startOfLine = this.lineColumnMap.getOffsetForLineAndColumn(this.line, 0);
let endOfIndent = startOfLine;
while (endOfIndent < source.length) {
if (source[endOfIndent] !== ' ' && source[endOfIndent] !== '\t') {
break;
}
endOfIndent++;
}
return source.slice(startOfLine, endOfIndent);
}
get raw() {
return this.source.slice(
this.start,
this.end
);
}
get(key) {
if (!Array.isArray(this.node) && this.node.children.indexOf(key) < 0) {
return null;
}
const child = this.node[key];
if (!child) {
return null;
}
return new Node(child, this.source, this.lineColumnMap, this.patcher);
}
get type() {
return this.node.constructor.name;
}
get start() {
let loc = this.node.locationData;
return this.lineColumnMap.getOffsetForLineAndColumn(loc.first_line, loc.first_column);
}
get end() {
let loc = this.node.locationData;
return this.lineColumnMap.getOffsetForLineAndColumn(loc.last_line, loc.last_column) + 1;
}
get line() {
return this.node.locationData.first_line;
}
get column() {
return this.node.locationData.first_column;
}
}
function printAST(source) {
let indent = 0;
const ast = nodes(source);
const lineColumnMap = new LineColumnMap(source);
traverse(
ast,
{
'*': {
enter(node) {
let raw = node.raw;
if (raw.length > 20) { raw = raw.slice(0, 20) + '…'; }
console.log(`${' '.repeat(indent++)}${node.type} ${node.line + 1}:${node.column + 1} ${JSON.stringify(raw)}`);
},
exit(node) {
indent--;
}
}
},
{
prepareContext(node) {
return new Node(node, source, lineColumnMap);
}
}
);
}
function readInput(input, callback) {
let error;
let data = '';
if (input.setEncoding) {
input.setEncoding('utf8');
}
input.on('data', function(chunk) {
data += chunk;
});
input.on('end', function() {
callback(error, error ? null : data);
});
}
function edit(source, traversers) {
const patcher = new MagicString(source);
const ast = nodes(source);
const lineColumnMap = new LineColumnMap(source);
traverse(ast, traversers, {
prepareContext(node) {
return new Node(node, source, lineColumnMap, patcher);
}
});
return patcher.toString();
}
function traverse(node, visitors, options={}) {
let type = node.constructor.name;
const generalEnters = [];
const specificEnters = [];
const generalExits = [];
const specificExits = [];
visitors.forEach(visitor => {
let callbacks = visitor['*'];
if (callbacks) {
if (callbacks.enter) {
generalEnters.push(callbacks.enter);
}
if (callbacks.exit) {
generalExits.unshift(callbacks.exit);
}
}
callbacks = visitor[type];
if (callbacks) {
if (callbacks.enter) {
specificEnters.push(callbacks.enter);
}
if (callbacks.exit) {
specificExits.unshift(callbacks.exit);
}
}
});
const enters = generalEnters.concat(specificEnters);
const exits = specificExits.concat(generalExits);
let context = node;
if ((enters.length > 0 || exits.length > 0) && options.prepareContext) {
context = options.prepareContext(node);
}
enters.forEach(enter => enter(context));
for (let key of node.children) {
if (node.hasOwnProperty(key)) {
if (key === 'locationData') {
continue;
}
let value = node[key];
if (Array.isArray(value)) {
for (let element of value) {
traverse(element, visitors, options);
}
} else if (typeof value === 'object') {
traverse(value, visitors, options);
}
}
}
exits.forEach(exit => exit(context));
}
const filename = process.argv[3];
const input = filename ? createReadStream(filename, { encoding: 'utf8' }) : process.stdin;
readInput(input, (err, content) => {
switch (process.argv[2]) {
case 'print':
printAST(content);
break;
case 'convert':
console.log(convert(content));
break;
}
});
function convert(source) {
return edit(source, [BoolVisitor, CallVisitor]);
}
const BoolVisitor = {
Bool: {
enter(node) {
switch (node.raw) {
case 'no':
node.overwrite('false');
break;
case 'yes':
node.overwrite('true');
break;
}
}
}
};
const CallVisitor = {
Call: {
enter(node) {
const callee = node.get('variable');
if (callee.peek(1) !== '(') {
const firstArgument = node.get('args').get(0);
if (firstArgument.line === callee.line) {
node.needsThisLineParens = true;
callee.overwriteUntil(firstArgument, '(');
} else {
node.needsNextLineParens = true;
callee.append('(');
}
}
},
exit(node) {
if (node.needsThisLineParens) {
node.append(')');
} else if (node.needsNextLineParens) {
node.append(`${node.indent})`);
}
}
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment