Last active
March 26, 2022 17:44
-
-
Save raphtlw/b2a8ab392b89c75e9313853420cfb59e to your computer and use it in GitHub Desktop.
TypeScript macro preprocessor
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
#!/usr/bin/env ts-node | |
import assert from "assert"; | |
import fs from "fs"; | |
import readline from "readline"; | |
const argv = process.argv.slice(2); | |
const argFileName = argv[0]; | |
function error(msg: string) { | |
console.log(`ERROR: ${msg}`); | |
process.exit(1); | |
} | |
Object.defineProperty(Array.prototype, "last", { | |
value: function () { | |
return this[this.length - 1]; | |
}, | |
}); | |
declare global { | |
interface Array<T> { | |
last(): T; | |
} | |
} | |
const MATCH = { | |
MACRO_DECL: /macro`(.*?)->(.*?)`/gm, | |
MACRO_DECL_IDENTIFIER: /macro`.*?|[`;]/gm, | |
MACRO_USAGE_DECL_START: /^`/gm, | |
MACRO_USAGE_DECL_END: /^(?!macro).*?(`;?)/, | |
}; | |
enum MacroType { | |
BLOCK, | |
FUNCTION, | |
CONSTANT, | |
} | |
type MacroDecl = { | |
expr: string; | |
expanded: string; | |
type: MacroType; | |
startLn: number; | |
}; | |
type MacroUseBlock = { | |
startLn: number; | |
lines: string[]; | |
}; | |
class MacroEngine { | |
decls: MacroDecl[] = []; | |
usages: MacroUseBlock[] = []; | |
pointerInBlock = false; | |
define(decl: MacroDecl) { | |
if (this.decls.findIndex((v) => v.expr === decl.expr) === -1) { | |
this.decls.push(decl); | |
} | |
} | |
parse(line: string, lineNumber: number) { | |
if (line.match(MATCH.MACRO_DECL)) { | |
console.log(`line ${line} is a macro declaration`); | |
const [expr, expanded] = line | |
.replace(MATCH.MACRO_DECL_IDENTIFIER, "") | |
.split("->") | |
.map((l) => l.trim()); | |
this.decls.push({ | |
expr, | |
expanded, | |
type: MacroType.BLOCK, | |
startLn: lineNumber, | |
}); | |
} | |
if ( | |
line.match(MATCH.MACRO_USAGE_DECL_START) || | |
line.match(MATCH.MACRO_USAGE_DECL_END) || | |
this.pointerInBlock | |
) { | |
this.use(line, lineNumber); | |
} | |
} | |
use(line: string, lineNumber: number) { | |
if (line.match(MATCH.MACRO_USAGE_DECL_START)) { | |
const lineWithoutStart = line.replace(MATCH.MACRO_USAGE_DECL_START, ""); | |
const macroExpr = lineWithoutStart.split(" ")[0]; | |
console.log("macroExpr", macroExpr); | |
const macro = this.decls.find((decl) => decl.expr === macroExpr); | |
assert(macro, "Trying to use macro which is not defined"); | |
// replace macro block contents | |
let generatedLine = lineWithoutStart.replace(macroExpr, macro.expanded); | |
// keep track of current block | |
this.usages.push({ | |
startLn: lineNumber, | |
lines: [generatedLine], | |
}); | |
this.pointerInBlock = true; | |
return; | |
} | |
const declEndMatch = line.match(MATCH.MACRO_USAGE_DECL_END); | |
if (declEndMatch) { | |
const lineWithoutEnd = line.replace(declEndMatch[1], ""); | |
this.usages.last().lines.push(lineWithoutEnd); | |
this.pointerInBlock = false; | |
return; | |
} | |
if (this.pointerInBlock) { | |
this.usages.last().lines.push(line); | |
return; | |
} | |
} | |
} | |
const lineCounter = ( | |
(i = 0) => | |
() => | |
++i | |
)(); | |
async function main() { | |
const engine = new MacroEngine(); | |
const rl = readline.createInterface({ | |
input: fs.createReadStream(argFileName), | |
crlfDelay: Infinity, | |
}); | |
for await (const line of rl) { | |
const lineNumber = lineCounter(); | |
engine.parse(line, lineNumber); | |
} | |
console.log(engine.decls); | |
console.log(engine.usages); | |
const fileLines = fs.readFileSync(argFileName).toString().split("\n"); | |
console.log(fileLines); | |
for (const usage of engine.usages) { | |
fileLines.splice(usage.startLn - 1, usage.lines.length, ...usage.lines); | |
} | |
for (const decl of engine.decls) { | |
fileLines[decl.startLn - 1] = ""; | |
} | |
console.log(fileLines); | |
console.log(fileLines.join("\n").trim()); | |
} | |
if (argv.length > 0) { | |
main(); | |
} else { | |
error("input file not specified"); | |
} | |
if (!argFileName.endsWith(".ts")) { | |
error("input file is not a TypeScript file"); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment