UNPKG

edgerender-yatl

Version:

Yet Another Template Language

302 lines 11.4 kB
"use strict"; 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