Skip to content

Instantly share code, notes, and snippets.

@panzi
Created October 12, 2022 22:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save panzi/d4b14498a4adad68663e798c9deb823d to your computer and use it in GitHub Desktop.
Save panzi/d4b14498a4adad68663e798c9deb823d to your computer and use it in GitHub Desktop.
A very simple pre-processor that I use in some build step. Could be more efficient and feature rich, but that is not the scope of this.
/**
* Very simple pre-processor.
*
* Syntax:
*
* #if FLAG
* ...
* #elif OTHER_FLAG
* ...
* #else
* ...
* #endif
*
* Simple negation is also supported:
*
* #if !FLAG
* ...
* #endif
*
* Nesting is also supported:
*
* #if FLAG
* #if OTHER_FLAG
* ...
* #endif
* #endif
*
* @param {string} input
* @param {{ [flag: string]: boolean }} flags
* @returns string
*/
function preproc(input, flags) {
const lines = input.split('\n');
function parseInstr(line) {
const match = /^#\s*([_a-zA-Z]+\b)(.*)$/.exec(line);
if (!match) {
return null;
}
return [match[1], match[2].trim()];
}
function parseFlag(arg) {
if (!arg) {
return null;
}
const match = /^\s*(!\s*)?([_a-zA-Z][_a-zA-Z0-9]*)\s*$/.exec(arg);
if (!match) {
return null;
}
return [!!match[1], match[2]];
}
function process(index) {
const buf = [];
while (index < lines.length) {
const line = lines[index];
if (line.startsWith('#')) {
const instr = parseInstr(line);
if (!instr) {
throw new Error(`syntax error on line ${index + 1}: ${line}`);
}
const [instrName, arg] = instr;
switch (instrName) {
case 'if':
const flag = parseFlag(arg);
if (!flag) {
throw new Error(`syntax error on line ${index + 1}: ${line}`);
}
const [notFlag, flagName] = flag;
const flagValue = flags[flagName];
let matched = false;
const [nextIndex, body] = process(index + 1);
if (notFlag ? !flagValue : flagValue) {
matched = true;
buf.push(body);
}
index = nextIndex;
while (index < lines.length) {
const line = lines[index];
const instr = parseInstr(line);
if (!instr) {
throw new Error(`syntax error on line ${index + 1}: ${line}`);
}
const [instrName, arg] = instr;
if (instrName !== 'elif') {
break;
}
const flag = parseFlag(arg);
if (!flag) {
throw new Error(`syntax error on line ${index + 1}: ${line}`);
}
const [notFlag, flagName] = flag;
const flagValue = flags[flagName];
const [nextIndex, body] = process(index + 1);
if (!matched && (notFlag ? !flagValue : flagValue)) {
matched = true;
buf.push(body);
}
index = nextIndex;
}
{
if (index >= lines.length) {
throw new Error(`syntax error on line ${index + 1}: unexpected end of input`);
}
let line = lines[index];
let instr = parseInstr(line);
if (!instr) {
throw new Error(`syntax error on line ${index + 1}: ${line}`);
}
let [instrName, arg] = instr;
if (instrName === 'else') {
if (arg) {
throw new Error(`syntax error on line ${index + 1}: ${line}`);
}
const [nextIndex, body] = process(index + 1);
if (!matched) {
matched = true;
buf.push(body);
}
index = nextIndex;
if (index >= lines.length) {
throw new Error(`syntax error on line ${index + 1}: unexpected end of input`);
}
line = lines[index];
instr = parseInstr(line);
if (!instr) {
throw new Error(`syntax error on line ${index + 1}: ${line}`);
}
[instrName, arg] = instr;
}
if (instrName !== 'endif') {
throw new Error(`syntax error on line ${index + 1}: ${line}`);
}
++ index;
}
break;
case 'elif':
case 'else':
case 'endif':
return [index, buf.join('\n')];
default:
throw new Error(`syntax error on line ${index + 1}: ${line}`);
}
} else {
buf.push(line);
++ index;
}
}
return [index, buf.join('\n')];
}
if (lines.length === 0) {
return '';
}
const [index, output] = process(0);
if (index < lines.length) {
throw new Error(`syntax error on line ${index + 1}: ${lines[index]}`);
}
return output;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment