edgerender-yatl
Version:
Yet Another Template Language
302 lines • 11.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.build_modifiers = exports.build_operations = exports.build_functions = exports.build_chains = exports.build_groups = void 0;
const groupings = {
'(': { close: ')', type: '()' },
'[': { close: ']', type: '[]' },
};
function build_groups(tokens) {
let depth = 0;
let open = null;
let close = null;
const members = [];
let current_group_type = null;
let current_group_args = [];
let current_arg = [];
for (const token of tokens) {
if (depth == 0) {
const g = groupings[token.type];
if (g) {
close = g.close;
open = token.type;
current_group_type = g.type;
depth = 1;
}
else {
if (token.type == ',') {
throw Error('commas can only occur inside brackets');
}
members.push(token);
}
}
else {
if (token.type == open) {
depth++;
current_arg.push(token);
}
else if (token.type == close) {
depth--;
if (depth == 0) {
if (current_arg.length) {
current_group_args.push(build_groups(current_arg));
}
members.push({
type: 'group',
subtype: current_group_type,
args: current_group_args,
});
current_group_args = [];
current_arg = [];
}
else {
current_arg.push(token);
}
}
else {
if (depth == 1 && token.type == ',') {
current_group_args.push(build_groups(current_arg));
current_arg = [];
}
else {
current_arg.push(token);
}
}
}
}
if (depth != 0) {
throw Error(`Unclosed group "${open}", "${close}" not found before end of string`);
}
return members;
}
exports.build_groups = build_groups;
function build_chains(groups) {
const new_groups = [];
for (let index = 0; index < groups.length; index++) {
const g = groups[index];
if (g.type == 'symbol') {
const chain = [];
while (true) {
const next = groups[index + 1];
if (is_chain_op(next)) {
const lookup = groups[index + 2];
const op = next.type;
if (lookup && lookup.type == 'group' && lookup.subtype == '[]') {
chain.push(chain_from_brackets(lookup, op));
}
else if (lookup.type == 'symbol') {
// type here is string since we use the raw item, rather than considering it as a variable
chain.push({ op, lookup: lookup.value, type: 'str' });
}
else if (!lookup) {
throw Error(`expression ended with operator "${op}", no final token`);
}
else {
throw Error('"." and ".?" are only valid between tokens');
}
index += 2;
}
else if (next && next.type == 'group' && next.subtype == '[]') {
chain.push(chain_from_brackets(next, '.'));
index += 1;
}
else {
break;
}
}
new_groups.push({ type: 'var', symbol: g.value, chain });
}
else if (g.type == 'group') {
if (g.subtype == '[]') {
throw Error('square brackets [] can only be used after a token');
}
else {
new_groups.push({ ...g, args: g.args.map(a => build_chains(a)) });
}
}
else if (is_chain_op(g)) {
throw Error('"." and ".?" are only valid between tokens');
}
else {
// normal token
new_groups.push(g);
}
}
return new_groups;
}
exports.build_chains = build_chains;
const is_chain_op = (g) => !!g && (g.type == '.' || g.type == '.?');
function chain_from_brackets(g, op) {
if (g.args.length != 1 || g.args[0].length != 1) {
throw Error('A single token or string must be used as the input to square brackets');
}
const arg = g.args[0][0];
if (arg.type != 'symbol' && arg.type != 'str' && arg.type != 'num') {
throw Error(`A token or string must be used as the input to square brackets, not "${arg.type}"`);
}
return { op, lookup: arg.value, type: arg.type };
}
function build_functions(groups) {
const new_groups = [];
for (let index = 0; index < groups.length; index++) {
const g = groups[index];
if (g.type == 'var') {
const next = groups[index + 1];
if (next && next.type == 'group' && next.subtype == '()') {
new_groups.push({ type: 'func', temp: true, var: g, args: next.args });
index++;
continue;
}
}
if (g.type == 'group') {
new_groups.push({ ...g, args: g.args.map(build_functions) });
}
else {
new_groups.push(g);
}
}
return new_groups;
}
exports.build_functions = build_functions;
// https://docs.python.org/3/reference/expressions.html#operator-precedence
const operator_precedence = ['|', '*', '/', '+', '-', '==', '!=', 'in', '!in', '&&', '||'];
const operator_set = new Set(operator_precedence);
const operator_mod_set = new Set(operator_precedence.concat('!'));
function build_operations(groups) {
let tmp_groups = groups;
for (const operator_type of operator_precedence) {
if (operator_type == '*') {
tmp_groups = build_modifiers(tmp_groups);
}
const new_groups = [];
for (let index = 0; index < tmp_groups.length; index++) {
const g = tmp_groups[index];
const args = [];
while (index < tmp_groups.length - 1 && tmp_groups[index + 1].type == operator_type) {
const arg = tmp_groups[index + 2];
if (!arg) {
throw Error(`expression ended unexpectedly with operator "${operator_type}"`);
}
if (operator_set.has(arg.type)) {
throw Error(`operator "${operator_type}" followed by another operator "${arg.type}`);
}
else {
args.push(arg);
index += 2;
}
}
if (args.length) {
if ((operator_type == 'in' || operator_type == '!in') && args.length > 1) {
throw Error(`chaining the "${operator_type}" operator is not permitted`);
}
new_groups.push({ type: 'operator', operator: operator_type, args: [g, ...args].map(apply_build_operations) });
}
else {
// operator still to be processed
new_groups.push(apply_build_operations(g));
}
}
tmp_groups = new_groups;
}
if (tmp_groups.length != 1) {
console.log('clauses: %o', tmp_groups);
throw Error(`internal error, ${tmp_groups.length} clauses found after reduction, should be just 1`);
}
return tmp_groups[0];
}
exports.build_operations = build_operations;
function apply_build_operations(g) {
if (g.type == 'group' || g.type == 'func') {
return { ...g, args: g.args.map(a => [build_operations(a)]) };
}
else if (g.type == 'mod') {
return { ...g, element: build_operations([g.element]) };
}
else {
return g;
}
}
const modifiable = new Set(['group', 'func', 'mod', 'var', 'num', 'operator']);
function build_modifiers(groups) {
/**
* Called after filters in build_operations to apply modifiers before going through other operators, see precedence
*/
while (true) {
const new_groups = [];
let found_modifiers = false;
for (let index = 0; index < groups.length; index++) {
const g = groups[index];
if (g.type == '!' || g.type == '-') {
// TODO anything that can not be modified?
if (index == groups.length - 1) {
throw Error(`expression ended unexpectedly with modifier "${g.type}"`);
}
if (index == 0 || operator_mod_set.has(groups[index - 1].type)) {
// at the beginning or after an operator
const next = groups[index + 1];
if (modifiable.has(next.type)) {
new_groups.push({ type: 'mod', mod: g.type, element: apply_build_operations(next) });
index++;
found_modifiers = true;
continue;
}
}
else if (g.type == '!') {
throw new Error('The not modifier "!" is not permitted between expressions');
}
}
if (g.type == 'group' || g.type == 'func') {
new_groups.push({ ...g, args: g.args.map(build_modifiers) });
}
else {
new_groups.push(g);
}
}
if (found_modifiers) {
groups = new_groups;
}
else {
return new_groups;
}
}
}
exports.build_modifiers = build_modifiers;
function mixed_as_clause(g) {
switch (g.type) {
case 'group':
if (g.subtype != '()') {
throw Error(`internal error, unexpected group type "${g.subtype}"`);
}
if (g.args.length == 1) {
return mixed_as_clause(g.args[0][0]);
}
else {
return { type: 'list', elements: g.args.map(a => mixed_as_clause(a[0])) };
}
case 'func':
return { type: 'func', var: g.var, args: g.args.map(a => mixed_as_clause(a[0])) };
case 'num':
return { type: 'num', value: g.value };
case 'str':
return { type: 'str', value: g.value };
case 'true':
case 'false':
return { type: 'bool', value: g.type == 'true' };
case 'mod':
return { type: 'mod', mod: g.mod, element: mixed_as_clause(g.element) };
case 'operator':
return { type: 'operator', operator: g.operator, args: g.args.map(mixed_as_clause) };
case 'var':
return g;
default:
throw Error(`Internal Error: got unexpected element: ${JSON.stringify(g)}`);
}
}
function build_expression(tokens) {
const groups = build_groups(tokens);
const chains = build_chains(groups);
const functions = build_functions(chains);
const element = build_operations(functions);
return mixed_as_clause(element);
}
exports.default = build_expression;
//# sourceMappingURL=build.js.map