/* Copyright 2005-2006 Garrett Rooney. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include "etl_template.h" #include "etl_variables.h" #include "../common/scope.h" #include "common.h" #include "lexer.h" static void destroy_token(void *t, void *baton) { etl_template_token_destroy(t); } static void bump_refcount(const char *key, void *val, void *baton) { etl_macro_contents_t *macro = val; macro->refcount++; } /* Forward declare some destructors we need at various points, since * I'm tired of forever moving them further up the file... */ static void destroy_expression(etl_expression_t *expr); static void destroy_expr(void *e, void *b); static void destroy_node(etl_template_node_t *node); static void destroy_macro(void *m, void *baton); static void destroy_menv(void *menv); static etl_error_t * template_parse(etl_template_t **tmpl, etl_stream_t *in, etl_hash_t *macros) { etl_array_t *tokens = etl_array_create(100); etl_template_parser_state_t state; etl_template_lexer_t *lexer; etl_token_t *tok; etl_error_t *err; void *parser; lexer = etl_template_lexer_create(in); parser = etl_template_parser_alloc(malloc); state.err = NULL; state.tmpl = NULL; state.spare = NULL; if (macros) { state.macros = etl_hash_copy(macros, NULL, NULL); /* Since we just stole a reference to these macros, bump their * refcounts. */ etl_hash_traverse(state.macros, bump_refcount, NULL); } else state.macros = etl_hash_create(); while ((err = etl_template_lexer_scan(&tok, lexer)) == ETL_SUCCESS) { /* Stash the token, so we can free it later once it's no longer * needed. */ etl_array_push(tokens, tok); if (tok->type != ETL_TMPL_TOK_EOI) { etl_template_parser_parse(parser, tok->type, tok, &state); } else { etl_template_parser_parse(parser, 0, tok, &state); break; } } etl_template_parser_parse(parser, 0, NULL, &state); /* Ok, at this point the tokens are useless, all necessary data has been * copied out of them, so just clean them up. */ etl_array_destroy(tokens, destroy_token, NULL); etl_template_parser_free(parser, free); etl_template_lexer_destroy(lexer); { list_entry_t *itr = state.spare; while (itr) { list_entry_t *prev = itr; itr = itr->next; free(prev); } } /* XXX If we error out during parsing/lexing it is possible that some of * our parser rules were already called, and thus we have leaked some * memory. We need to record stuff that we allocated in an array so * it can be cleaned up in the error case. For now, we just punt and * clean up the easy bits... */ /* Check for errors while lexing... */ if (err) { assert(! state.tmpl); etl_error_clear(state.err); etl_hash_destroy(state.macros, destroy_macro, NULL); return err; } /* Then errors from parsing... */ if (state.err) { assert(! state.tmpl); etl_hash_destroy(state.macros, destroy_macro, NULL); return state.err; } /* At this point, if we don't have a template from the parsing process * and there was no error then it's an empty template, so build one and * return it. */ if (state.tmpl) *tmpl = state.tmpl; else *tmpl = calloc(1, sizeof(**tmpl)); (*tmpl)->macros = state.macros; return ETL_SUCCESS; } etl_error_t * etl_template_parse(etl_template_t **tmpl, etl_stream_t *in) { return template_parse(tmpl, in, NULL); } etl_error_t * etl_template_parse_file(etl_template_t **tmpl, const char *filename) { etl_stream_t *in; ETL_ERR(etl_stream_file_open(&in, filename, etl_stream_open_read)); return etl_template_parse(tmpl, in); } static etl_error_t * print_var(const etl_variable_t *var, etl_hash_t *environment, etl_stream_t *out) { if (! var) return etl_error_create(ETL_EINVAL, "Can't print a NULL variable"); switch (etl_variable_type(var)) { case ETL_VARIABLE_STRING: ETL_ERR(etl_stream_printf(out, "%s", etl_variable_content_str(var))); break; case ETL_VARIABLE_INTEGER: ETL_ERR(etl_stream_printf(out, "%lld", etl_variable_content_int(var))); break; case ETL_VARIABLE_HASH: return etl_error_create(ETL_EINVAL, "Can't print a Hash"); case ETL_VARIABLE_ARRAY: return etl_error_create(ETL_EINVAL, "Can't print an Array"); default: abort(); } return ETL_SUCCESS; } static etl_error_t * html_filter(etl_stream_t *out, const char *contents) { int i, copied = 0; for (i = 0; contents[i]; ++i) { unsigned char c = (unsigned char) contents[i]; if (c != '<' && c != '>' && c != '&' && c != '"') continue; if (i - copied) { size_t st = i - copied; ETL_ERR(etl_stream_write(out, contents + copied, &st)); } switch (c) { case '<': ETL_ERR(etl_stream_printf(out, "<")); break; case '>': ETL_ERR(etl_stream_printf(out, ">")); break; case '&': ETL_ERR(etl_stream_printf(out, "&")); break; case '"': ETL_ERR(etl_stream_printf(out, """)); break; default: abort(); } copied = i + 1; } if (i - copied) { size_t st = i - copied; ETL_ERR(etl_stream_write(out, contents + copied, &st)); } return ETL_SUCCESS; } static etl_error_t * xml_filter(etl_stream_t *out, const char *contents) { return html_filter(out, contents); } static const char valid_uri_chars[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; static etl_error_t * url_filter(etl_stream_t *out, const char *contents) { int i, copied = 0; for (i = 0; contents[i]; ++i) { unsigned char c = (unsigned char) contents[i]; if (valid_uri_chars[c]) continue; if (i - copied) { size_t st = i - copied; ETL_ERR(etl_stream_write(out, contents + copied, &st)); } ETL_ERR(etl_stream_printf(out, "%%%02X", c)); copied = i + 1; } if (i - copied) { size_t st = i - copied; ETL_ERR(etl_stream_write(out, contents + copied, &st)); } return ETL_SUCCESS; } typedef etl_error_t * (*filter_func_t)(etl_stream_t *out, const char *contents); static filter_func_t filters[] = { html_filter, xml_filter, url_filter }; #define ETL_VAR_TRUE etl_variable_true() #define ETL_VAR_FALSE etl_variable_false() static int compare_vars(const etl_variable_t *left, const etl_variable_t *right, int cmp) { if (etl_variable_type(left) == ETL_VARIABLE_INTEGER && etl_variable_type(right) == ETL_VARIABLE_INTEGER) { uint64_t li = etl_variable_content_int(left); uint64_t ri = etl_variable_content_int(right); switch (cmp) { case ETL_EXPRESSION_OP_EQ: return li == ri; case ETL_EXPRESSION_OP_NEQ: return li != ri; case ETL_EXPRESSION_OP_LT: return li < ri; case ETL_EXPRESSION_OP_GT: return li > ri; case ETL_EXPRESSION_OP_LTEQ: return li <= ri; case ETL_EXPRESSION_OP_GTEQ: return li >= ri; default: abort(); } } else if (etl_variable_type(left) == ETL_VARIABLE_STRING && etl_variable_type(right) == ETL_VARIABLE_STRING) { const char *ls = etl_variable_content_str(left); const char *rs = etl_variable_content_str(right); switch (cmp) { case ETL_EXPRESSION_OP_EQ: return strcmp(ls, rs) == 0; case ETL_EXPRESSION_OP_NEQ: return strcmp(ls, rs) != 0; case ETL_EXPRESSION_OP_LT: return strcmp(ls, rs) < 0; case ETL_EXPRESSION_OP_GT: return strcmp(ls, rs) > 0; case ETL_EXPRESSION_OP_LTEQ: return strcmp(ls, rs) <= 0; case ETL_EXPRESSION_OP_GTEQ: return strcmp(ls, rs) >= 0; default: abort(); } } else if (etl_variable_type(left) == ETL_VARIABLE_ARRAY && etl_variable_type(right) == ETL_VARIABLE_ARRAY) { switch (cmp) { /* XXX provide a comparison function? */ case ETL_EXPRESSION_OP_EQ: return left == right; case ETL_EXPRESSION_OP_NEQ: return left != right; case ETL_EXPRESSION_OP_LT: case ETL_EXPRESSION_OP_GT: case ETL_EXPRESSION_OP_LTEQ: case ETL_EXPRESSION_OP_GTEQ: return 0; default: abort(); } } else if (etl_variable_type(left) == ETL_VARIABLE_HASH && etl_variable_type(right) == ETL_VARIABLE_HASH) { switch (cmp) { /* XXX provide a comparison function? */ case ETL_EXPRESSION_OP_EQ: return left == right; case ETL_EXPRESSION_OP_NEQ: return left != right; case ETL_EXPRESSION_OP_LT: case ETL_EXPRESSION_OP_GT: case ETL_EXPRESSION_OP_LTEQ: case ETL_EXPRESSION_OP_GTEQ: return 0; default: abort(); } } return 0; } static void destroy_var(void *v) { etl_variable_destroy(v); } static etl_error_t * eval_math(const etl_variable_t **result, const etl_variable_t *left, int op, const etl_variable_t *right, etl_scope_t *scope) { uint64_t li; uint64_t ri; if (etl_variable_type(left) != ETL_VARIABLE_INTEGER || etl_variable_type(right) != ETL_VARIABLE_INTEGER) return etl_error_create(ETL_ENOTIMPL, "Math not allowed on non-integers"); li = etl_variable_content_int(left); ri = etl_variable_content_int(right); switch (op) { case ETL_EXPRESSION_OP_MOD: *result = etl_variable_make_int(li % ri); break; case ETL_EXPRESSION_OP_PLUS: *result = etl_variable_make_int(li + ri); break; case ETL_EXPRESSION_OP_MINUS: *result = etl_variable_make_int(li - ri); break; case ETL_EXPRESSION_OP_TIMES: *result = etl_variable_make_int(li * ri); break; case ETL_EXPRESSION_OP_DIV: if (ri == 0) return etl_error_create(ETL_EINVAL, "Illegal divide by zero"); /* XXX How should we handle remainders? Do we need floats? */ *result = etl_variable_make_int(li / ri); break; default: abort(); } etl_scope_cleanup_register(scope, destroy_var, (void *) *result); return ETL_SUCCESS; } static etl_error_t * resolve_expression(const etl_variable_t **v, etl_hash_t *environment, const etl_expression_t *expr, etl_scope_t *scope) { etl_variable_t *var = NULL; if (expr->type == ETL_EXPRESSION_VAR) { const char *varname = expr->contents.v; var = etl_hash_get(environment, varname); if (! var) return etl_error_createf(ETL_EINVAL, "Variable '%s' not found", varname); *v = var; return ETL_SUCCESS; } else if (expr->type == ETL_EXPRESSION_LITERAL) { if (! expr->contents.lit) return etl_error_create(ETL_EINVAL, "Null literal expression found"); *v = expr->contents.lit; return ETL_SUCCESS; } else if (expr->type == ETL_EXPRESSION_NODE) { switch (expr->contents.n->op) { case ETL_EXPRESSION_OP_EQ: case ETL_EXPRESSION_OP_NEQ: case ETL_EXPRESSION_OP_LT: case ETL_EXPRESSION_OP_GT: case ETL_EXPRESSION_OP_LTEQ: case ETL_EXPRESSION_OP_GTEQ: { const etl_variable_t *left, *right; ETL_ERR(resolve_expression(&left, environment, expr->contents.n->left, scope)); ETL_ERR(resolve_expression(&right, environment, expr->contents.n->right, scope)); if (compare_vars(left, right, expr->contents.n->op)) *v = ETL_VAR_TRUE; else *v = ETL_VAR_FALSE; return ETL_SUCCESS; } break; case ETL_EXPRESSION_OP_MOD: case ETL_EXPRESSION_OP_PLUS: case ETL_EXPRESSION_OP_MINUS: case ETL_EXPRESSION_OP_TIMES: case ETL_EXPRESSION_OP_DIV: { const etl_variable_t *result = NULL, *left, *right; ETL_ERR(resolve_expression(&left, environment, expr->contents.n->left, scope)); ETL_ERR(resolve_expression(&right, environment, expr->contents.n->right, scope)); ETL_ERR(eval_math(&result, left, expr->contents.n->op, right, scope)); *v = result; return ETL_SUCCESS; } break; default: return etl_error_create(ETL_ENOTIMPL, "Operation not implemented"); } } else if (expr->type == ETL_EXPRESSION_INDEX) { const etl_variable_t *one, *two; ETL_ERR(resolve_expression(&one, environment, expr->contents.idx->first, scope)); ETL_ERR(resolve_expression(&two, environment, expr->contents.idx->second, scope)); if (etl_variable_type(one) == ETL_VARIABLE_HASH) { if (etl_variable_type(two) != ETL_VARIABLE_STRING) return etl_error_create(ETL_EINVAL, "Hash keys must be strings"); *v = etl_variable_hash_get(one, etl_variable_content_str(two)); if (! *v) return etl_error_createf(ETL_EINVAL, "Variable '%s' not found", etl_variable_content_str(two)); return ETL_SUCCESS; } else if (etl_variable_type(one) == ETL_VARIABLE_ARRAY) { if (etl_variable_type(two) != ETL_VARIABLE_INTEGER) return etl_error_create(ETL_EINVAL, "Array index must be an integer"); if (etl_variable_content_int(two) < 0 || etl_variable_content_int(two) > etl_variable_array_nelts(one)) return etl_error_createf( ETL_EINVAL, "Idx %lld is out of bounds", etl_variable_content_int(two) ); if (etl_variable_content_int(two) >= etl_variable_array_nelts(one)) return etl_error_create(ETL_EINVAL, "Out of bounds array access"); *v = etl_variable_array_idx(one, etl_variable_content_int(two)); if (! *v) return etl_error_createf(ETL_EINVAL, "Index '%d' was NULL", etl_variable_content_int(two)); return ETL_SUCCESS; } else return etl_error_create(ETL_EINVAL, "Can only index into Arrays or Hashes"); } else if (expr->type == ETL_EXPRESSION_METHOD) { const etl_variable_t *invocant; ETL_ERR(resolve_expression(&invocant, environment, expr->contents.method.invocant, scope)); switch (etl_variable_type(invocant)) { case ETL_VARIABLE_ARRAY: if (strcmp(expr->contents.method.name, "length") == 0) { if (etl_array_nelts(expr->contents.method.args) != 0) { return etl_error_createf( ETL_EINVAL, "Array.length() accepts 0 arguments, but was given %d", etl_array_nelts(expr->contents.method.args) ); } *v = etl_variable_make_int(etl_variable_array_nelts(invocant)); etl_scope_cleanup_register(scope, destroy_var, (void *) *v); return ETL_SUCCESS; } else { return etl_error_createf(ETL_EINVAL, "Invocant doesn't respond to '%s'", expr->contents.method.name); } break; default: return etl_error_createf(ETL_EINVAL, "Invocant doesn't respond to '%s'", expr->contents.method.name); } } else { return etl_error_create(ETL_EINVAL, "Can't resolve that expression"); } } static etl_error_t * for_loop(etl_template_node_t *itr, etl_resolver_t *resolver, etl_hash_t *environment, etl_hash_t *macros, etl_stream_t *out, etl_scope_t *scope); typedef struct { const char *key; void *val; void (*destructor)(void *, void *); etl_hash_t *hash; } reset_baton_t; static void reset_value(void *data) { reset_baton_t *rb = data; void *prev = etl_hash_set(rb->hash, rb->key, rb->val); if (rb->destructor) rb->destructor(prev, NULL); free(data); } static etl_error_t * execute_node(etl_template_node_t *n, etl_resolver_t *resolver, etl_hash_t *environment, etl_hash_t *macros, etl_stream_t *out, etl_scope_t *scope) { etl_template_node_t *itr = n; etl_error_t *err = NULL; while (itr) { int invert_sense = 0; switch (itr->type) { case ETL_NODE_PRINT: { const etl_variable_t *var; ETL_ERR(resolve_expression(&var, environment, itr->contents.p.var, scope)); ETL_ERR(print_var(var, environment, out)); } break; case ETL_NODE_FILTER: { etl_template_filter_type_t filt = itr->contents.flt->filter; const etl_variable_t *var; ETL_ERR(resolve_expression(&var, environment, itr->contents.flt->var, scope)); if (etl_variable_type(var) != ETL_VARIABLE_STRING) return etl_error_create(ETL_EINVAL, "Can't filter non-string variables"); if (filt < 0 || filt > (sizeof (filters) / sizeof (filters[0]))) return etl_error_create(ETL_EINVAL, "Filter not found"); else ETL_ERR(filters[filt](out, etl_variable_content_str(var))); } break; case ETL_NODE_LITERAL: ETL_ERR(etl_stream_printf(out, "%s", itr->contents.l.data)); break; case ETL_NODE_UNLESS: invert_sense = 1; /* FALLTHROUGH */ case ETL_NODE_IF: { etl_template_node_t *yes, *no; const etl_variable_t *var; if (invert_sense) { yes = itr->contents.i->no_task; no = itr->contents.i->yes_task; } else { yes = itr->contents.i->yes_task; no = itr->contents.i->no_task; } err = resolve_expression(&var, environment, itr->contents.i->var, scope); if (err) { var = NULL; etl_error_clear(err); } if (var && ((etl_variable_type(var) == ETL_VARIABLE_STRING && atoi(etl_variable_content_str(var)) != 0) || (etl_variable_type(var) == ETL_VARIABLE_INTEGER && etl_variable_content_int(var) != 0))) { if (yes) { ETL_ERR(execute_node(yes, resolver, environment, macros, out, scope)); } } else if (no) { ETL_ERR(execute_node(no, resolver, environment, macros, out, scope)); } } break; case ETL_NODE_FOR: ETL_ERR(for_loop(itr, resolver, environment, macros, out, scope)); break; case ETL_NODE_INCLUDE: { const etl_variable_t *var; etl_template_t *tmpl; etl_stream_t *tmpin; ETL_ERR(resolve_expression(&var, environment, itr->contents.inc.file, scope)); if (etl_variable_type(var) != ETL_VARIABLE_STRING) return etl_error_create(ETL_EINVAL, "Argument to include must be a String"); ETL_ERR(etl_resolver_resolve(&tmpin, resolver, etl_variable_content_str(var))); ETL_ERR(template_parse(&tmpl, tmpin, macros)); { etl_error_t *err = execute_node(tmpl->tree, resolver, environment, macros, out, scope); etl_template_destroy(tmpl); if (err) return err; } } break; case ETL_NODE_MACRO: { /* Making a new copy of the environment is probably a bit * wasteful, but whatever... */ etl_macro_contents_t *macro; etl_scope_t *subscope; etl_hash_t *menv; int idx; subscope = etl_scope_create(scope); macro = etl_hash_get(macros, itr->contents.macro->name); if (! macro) return etl_error_createf(ETL_EINVAL, "Macro '%s' is not defined", itr->contents.macro->name); if (etl_array_nelts(macro->arg_names) != etl_array_nelts(itr->contents.macro->arg_values)) { return etl_error_createf( ETL_EINVAL, "Macro '%s' takes %%d arguments, but was given %%d", itr->contents.macro->name, etl_array_nelts(macro->arg_names), etl_array_nelts(itr->contents.macro->arg_values) ); } menv = etl_hash_copy(environment, NULL, NULL); etl_scope_cleanup_register(subscope, destroy_menv, menv); /* Resolve our args and stick them in our new hash so we can * evaluate the macro. */ for (idx = 0; idx < etl_array_nelts(macro->arg_names); ++idx) { const etl_variable_t *var; const etl_expression_t *expr = etl_array_idx(itr->contents.macro->arg_values, idx); ETL_ERR(resolve_expression(&var, environment, expr, subscope)); etl_hash_set(menv, etl_array_idx(macro->arg_names, idx), var); } ETL_ERR(execute_node(macro->body, resolver, menv, macros, out, subscope)); etl_scope_destroy(subscope); } break; case ETL_NODE_DEFMACRO: { reset_baton_t *rb = malloc(sizeof(*rb)); rb->hash = macros; rb->key = itr->contents.macro->name; rb->val = etl_hash_get(macros, itr->contents.macro->name); rb->destructor = destroy_macro; etl_scope_cleanup_register(scope, reset_value, rb); etl_hash_set(macros, itr->contents.macro->name, itr->contents.macro); } break; default: abort(); } itr = itr->next; } return ETL_SUCCESS; } static etl_error_t * loop_body(etl_template_node_t *itr, etl_resolver_t *resolver, etl_hash_t *environment, etl_hash_t *macros, etl_stream_t *out, const etl_variable_t *index, etl_scope_t *scope) { etl_variable_t *old; etl_error_t *err; old = etl_hash_get(environment, itr->contents.f->var); etl_hash_set(environment, itr->contents.f->var, index); err = execute_node(itr->contents.f->task, resolver, environment, macros, out, scope); etl_hash_set(environment, itr->contents.f->var, old); return err; } typedef struct { etl_variable_t *key; etl_scope_t *subscope; const char *keyvar; const char *valvar; etl_hash_t *environment; etl_template_node_t *itr; etl_resolver_t *resolver; etl_hash_t *macros; etl_stream_t *out; etl_error_t *err; } loop_body_baton_t; static void do_loop_body(const char *k, etl_variable_t *v, void *b) { loop_body_baton_t *baton = b; if (baton->err) return; etl_variable_str_set(baton->key, k); etl_scope_clear(baton->subscope); if (baton->keyvar) etl_hash_set(baton->environment, baton->keyvar, baton->key); if (baton->valvar) etl_hash_set(baton->environment, baton->valvar, v); baton->err = execute_node(baton->itr->contents.f->task, baton->resolver, baton->environment, baton->macros, baton->out, baton->subscope); } static etl_error_t * for_loop(etl_template_node_t *itr, etl_resolver_t *resolver, etl_hash_t *environment, etl_hash_t *macros, etl_stream_t *out, etl_scope_t *scope) { etl_error_t *err = ETL_SUCCESS; etl_scope_t *subscope = etl_scope_create(scope); if (itr->contents.f->type == ETL_FOR_VAR) { const etl_variable_t *var; ETL_ERR(resolve_expression(&var, environment, itr->contents.f->over.arr, scope)); switch (etl_variable_type(var)) { case ETL_VARIABLE_ARRAY: { uint32_t i; for (i = 0; i < etl_variable_array_nelts(var); ++i) { etl_scope_clear(subscope); ETL_ERR(loop_body(itr, resolver, environment, macros, out, etl_variable_array_idx(var, i), subscope)); } } break; case ETL_VARIABLE_HASH: { etl_variable_t *key, *oldkey = NULL, *oldval = NULL; loop_body_baton_t baton; char *keyvar, *valvar; key = etl_variable_make_str(""); etl_scope_cleanup_register(scope, destroy_var, key); keyvar = itr->contents.f->var; valvar = itr->contents.f->var2; if (keyvar) oldkey = etl_hash_get(environment, keyvar); if (valvar) oldval = etl_hash_get(environment, valvar); baton.key = key; baton.subscope = subscope; baton.keyvar = keyvar; baton.valvar = valvar; baton.environment = environment; baton.itr = itr; baton.resolver = resolver; baton.macros = macros; baton.out = out; baton.err = ETL_SUCCESS; etl_variable_hash_traverse(var, do_loop_body, &baton); if (baton.err) return baton.err; if (keyvar) etl_hash_set(environment, keyvar, oldkey); if (valvar) etl_hash_set(environment, valvar, oldval); } break; default: return etl_error_create(ETL_EINVAL, "Can't loop over that type of variable"); } } else if (itr->contents.f->type == ETL_FOR_RANGE) { const etl_variable_t *from, *to; etl_variable_t *index; int64_t idx; ETL_ERR(resolve_expression(&from, environment, itr->contents.f->over.range.from, scope)); ETL_ERR(resolve_expression(&to, environment, itr->contents.f->over.range.to, scope)); if (etl_variable_type(from) != ETL_VARIABLE_INTEGER || etl_variable_type(to) != ETL_VARIABLE_INTEGER) return etl_error_create( ETL_EINVAL, "Can't use that type of variable as a endpoint of a range" ); index = etl_variable_make_int(0); etl_scope_cleanup_register(scope, destroy_var, index); if (etl_variable_content_int(from) <= etl_variable_content_int(to)) { for (idx = etl_variable_content_int(from); idx <= etl_variable_content_int(to); ++idx) { etl_variable_int_set(index, idx); etl_scope_clear(subscope); ETL_ERR(loop_body(itr, resolver, environment, macros, out, index, subscope)); } } else { for (idx = etl_variable_content_int(from); idx >= etl_variable_content_int(to); --idx) { etl_variable_int_set(index, idx); etl_scope_clear(subscope); ETL_ERR(loop_body(itr, resolver, environment, macros, out, index, subscope)); } } } else { return etl_error_create(ETL_ENOTIMPL, "That kind of loop isn't implemented yet"); } etl_scope_destroy(subscope); return err; } etl_error_t * etl_template_execute(etl_template_t *t, etl_resolver_t *resolver, etl_hash_t *environment, etl_stream_t *out) { etl_scope_t *scope = etl_scope_create(NULL); etl_error_t *err; err = execute_node(t->tree, resolver, environment, t->macros, out, scope); etl_scope_destroy(scope); return err; } static void destroy_string(void *s, void *b) { free(s); } static void destroy_expr(void *e, void *b) { destroy_expression(e); } static void destroy_expression(etl_expression_t *expr) { if (! expr) return; switch (expr->type) { case ETL_EXPRESSION_VAR: free(expr->contents.v); break; case ETL_EXPRESSION_NODE: destroy_expression(expr->contents.n->left); destroy_expression(expr->contents.n->right); free(expr->contents.n); break; case ETL_EXPRESSION_INDEX: destroy_expression(expr->contents.idx->first); destroy_expression(expr->contents.idx->second); free(expr->contents.idx); break; case ETL_EXPRESSION_LITERAL: etl_variable_destroy(expr->contents.lit); break; case ETL_EXPRESSION_METHOD: destroy_expression(expr->contents.method.invocant); free(expr->contents.method.name); etl_array_destroy(expr->contents.method.args, destroy_expr, NULL); break; default: abort(); } free(expr); } static void destroy_menv(void *menv) { etl_hash_destroy(menv, NULL, NULL); } static void destroy_macro(void *m, void *baton) { etl_macro_contents_t *macro = m; if (! macro) return; if (--macro->refcount != 0) { /* If we've gone below zero something is wrong... */ assert(macro->refcount > 0); return; } free(macro->name); if (macro->arg_names) etl_array_destroy(macro->arg_names, destroy_string, NULL); if (macro->arg_values) etl_array_destroy(macro->arg_values, destroy_expr, NULL); destroy_node(macro->body); free(macro); } static void destroy_node(etl_template_node_t *node) { if (! node) return; switch (node->type) { case ETL_NODE_PRINT: destroy_expression(node->contents.p.var); break; case ETL_NODE_IF: case ETL_NODE_UNLESS: destroy_expression(node->contents.i->var); destroy_node(node->contents.i->yes_task); destroy_node(node->contents.i->no_task); free(node->contents.i); break; case ETL_NODE_FOR: free(node->contents.f->var); free(node->contents.f->var2); if (node->contents.f->type == ETL_FOR_VAR) destroy_expression(node->contents.f->over.arr); else { destroy_expression(node->contents.f->over.range.from); destroy_expression(node->contents.f->over.range.to); } destroy_node(node->contents.f->task); free(node->contents.f); break; case ETL_NODE_MACRO: destroy_macro(node->contents.macro, NULL); break; case ETL_NODE_DEFMACRO: /* Nothing to do here, macros are cleaned up elsewhere */ break; case ETL_NODE_LITERAL: free(node->contents.l.data); break; case ETL_NODE_FILTER: destroy_expression(node->contents.flt->var); free(node->contents.flt); break; case ETL_NODE_INCLUDE: destroy_expression(node->contents.inc.file); break; default: abort(); } destroy_node(node->next); free(node); } void etl_template_destroy(etl_template_t *tmpl) { if (! tmpl) return; destroy_node(tmpl->tree); etl_hash_destroy(tmpl->macros, destroy_macro, NULL); free(tmpl); }