Created
September 3, 2015 16:50
-
-
Save eventualbuddha/490105cc38619cedf139 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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