css-doodle
Version:
A web component for drawing patterns with CSS
296 lines (274 loc) • 7.43 kB
JavaScript
/**
* Based on the Shunting-yard algorithm.
*/
import { is_invalid_number, last } from './utils/index.js';
import { cache } from './cache.js';
const default_context = {
'π': Math.PI,
gcd: (a, b) => {
while (b) [a, b] = [b, a % b];
return a;
}
}
const operator = {
'^': 7,
'*': 6, '/': 6, '÷': 6, '%': 6,
'&': 5, '|': 5,
'+': 4, '-': 4,
'<': 3, '<<': 3,
'>': 3, '>>': 3,
'=': 3, '==': 3,
'≤': 3, '<=': 3,
'≥': 3, '>=': 3,
'≠': 3, '!=': 3,
'∧': 2, '&&': 2,
'∨': 2, '||': 2,
'(': 1 , ')': 1,
}
function calc(expr, context, repeat = []) {
let stack = [];
while (expr.length) {
let { name, value, type } = expr.shift();
if (type === 'variable') {
let result = context[value];
if (is_invalid_number(result)) {
result = Math[value];
}
if (is_invalid_number(result)) {
result = expand(value, context, repeat);
}
if (is_invalid_number(result)) {
if (/^\-\D/.test(value)) {
result = expand('-1' + value.substr(1), context, repeat);
}
}
if (result === undefined) {
result = 0;
}
if (typeof result !== 'number') {
repeat.push(result);
if (is_cycle(repeat)) {
result = 0;
repeat = [];
} else {
result = calc(infix_to_postfix(result), context, repeat)
}
}
stack.push(result);
}
else if (type === 'function') {
let negative = false;
if (/^\-/.test(name)) {
negative = true;
name = name.substr(1);
}
let output = value.map(v => calc(v, context, repeat));
let fns = name.split('.');
let fname;
while (fname = fns.pop()) {
if (!fname) continue;
let fn = context[fname] || Math[fname];
output = (typeof fn === 'function')
? (Array.isArray(output) ? fn(...output) : fn(output))
: 0;
}
if (negative) {
output = -1 * output;
}
stack.push(output);
} else {
if (/\d+/.test(value)) stack.push(value);
else {
let right = stack.pop();
let left = stack.pop();
stack.push(compute(
value, Number(left), Number(right)
));
}
}
}
return Number(stack[0]) || 0;
}
function get_tokens(input) {
if (cache.has(input)) {
return cache.get(input);
}
let expr = String(input);
let tokens = [], num = '';
for (let i = 0; i < expr.length; ++i) {
let c = expr[i];
if (operator[c]) {
let last_token = last(tokens);
if (c == '=' && last_token && /^[!<>=]$/.test(last_token.value)) {
last_token.value += c;
}
else if (/^[|&<>]$/.test(c) && last_token && last_token.value == c) {
last_token.value += c;
}
else if (c == '-' && expr[i - 1] == 'e') {
num += c;
}
else if (!tokens.length && !num.length && /[+-]/.test(c)) {
num += c;
} else {
let { type, value } = last_token || {};
if (type == 'operator'
&& !num.length
&& /[^()]/.test(c)
&& /[^()]/.test(value)) {
num += c;
} else {
if (num.length) {
tokens.push({ type: 'number', value: num });
num = '';
}
tokens.push({ type: 'operator', value: c });
}
}
}
else if (/\S/.test(c)) {
if (c == ',') {
tokens.push({ type: 'number', value: num });
num = '';
tokens.push({ type: 'comma', value: c });
} else if (c == '!') {
tokens.push({ type: 'number', value: num });
tokens.push({ type: 'operator', value: c });
num = '';
} else {
num += c;
}
}
}
if (num.length) {
tokens.push({ type: 'number', value: num });
}
cache.set(input, tokens);
return tokens;
}
function infix_to_postfix(input) {
let tokens = get_tokens(input);
const op_stack = [], expr = [];
for (let i = 0; i < tokens.length; ++i) {
let { type, value } = tokens[i];
let next = tokens[i + 1] || {};
if (type == 'number') {
if (next.value == '(' && /[^\d.\-]/.test(value)) {
let func_body = '';
let stack = [];
let values = [];
i += 1;
while (tokens[i++] !== undefined) {
let token = tokens[i];
if (token === undefined) break;
let c = token.value;
if (c == ')') {
if (!stack.length) break;
stack.pop();
func_body += c;
}
else {
if (c == '(') stack.push(c);
if (c == ',' && !stack.length) {
let arg = infix_to_postfix(func_body);
if (arg.length) values.push(arg);
func_body = '';
} else {
func_body += c;
}
}
}
if (func_body.length) {
values.push(infix_to_postfix(func_body));
}
expr.push({
type: 'function',
name: value,
value: values
});
}
else if (/[^\d.\-]/.test(value)) {
expr.push({ type: 'variable', value });
}
else {
expr.push({ type: 'number', value });
}
}
else if (type == 'operator') {
if (value == '(') {
op_stack.push(value);
}
else if (value == ')') {
while (op_stack.length && last(op_stack) != '(') {
expr.push({ type: 'operator', value: op_stack.pop() });
}
op_stack.pop();
}
else {
while (op_stack.length && operator[last(op_stack)] >= operator[value]) {
let op = op_stack.pop();
if (!/[()]/.test(op)) expr.push({ type: 'operator', value: op });
}
op_stack.push(value);
}
}
}
while (op_stack.length) {
expr.push({ type: 'operator', value: op_stack.pop() });
}
return expr;
}
function compute(op, a, b) {
switch (op) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '%': return a % b;
case '|': return a | b;
case '&': return a & b;
case '<': return a < b;
case '>': return a > b;
case '^': return Math.pow(a, b);
case '÷': case '/': return a / b;
case '=': case '==': return a == b;
case '≤': case '<=': return a <= b;
case '≥': case '>=': return a >= b;
case '≠': case '!=': return a != b;
case '∧': case '&&': return a && b;
case '∨': case '||': return a || b;
case '<<': return a << b;
case '>>': return a >> b;
}
}
function expand(value, context, repeat) {
let [_, num, variable] = value.match(/([\d.\-]+)(.*)/) || [];
let v = context[variable];
if (v === undefined) {
return v;
}
if (typeof v === 'number') {
return Number(num) * v;
} else {
repeat.push(v);
if (is_cycle(repeat)) {
repeat = [];
return 0;
} else {
return num * calc(infix_to_postfix(v), context, repeat);
}
}
}
function is_cycle(array) {
if (array.length > 50) return true;
let tail = last(array);
for (let i = 2; i <= 4; ++i) {
let item = array[array.length - i];
if (item === undefined) return false;
if (tail !== item) return false;
}
return true;
}
export default function(input, context) {
const expr = infix_to_postfix(input);
return calc(expr, Object.assign({}, default_context, context));
}