css-doodle
Version:
A web component for visual art & creative coding
186 lines (167 loc) • 4.63 kB
JavaScript
import { scan } from '../parser/tokenizer.js';
const PREC = {
'(': 20, ')': 20,
'.': 19, '[': 19,
'!': 16, '~': 16,
'*': 14, '/': 14, '%': 14,
'+': 13, '-': 13,
'<<': 12, '>>': 12,
'<': 11, '<=': 11, '>': 11, '>=': 11, '≤': 11, '≥': 11,
'==': 10, '!=': 10, '=': 10, '≠': 10,
'&': 9,
'^': 8,
'|': 7,
'&&': 6,
'||': 5
};
const OP_ALIAS = {
'=': '==',
'≤': '<=',
'≥': '>=',
'≠': '!='
};
const TWO_CHAR_OPS = new Set([
'<<', '>>', '==', '!=', '<=', '>=', '&&', '||'
]);
function preprocessTokens(rawTokens) {
const tokens = [];
for (let i = 0; i < rawTokens.length; i++) {
const t = rawTokens[i];
if (t.type === 'Space') continue;
const next = rawTokens[i + 1];
if (next && t.isWord() && next.isNumber()) {
t.value += next.value;
i++;
tokens.push(t);
continue;
}
if (next) {
const combined = t.value + next.value;
if (TWO_CHAR_OPS.has(combined)) {
t.value = combined;
i++;
tokens.push(t);
continue;
}
}
tokens.push(t);
}
return tokens;
}
export default function transform(code, { expect = null } = {}) {
const tokens = preprocessTokens(scan(code));
let pos = 0;
const peek = () => tokens[pos];
const consume = () => tokens[pos++];
function parse(min = 0) {
const t = consume();
if (!t) return null;
let n;
if (t.isNumber()) {
n = { type: 'Lit', val: t.value };
} else if (t.isWord()) {
const next = peek();
n = (next && next.isSymbol('(')) ? call(t.value) : { type: 'Var', val: t.value };
} else if (t.value === '(') {
n = parse();
consume();
} else if (t.value === '!') {
n = { type: 'Pre', val: '!', right: parse(16) };
} else if (t.value === '~') {
n = { type: 'Pre', val: '~', right: parse(16) };
} else if (t.value === '-') {
n = { type: 'Pre', val: '-', right: parse(16) };
} else {
n = { type: 'Lit', val: '0' };
}
while (pos < tokens.length) {
const op = peek();
if (!op.isSymbol() || op.value === ')' || op.value === ',') break;
const p = PREC[op.value];
if (!p || p < min) break;
consume();
n = { type: 'Bin', val: op.value, left: n, right: parse(p + 1) };
}
return n;
}
function call(val) {
consume();
let args = [];
let next = peek();
if (next && next.value !== ')') {
do {
args.push(parse());
next = peek();
} while (next && next.value === ',' && consume() && (next = peek()));
}
consume();
return { type: 'Call', val, args };
}
function gen(n, exp) {
if (!n) return '';
if (n.type === 'Lit') {
if (exp === 'bool') return `bool(${n.val.includes('.') ? n.val : n.val + '.0'})`;
if (exp === 'int') return String(Math.floor(n.val));
return n.val.includes('.') ? n.val : n.val + '.0';
}
if (n.type === 'Var') return exp === 'int' ? `int(${n.val})` : n.val;
if (n.type === 'Pre') {
if (n.val === '!') return `!${gen(n.right, 'bool')}`;
if (n.val === '~') return `~${gen(n.right, 'int')}`;
return `-${gen(n.right, exp)}`;
}
if (n.type === 'Call') {
let args = n.args.map(a => gen(a, 'float')).join(', ');
if (n.val === 'int') return exp === 'float' ? `float(int(${args}))` : `int(${args})`;
if (n.val === 'float') return args;
return `${n.val}(${args})`;
}
const op = n.val;
let res = 'float', argExp = 'float';
switch (op) {
case '%':
res = 'float';
break;
case '&':
case '^':
case '|':
case '<<':
case '>>':
res = argExp = 'int';
break;
case '&&':
case '||':
res = argExp = 'bool';
break;
case '==':
case '!=':
case '=':
case '≠':
res = 'bool';
argExp = 'float';
break;
case '<':
case '>':
case '<=':
case '>=':
case '≤':
case '≥':
res = 'bool';
argExp = 'float';
break;
}
const l = gen(n.left, argExp);
const r = gen(n.right, argExp);
const glslOp = OP_ALIAS[op] || op;
const out = (op === '%') ? `mod(${l}, ${r})` : `(${l} ${glslOp} ${r})`;
if (res === 'bool' && exp) return `${exp}(${out})`;
if (res !== exp && exp) {
if (exp === 'bool') return `bool(${out})`;
if (exp === 'int') return `int(${out})`;
if (exp === 'float') return `float(${out})`;
}
return out;
}
try { return gen(parse(), expect); }
catch (e) { console.error(e); return code; }
}