From b3d040d6fd6e43240327627dbcf10345068be8f4 Mon Sep 17 00:00:00 2001 From: yukkop Date: Wed, 9 Apr 2025 19:52:43 +0000 Subject: [PATCH] feat: `hectic` C: template parser, template to json --- package/c/hectic/docs/templater.md | 20 +- package/c/hectic/hectic.c | 716 ++++++++++++++++++++------- package/c/hectic/hectic.h | 155 ++++-- package/c/hectic/make.sh | 6 +- package/c/hectic/test/04-templater.c | 233 +++------ 5 files changed, 723 insertions(+), 407 deletions(-) diff --git a/package/c/hectic/docs/templater.md b/package/c/hectic/docs/templater.md index ab542eb..dc4adc0 100755 --- a/package/c/hectic/docs/templater.md +++ b/package/c/hectic/docs/templater.md @@ -16,10 +16,6 @@ The templating engine supports flexible customization of tag syntax parameters. A non-empty string marking the end of a tag. *Example:* `%}` -- **Null Handler** - A non-empty string used to specify alternative content when a value is null. - *Example:* `%%` - --- ## Section Tags @@ -34,10 +30,6 @@ Parameters defining syntax for blocks controlling loops or nested structures. Delimiter between variables and collections. *Example:* ` in ` | `#` -- **Optional Suffix** - Additional modifier, e.g., for joining collections. - *Example:* ` join ` | `#` - - **Post-Suffix** Finalizes the section declaration block. *Example:* `do ` | `:` @@ -50,8 +42,6 @@ Parameters defining syntax for blocks controlling loops or nested structures. {% for inner_item in item.inner_items join '\n' do

some other text

{% inner_item %} - %% -

Sorry, list is empty

%} \n %} @@ -77,8 +67,6 @@ Inserts variable values or expression results directly into templates. *Interpolation Example:* ```tpl {% interpolation_field %} - {% interpolation_field_null %% Sorry, empty %} - {% interpolation_field_null %% {% interpolation_field %} %} ``` *Context Example:* ```json @@ -138,11 +126,12 @@ Includes content from other templates. Enables calling functions with arguments. - **Prefix** Denotes a function call. - *Example:* `call` | *(Empty)* + *Example:* `exec` | *(Empty)* *Function Example:* ```tpl - {% call my_function(arg1, arg2, 'literal') %} + {% exec my_function(arg1, arg2, 'literal') %} + {% exec RETURN 'aaaaa' %} ``` ## Notes @@ -151,5 +140,4 @@ Enables calling functions with arguments. - **Unclosed Tags:** Must return an error. - **Missing Fields/Functions/Templates:** Configurable to either return an error or warning. - **Circular Includes:** Detect when possible. -- **No Shadowing:** Variables defined in section tags must not conflict with context variable names, otherwise, return an error. - +- **No Shadowing:** Variables defined in section tags must not conflict with context variable names, otherwise, return an error. \ No newline at end of file diff --git a/package/c/hectic/hectic.c b/package/c/hectic/hectic.c index 51836e9..57b632d 100644 --- a/package/c/hectic/hectic.c +++ b/package/c/hectic/hectic.c @@ -1,6 +1,7 @@ #include "hectic.h" #include -#include // For strdup, strchr, etc. +#include +#include // On systems without strsep, provide a custom implementation #ifndef _GNU_SOURCE @@ -170,14 +171,16 @@ char* raise_message( strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &tm_info); // Print timestamp, log level with color, location info - fprintf(stderr, "%s %s%s%s [%s:%s:%d] ", + fprintf(stderr, "%s %s%s%s [%s:%s:%s%d%s] ", timeStr, log_level_to_color(level), log_level_to_string(level), OPTIONAL_COLOR(COLOR_RESET), file, func, - line); + OPTIONAL_COLOR(COLOR_GREEN), + line, + OPTIONAL_COLOR(COLOR_RESET)); // Print the actual message with variable arguments va_list args; @@ -219,35 +222,58 @@ Arena arena_init__(POSITION_INFO_DECLARATION, size_t size) { return arena; } -void* arena_alloc_or_null__(POSITION_INFO_DECLARATION, Arena *arena, size_t size) { - // Function entry at TRACE level +void* arena_alloc_or_null__(POSITION_INFO_DECLARATION, Arena *arena, size_t size, bool expand) { raise_message(LOG_LEVEL_TRACE, POSITION_INFO, - "ALLOC: Requesting memory from arena (arena: %p, size: %zu bytes)", arena, size); - - void *mem = NULL; + "ALLOC: Requesting memory from arena (arena: %p, size: %zu bytes)", arena, size); + if (arena->begin == 0) { raise_message(LOG_LEVEL_DEBUG, POSITION_INFO, - "ALLOC: Arena not initialized, creating new arena"); - *arena = arena_init__(POSITION_INFO, 1024); // ARENA_DEFAULT_SIZE assumed as 1024 + "ALLOC: Arena not initialized, creating new arena"); + *arena = arena_init__(POSITION_INFO, 1024); } - - size_t current = (size_t)arena->current - (size_t)arena->begin; - if (arena->capacity <= current || arena->capacity - current < size) { - raise_message(LOG_LEVEL_WARN, POSITION_INFO, - "ALLOC: Insufficient memory in arena (address: %p, capacity: %zu bytes, used: %zu bytes, requested: %zu bytes)", - arena->begin, arena->capacity, current, size); - return NULL; - } else { - raise_message(LOG_LEVEL_DEBUG, POSITION_INFO, - "ALLOC: Allocating from arena (address: %p, capacity: %zu bytes, used: %zu bytes, requested: %zu bytes)", - arena->begin, arena->capacity, current, size); - mem = arena->current; - arena->current = (char*)arena->current + size; + + // align size to 8 + size = (size + 7) & ~((size_t)7); + + size_t used = (size_t)arena->current - (size_t)arena->begin; + size_t available = arena->capacity - used; + + if (available < size) { + if (expand) { + // FIXME(yukkop): All pointers to the arena will be invalidated + // We need to use a virtual memory allocator to avoid this issue + size_t new_capacity = arena->capacity * 2 + size; + raise_message(LOG_LEVEL_WARN, POSITION_INFO, + "ALLOC: Expanding arena (old: %zu, new: %zu)", arena->capacity, new_capacity); + + void *new_mem = malloc(new_capacity); + if (!new_mem) { + raise_message(LOG_LEVEL_WARN, POSITION_INFO, + "ALLOC: Failed to expand arena (requested: %zu bytes)", new_capacity); + return NULL; + } + + memcpy(new_mem, arena->begin, used); + free(arena->begin); + arena->begin = new_mem; + arena->current = (char *)new_mem + used; + arena->capacity = new_capacity; + + raise_message(LOG_LEVEL_WARN, POSITION_INFO, + "ALLOC: Arena expanded successfully (address: %p, capacity: %zu)", new_mem, new_capacity); + } else { + raise_message(LOG_LEVEL_WARN, POSITION_INFO, + "ALLOC: Insufficient memory in arena (address: %p, capacity: %zu bytes, used: %zu bytes, requested: %zu bytes)", + arena->begin, arena->capacity, used, size); + return NULL; + } } - - // Success logging - raise_message(LOG_LEVEL_DEBUG, POSITION_INFO, - "ALLOC: Memory allocated successfully (address: %p, size: %zu bytes)", mem, size); + + void *mem = arena->current; + arena->current = (char*)arena->current + size; + + raise_message(LOG_LEVEL_DEBUG, POSITION_INFO, + "ALLOC: Memory allocated (address: %p, size: %zu)", mem, size); return mem; } @@ -256,7 +282,7 @@ void* arena_alloc__(POSITION_INFO_DECLARATION, Arena *arena, size_t size) { raise_message(LOG_LEVEL_DEBUG, POSITION_INFO, "ALLOC: Allocating memory (arena: %p, size: %zu bytes)", arena, size); - void *mem = arena_alloc_or_null__(POSITION_INFO, arena, size); + void *mem = arena_alloc_or_null__(POSITION_INFO, arena, size, true); if (!mem) { raise_message(LOG_LEVEL_DEBUG, POSITION_INFO, "ALLOC: Allocation failed (arena: %p, requested: %zu bytes)", arena, size); @@ -358,6 +384,39 @@ char* arena_strdup__(POSITION_INFO_DECLARATION, Arena *arena, const char *s) { return result; } +char* arena_strncpy__(POSITION_INFO_DECLARATION, Arena *arena, const char *start, size_t len) { + // Function entry logging + raise_message(LOG_LEVEL_TRACE, POSITION_INFO, + "ALLOC: Copying string (arena: %p, source: %p, length: %zu, preview: %.20s%s)", + arena, start, len, start ? start : "", start && strlen(start) > 20 ? "..." : ""); + + // Check for NULL string + if (!start) { + raise_message(LOG_LEVEL_DEBUG, POSITION_INFO, + "ALLOC: Source string is NULL, returning NULL"); + return NULL; + } + + // Allocate memory for the string plus null terminator + char *result = (char*)arena_alloc__(POSITION_INFO, arena, len + 1); + if (!result) { + raise_message(LOG_LEVEL_DEBUG, POSITION_INFO, + "ALLOC: Memory allocation failed"); + return NULL; + } + + // Copy the string and ensure null termination + strncpy(result, start, len); + result[len] = '\0'; + + // Success logging + raise_message(LOG_LEVEL_DEBUG, POSITION_INFO, + "ALLOC: String copied successfully (result: %p, length: %zu bytes)", + result, len + 1); + + return result; +} + char* arena_repstr__(POSITION_INFO_DECLARATION, Arena *arena, const char *src, size_t start, size_t len, const char *rep) { // Function entry logging @@ -413,6 +472,7 @@ char* arena_repstr__(POSITION_INFO_DECLARATION, Arena *arena, return new_str; } +// FIXME(yukkop): this is who void* arena_realloc_copy__(POSITION_INFO_DECLARATION, Arena *arena, void *old_ptr, size_t old_size, size_t new_size) { void *new_ptr = NULL; @@ -421,7 +481,7 @@ void* arena_realloc_copy__(POSITION_INFO_DECLARATION, Arena *arena, } else if (new_size <= old_size) { new_ptr = old_ptr; } else { - new_ptr = arena_alloc_or_null__(POSITION_INFO, arena, new_size); + new_ptr = arena_alloc_or_null__(POSITION_INFO, arena, new_size, true); if (new_ptr) memcpy(new_ptr, old_ptr, old_size); } @@ -937,6 +997,71 @@ Json *json_get_object_item__(POSITION_INFO_DECLARATION, const Json * const objec return NULL; } +char* slice_to_debug_str__(POSITION_INFO_DECLARATION, Arena *arena, Slice slice) { + // Create complete information about the Slice structure + char buffer_meta[128]; + snprintf(buffer_meta, sizeof(buffer_meta), "Slice{addr=%p, data=%p, len=%zu, isize=%zu, content=", + (void*)&slice, slice.data, slice.len, slice.isize); + + size_t meta_len = strlen(buffer_meta); + + // For NULL data, output a simple message + if (!slice.data) { + char* result = arena_alloc(arena, meta_len + 6); + strcpy(result, buffer_meta); + strcat(result, "NULL}"); + return result; + } + + // Allocate buffer with space for quotes, metadata and null terminator + size_t buffer_size = meta_len + slice.len * 4 + 20; // Extra space for escaping and closing brace + char* buffer = arena_alloc(arena, buffer_size); + + // Copy metadata + strcpy(buffer, buffer_meta); + char* pos = buffer + meta_len; + + *pos++ = '"'; + + // Copy slice data with escaping + for (size_t i = 0; i < slice.len; i++) { + char c = ((char*)slice.data)[i]; + if (c == '\0') { + *pos++ = '\\'; + *pos++ = '0'; + } else if (c == '\n') { + *pos++ = '\\'; + *pos++ = 'n'; + } else if (c == '\r') { + *pos++ = '\\'; + *pos++ = 'r'; + } else if (c == '\t') { + *pos++ = '\\'; + *pos++ = 't'; + } else if (c == '"') { + *pos++ = '\\'; + *pos++ = '"'; + } else if (c == '\\') { + *pos++ = '\\'; + *pos++ = '\\'; + } else if (c < 32 || c > 126) { + // Non-printable characters as hex + pos += sprintf(pos, "\\x%02x", (unsigned char)c); + } else { + *pos++ = c; + } + } + + *pos++ = '"'; + *pos++ = '}'; // Closing brace for the structure + *pos = '\0'; + + raise_message(LOG_LEVEL_TRACE, POSITION_INFO, "slice_to_debug_str: %s", buffer); + + return buffer; +} + + // ----------- // -- slice -- // ----------- @@ -1001,75 +1126,9 @@ int* arena_slice_copy__(POSITION_INFO_DECLARATION, Arena *arena, Slice s) { return copy; } -// ------------ -// -- debug -- -// ------------ +char* json_to_debug_str__(POSITION_INFO_DECLARATION, Arena *arena, Json json) { + raise_message(LOG_LEVEL_TRACE, POSITION_INFO, "json_to_debug_str(, )"); -char* slice_to_debug_str(Arena *arena, Slice slice) { - // Create complete information about the Slice structure - char buffer_meta[128]; - snprintf(buffer_meta, sizeof(buffer_meta), "Slice{addr=%p, data=%p, len=%zu, isize=%zu, content=", - (void*)&slice, slice.data, slice.len, slice.isize); - - size_t meta_len = strlen(buffer_meta); - - // For NULL data, output a simple message - if (!slice.data) { - char* result = arena_alloc(arena, meta_len + 6); - strcpy(result, buffer_meta); - strcat(result, "NULL}"); - return result; - } - - // Allocate buffer with space for quotes, metadata and null terminator - size_t buffer_size = meta_len + slice.len * 4 + 20; // Extra space for escaping and closing brace - char* buffer = arena_alloc(arena, buffer_size); - - // Copy metadata - strcpy(buffer, buffer_meta); - char* pos = buffer + meta_len; - - *pos++ = '"'; - - // Copy slice data with escaping - for (size_t i = 0; i < slice.len; i++) { - char c = ((char*)slice.data)[i]; - if (c == '\0') { - *pos++ = '\\'; - *pos++ = '0'; - } else if (c == '\n') { - *pos++ = '\\'; - *pos++ = 'n'; - } else if (c == '\r') { - *pos++ = '\\'; - *pos++ = 'r'; - } else if (c == '\t') { - *pos++ = '\\'; - *pos++ = 't'; - } else if (c == '"') { - *pos++ = '\\'; - *pos++ = '"'; - } else if (c == '\\') { - *pos++ = '\\'; - *pos++ = '\\'; - } else if (c < 32 || c > 126) { - // Non-printable characters as hex - pos += sprintf(pos, "\\x%02x", (unsigned char)c); - } else { - *pos++ = c; - } - } - - *pos++ = '"'; - *pos++ = '}'; // Closing brace for the structure - *pos = '\0'; - - raise_trace("slice_to_debug_str: %s", buffer); - - return buffer; -} - -char* json_to_debug_str(Arena *arena, Json json) { // Add information about the JSON structure itself char meta_buffer[256]; @@ -1429,106 +1488,415 @@ char* logger_rules_to_string(Arena *arena) { // Look at package\c\hectic\docs\templater.md -TemplateConfig *template_default_config__(POSITION_INFO_DECLARATION, Arena *arena) { - TemplateConfig *config = arena_alloc__(POSITION_INFO, arena, sizeof(TemplateConfig)); - if (!config) return NULL; - - config->open_brace = "{%"; - config->close_brace = "%}"; - config->null_handler = "%%"; - config->section_prefix = "for "; - config->section_suffix = " in "; - config->section_optional_suffix = " join "; - config->section_post_suffix = " do "; - config->interpolation_prefix = ""; - config->include_prefix = "include "; - config->function_prefix = "call "; - +TemplateConfig template_default_config__(POSITION_INFO_DECLARATION) { + raise_message(LOG_LEVEL_TRACE, POSITION_INFO, "TEMPLATE: Default config"); + TemplateConfig config; + + config.Syntax.Braces.open = "{%"; + config.Syntax.Braces.close = "%}"; + config.Syntax.Section.control = "for "; + config.Syntax.Section.source = " in "; + config.Syntax.Section.begin = " do "; + config.Syntax.Interpolate.invoke = ""; + config.Syntax.Include.invoke = "include "; + config.Syntax.Execute.invoke = "exec "; + config.Syntax.nesting = "->"; + return config; } -static TemplateNode *template_node_create__(POSITION_INFO_DECLARATION, Arena *arena, TemplateNodeType type, TemplateValue *value) { - TemplateNode *node = arena_alloc__(POSITION_INFO, arena, sizeof(TemplateNode)); - if (!node) { - raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "Failed to allocate node"); - } - - node->type = type; - node->value = *value; - node->children = NULL; - node->next = NULL; - - return node; -} - #define CHECK_CONFIG_STR(field, name) \ do { \ - if (!(config->field)) { \ - raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "CONFIG: " name " is NULL"); \ + if (config->Syntax.field == NULL) { \ + raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "VALIDATE: " name " is NULL"); \ return false; \ } \ - if (strlen(config->field) > TEMPLATE_MAX_PREFIX_LEN) { \ - raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "CONFIG: " name " is too long"); \ + if (strlen(config->Syntax.field) > TEMPLATE_MAX_PREFIX_LEN) { \ + raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "VALIDATE: " name " is too long"); \ return false; \ } \ } while (0) -bool template_validate_config__(POSITION_INFO_DECLARATION, TemplateConfig *config) { +bool template_validate_config__(POSITION_INFO_DECLARATION, const TemplateConfig *config) { + raise_trace("VALIDATE: config %p", config); if (!config) { - raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "Config is NULL"); + raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "VALIDATE: Config is NULL"); return false; } - CHECK_CONFIG_STR(open_brace, "Open brace"); - CHECK_CONFIG_STR(close_brace, "Close brace"); - CHECK_CONFIG_STR(null_handler, "Null handler"); - CHECK_CONFIG_STR(section_prefix, "Section prefix"); - CHECK_CONFIG_STR(section_suffix, "Section suffix"); - CHECK_CONFIG_STR(section_optional_suffix, "Section optional suffix"); - CHECK_CONFIG_STR(section_post_suffix, "Section post suffix"); - CHECK_CONFIG_STR(interpolation_prefix, "Interpolation prefix"); - CHECK_CONFIG_STR(include_prefix, "Include prefix"); - CHECK_CONFIG_STR(function_prefix, "Function prefix"); + assert(config->Syntax.Braces.open != NULL); + assert(config->Syntax.Braces.close != NULL); + assert(config->Syntax.Section.control != NULL); + assert(config->Syntax.Section.source != NULL); + assert(config->Syntax.Section.begin != NULL); + assert(config->Syntax.Interpolate.invoke != NULL); + assert(config->Syntax.Include.invoke != NULL); + assert(config->Syntax.Execute.invoke != NULL); + assert(config->Syntax.nesting != NULL); + + CHECK_CONFIG_STR(Braces.open, "Open brace"); + CHECK_CONFIG_STR(Braces.close, "Close brace"); + CHECK_CONFIG_STR(Section.control, "Section control"); + CHECK_CONFIG_STR(Section.source, "Section source"); + CHECK_CONFIG_STR(Section.begin, "Section begin"); + CHECK_CONFIG_STR(Interpolate.invoke, "Interpolation invoke"); + CHECK_CONFIG_STR(Include.invoke, "Include invoke"); + CHECK_CONFIG_STR(Execute.invoke, "Execute invoke"); + CHECK_CONFIG_STR(nesting, "Nesting"); return true; } -TemplateNode *template_parse__(POSITION_INFO_DECLARATION, Arena *arena, const char *template, TemplateConfig *config) { - if (!arena) { - raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "Arena is NULL"); - } - - if (!config) { - raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "Config is NULL"); - } - - if (!template) { - raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "Template is NULL"); +#undef CHECK_CONFIG_STR + +#define TEMPLATE_ASSERT_SYNTAX(pattern, message_arg, code_arg) \ + if (strncmp(*s, pattern, strlen(pattern))) { \ + raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "PARSE: " message_arg); \ + result->error.code = code_arg; \ + result->error.message = message_arg; \ + return result; \ } - // Find the first open brace - const char *open_brace = strstr(template, config->open_brace); - if (!open_brace) { - raise_message(LOG_LEVEL_LOG, POSITION_INFO, "No open brace found"); - TemplateValue val = {.text = {.content = (char *)template}}; - return template_node_create__(POSITION_INFO, arena, - TEMPLATE_NODE_TEXT, &val); +TemplateResult *template_parse__(POSITION_INFO_DECLARATION, Arena *arena, const char **s, const TemplateConfig *config); + +TemplateResult *template_parse_interpolation__(POSITION_INFO_DECLARATION, Arena *arena, const char **s_ptr, const TemplateConfig *config) { + raise_message(LOG_LEVEL_TRACE, POSITION_INFO, "PARSE: Interpolation"); + + TemplateResult *result = arena_alloc__(POSITION_INFO, arena, sizeof(TemplateResult)); + + const char **s = s_ptr; + + // Skip to the content of the interpolation + *s += strlen(config->Syntax.Braces.open); + *s = skip_whitespace(*s); + *s += strlen(config->Syntax.Interpolate.invoke); + + *s = skip_whitespace(*s); + const char *key_start = *s; + + while (isalnum(**s)) { + if (**s == ' ' || strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close))) break; + TEMPLATE_ASSERT_SYNTAX(config->Syntax.Braces.open, "Nested tag in interpolation", TEMPLATE_ERROR_NESTED_INTERPOLATION); + + (*s)++; } - // Deside tag type by prefix - const char *tag_prefix = open_brace + strlen(config->open_brace); - if (strncmp(tag_prefix, config->section_prefix, strlen(config->section_prefix)) == 0) { - // Section tag - } else if (strncmp(tag_prefix, config->interpolation_prefix, strlen(config->interpolation_prefix)) == 0) { - // Interpolation tag - } else if (strncmp(tag_prefix, config->include_prefix, strlen(config->include_prefix)) == 0) { - // Include tag - } else if (strncmp(tag_prefix, config->function_prefix, strlen(config->function_prefix)) == 0) { - // Function tag - } else { - raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "Unknown tag prefix: %s", slice_create__(POSITION_INFO, 1, (char *)tag_prefix, strlen(tag_prefix), 0, TEMPLATE_MAX_PREFIX_LEN)); + size_t key_len = *s - key_start; + result->node.value.interpolate.key = arena_strncpy__(POSITION_INFO, arena, key_start, key_len); + + result->node.type = TEMPLATE_NODE_INTERPOLATE; + + *s_ptr = *s + strlen(config->Syntax.Braces.close); + + return result; +} + +TemplateResult *template_parse_section__(POSITION_INFO_DECLARATION, Arena *arena, const char **s_ptr, const TemplateConfig *config) { + raise_message(LOG_LEVEL_TRACE, POSITION_INFO, "PARSE: Section"); + + TemplateResult *result = arena_alloc__(POSITION_INFO, arena, sizeof(TemplateResult)); + result->node.type = TEMPLATE_NODE_SECTION; + + const char **s = s_ptr; + + // Skip to the content of the section + *s += strlen(config->Syntax.Braces.open); + *s = skip_whitespace(*s); + *s += strlen(config->Syntax.Section.control); + + // Find the iterator name + *s = skip_whitespace(*s); + const char *iterator_start = *s; + + while (isalnum(**s)) { + if (**s == ' ' || **s == '\n' || **s == '\t' || strncmp(*s, config->Syntax.Section.source, strlen(config->Syntax.Section.source))) break; + TEMPLATE_ASSERT_SYNTAX(config->Syntax.Braces.close, "Unexpected section end", TEMPLATE_ERROR_UNEXPECTED_SECTION_END); + TEMPLATE_ASSERT_SYNTAX(config->Syntax.Braces.open, "Nested tag in section element name", TEMPLATE_ERROR_NESTED_SECTION_ITERATOR); + + (*s)++; + } + + size_t iterator_len = *s - iterator_start; + result->node.value.section.iterator = arena_strncpy__(POSITION_INFO, arena, iterator_start, iterator_len); + + // Find the collection name + *s = skip_whitespace(*s); + const char *collection_start = *s; + + while (isalnum(**s)) { + if (**s == ' ' || **s == '\n' || **s == '\t' || strncmp(*s, config->Syntax.Section.begin, strlen(config->Syntax.Section.begin))) break; + TEMPLATE_ASSERT_SYNTAX(config->Syntax.Braces.close, "Unexpected section end", TEMPLATE_ERROR_UNEXPECTED_SECTION_END); + TEMPLATE_ASSERT_SYNTAX(config->Syntax.Braces.open, "Nested tag in section iterator", TEMPLATE_ERROR_NESTED_SECTION_ITERATOR); + + (*s)++; + } + + size_t collection_len = *s - collection_start; + result->node.value.section.collection = arena_strncpy__(POSITION_INFO, arena, collection_start, collection_len); + + // Parse the body + TemplateResult *body_result = template_parse__(POSITION_INFO, arena, s, config); + if (body_result->error.code != TEMPLATE_ERROR_NONE) { + return body_result; + } + + result->node.value.section.body = &body_result->node; + + *s_ptr = *s + strlen(config->Syntax.Braces.close); + + return result; +} + +TemplateResult *template_parse_include__(POSITION_INFO_DECLARATION, Arena *arena, const char **s_ptr, const TemplateConfig *config) { + raise_message(LOG_LEVEL_TRACE, POSITION_INFO, "PARSE: Include"); + TemplateResult *result = arena_alloc__(POSITION_INFO, arena, sizeof(TemplateResult)); + result->node.type = TEMPLATE_NODE_INCLUDE; + + const char **s = s_ptr; + + // Skip to the content of the include + *s += strlen(config->Syntax.Braces.open); + *s = skip_whitespace(*s); + *s += strlen(config->Syntax.Include.invoke); + + *s = skip_whitespace(*s); + const char *include_start = *s; + + while (isalnum(**s)) { + if (**s == ' ' || **s == '\n' || **s == '\t' || strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close))) break; + TEMPLATE_ASSERT_SYNTAX(config->Syntax.Braces.open, "Nested tag in include", TEMPLATE_ERROR_NESTED_INCLUDE); + + (*s)++; + } + + size_t include_len = *s - include_start; + result->node.value.include.key = arena_strncpy__(POSITION_INFO, arena, include_start, include_len); + + *s_ptr = *s + strlen(config->Syntax.Braces.close); + + return result; +} + +TemplateResult *template_parse_execute__(POSITION_INFO_DECLARATION, Arena *arena, const char **s_ptr, const TemplateConfig *config) { + raise_message(LOG_LEVEL_TRACE, POSITION_INFO, "PARSE: Execute"); + + TemplateResult *result = arena_alloc__(POSITION_INFO, arena, sizeof(TemplateResult)); + result->node.type = TEMPLATE_NODE_EXECUTE; + + const char **s = s_ptr; + + *s += strlen(config->Syntax.Braces.open); + *s = skip_whitespace(*s); + *s += strlen(config->Syntax.Execute.invoke); + + *s = skip_whitespace(*s); + const char *code_start = *s; + + while (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close))) { + TEMPLATE_ASSERT_SYNTAX(config->Syntax.Braces.open, "Nested tag in execute", TEMPLATE_ERROR_NESTED_EXECUTE); + (*s)++; + } + + size_t code_len = *s - code_start; + result->node.value.execute.code = arena_strncpy__(POSITION_INFO, arena, code_start, code_len); + + *s_ptr = *s + strlen(config->Syntax.Braces.close); + + return result; +} + +TemplateResult *template_parse__(POSITION_INFO_DECLARATION, Arena *arena, const char **s, const TemplateConfig *config) { + raise_message(LOG_LEVEL_TRACE, POSITION_INFO, "PARSE: Iteration start"); + + if (!template_validate_config__(POSITION_INFO, config)) { + raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "PARSE: Invalid config"); return NULL; } - return NULL; -} \ No newline at end of file + if (!arena) { + raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "PARSE: Arena is NULL"); + return NULL; + } + + assert(config->Syntax.Braces.open != NULL); + + const char *start = *s; + + TemplateNode *root = arena_alloc__(POSITION_INFO, arena, sizeof(TemplateNode)); + TemplateNode *current = root; + + int open_brace_len = strlen(config->Syntax.Braces.open); + + while (*s) { + // Find the first open brace + if (strncmp(*s, config->Syntax.Braces.open, open_brace_len) == 0) { + // Add text node if there is any text before the tag + if (start != *s) { + raise_message(LOG_LEVEL_TRACE, POSITION_INFO, "PARSE: Text node: %s", arena_strncpy__(POSITION_INFO, DISPOSABLE_ARENA, start, *s - start)); + current->type = TEMPLATE_NODE_TEXT; + current->value.text.content = arena_strncpy__(POSITION_INFO, arena, start, *s - start); + } + + // Deside tag type by prefix + TemplateResult *current_result = arena_alloc__(POSITION_INFO, arena, sizeof(TemplateResult)); + { + raise_message(LOG_LEVEL_TRACE, POSITION_INFO, "PARSE: Found tag"); + + const char *tag_prefix = *s + open_brace_len; + tag_prefix = skip_whitespace(tag_prefix); + raise_trace("tag_prefix: %p", tag_prefix); + assert(tag_prefix != NULL); + assert(config->Syntax.Section.control != NULL); + assert(config->Syntax.Interpolate.invoke != NULL); + assert(config->Syntax.Include.invoke != NULL); + assert(config->Syntax.Execute.invoke != NULL); + assert(config->Syntax.nesting != NULL); + + if (strncmp(tag_prefix, config->Syntax.Section.control, strlen(config->Syntax.Section.control)) == 0) { + raise_message(LOG_LEVEL_TRACE, POSITION_INFO, "PARSE: Section tag"); + current_result = template_parse_section__(POSITION_INFO, arena, s, config); + } else if (strncmp(tag_prefix, config->Syntax.Interpolate.invoke, strlen(config->Syntax.Interpolate.invoke)) == 0) { + raise_message(LOG_LEVEL_TRACE, POSITION_INFO, "PARSE: Interpolation tag"); + current_result = template_parse_interpolation__(POSITION_INFO, arena, s, config); + } else if (strncmp(tag_prefix, config->Syntax.Include.invoke, strlen(config->Syntax.Include.invoke)) == 0) { + raise_message(LOG_LEVEL_TRACE, POSITION_INFO, "PARSE: Include tag"); + current_result = template_parse_include__(POSITION_INFO, arena, s, config); + } else if (strncmp(tag_prefix, config->Syntax.Execute.invoke, strlen(config->Syntax.Execute.invoke)) == 0) { + raise_message(LOG_LEVEL_TRACE, POSITION_INFO, "PARSE: Execute tag"); + current_result = template_parse_execute__(POSITION_INFO, arena, s, config); + } else { + raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "PARSE: Unknown tag prefix: %s", slice_create__(POSITION_INFO, 1, (char *)tag_prefix, strlen(tag_prefix), 0, TEMPLATE_MAX_PREFIX_LEN)); + + TemplateResult *error_result = arena_alloc__(POSITION_INFO, arena, sizeof(TemplateResult)); + + error_result->error.code = TEMPLATE_ERROR_UNKNOWN_TAG; + error_result->error.message = "Unknown tag prefix"; + + return error_result; + } + } + + if (current_result->error.code != TEMPLATE_ERROR_NONE) { + return current_result; + } + + *current = current_result->node; + current->next = arena_alloc__(POSITION_INFO, arena, sizeof(TemplateNode)); + current = current->next; + } + + (*s)++; + } + + // Add text node if there is any text after the last tag + if (start != *s) { + current->type = TEMPLATE_NODE_TEXT; + current->value.text.content = arena_strncpy__(POSITION_INFO, arena, start, *s - start); + } + + TemplateResult *result = arena_alloc__(POSITION_INFO, arena, sizeof(TemplateResult)); + result->node = *root; + + return result; +} + +#undef TEMPLATE_ASSERT_SYNTAX + +#define TEMPLATE_NODE_MAX_DEBUG_DEPTH 20 + +static char *template_node_type_to_string(TemplateNodeType type) { + switch (type) { + case TEMPLATE_NODE_SECTION: return "SECTION"; + case TEMPLATE_NODE_INTERPOLATE: return "INTERPOLATE"; + case TEMPLATE_NODE_EXECUTE: return "EXECUTE"; + case TEMPLATE_NODE_INCLUDE: return "INCLUDE"; + case TEMPLATE_NODE_TEXT: return "TEXT"; + default: return "UNKNOWN"; + } +} + +char *template_node_to_debug_str__(POSITION_INFO_DECLARATION, Arena *arena, const TemplateNode *node, int depth) { + if (!node) return arena_strncpy__(POSITION_INFO, arena, "", 0); + + if (depth > TEMPLATE_NODE_MAX_DEBUG_DEPTH) { + return arena_strncpy__(POSITION_INFO, arena, "...", 3); + } + + // Use a temporary buffer on the stack for building the string + char temp_buf[MEM_MiB]; + size_t len = 0; + + #define APPEND(...) do { \ + int written = snprintf(temp_buf + len, sizeof(temp_buf) - len, ##__VA_ARGS__); \ + if (written < 0) return NULL; \ + len += written; \ + } while (0) + + if (depth == 0) { + APPEND("["); + } + + APPEND("{\"type\":\"%s\",", template_node_type_to_string(node->type)); + + switch (node->type) { + case TEMPLATE_NODE_SECTION: + APPEND("\"content\":{\"iterator\":\"%s\",\"collection\"=\"%s\"}", + node->value.section.iterator, + node->value.section.collection); + char *body_str = template_node_to_debug_str__(POSITION_INFO, arena, node->value.section.body, depth + 1); + if (body_str) { + APPEND(",\"body\":%s", body_str); + } + break; + case TEMPLATE_NODE_INTERPOLATE: + APPEND("\"content\":{\"key\":\"%s\"}", node->value.interpolate.key); + break; + case TEMPLATE_NODE_EXECUTE: + APPEND("\"content\":{\"code\":\"%s\"}", node->value.execute.code); + break; + case TEMPLATE_NODE_INCLUDE: + APPEND("\"content\":{\"key\":\"%s\"}", node->value.include.key); + break; + case TEMPLATE_NODE_TEXT: + APPEND("\"content\":{\"content\":\"%s\"}", node->value.text.content); + break; + default: + break; + } + APPEND("}"); + + if (node->error.code != TEMPLATE_ERROR_NONE) { + APPEND("\"error\":{\"code\":%d,\"message\":\"%s\"}", node->error.code, node->error.message); + } + + if (node->children) { + APPEND("\"children\":["); + char *child_str = template_node_to_debug_str__(POSITION_INFO, arena, node->children, depth + 1); + if (child_str) { + APPEND(",%s", child_str); + } + APPEND("]"); + } + + if (node->next) { + char *next_str = template_node_to_debug_str__(POSITION_INFO, arena, node->next, depth + 1); + if (next_str) { + APPEND(",%s", next_str); + } + } + + if (depth == 0) { + APPEND("]"); + } + + // Copy the final string to arena-allocated memory + char *result = arena_strncpy__(POSITION_INFO, arena, temp_buf, len); + return result; +} + +// --------- +// -- End -- +// --------- + +#undef POSITION_INFO_DECLARATION +#undef POSITION_INFO diff --git a/package/c/hectic/hectic.h b/package/c/hectic/hectic.h index 124dd7a..c294b6a 100644 --- a/package/c/hectic/hectic.h +++ b/package/c/hectic/hectic.h @@ -275,8 +275,9 @@ typedef struct { Arena arena_init__(const char *file, const char *func, int line, size_t size); -void* arena_alloc_or_null__(const char *file, const char *func, int line, Arena *arena, size_t size); +void* arena_alloc_or_null__(const char *file, const char *func, int line, Arena *arena, size_t size, bool expand); +// FIXME(yukkop): ptr % 8 == 0 void* arena_alloc__(const char *file, const char *func, int line, Arena *arena, size_t size); void arena_reset__(const char *file, const char *func, int line, Arena *arena); @@ -291,10 +292,12 @@ char* arena_repstr__(const char *file, const char *func, int line, Arena *arena, void* arena_realloc_copy__(const char *file, const char *func, int line, Arena *arena, void *old_ptr, size_t old_size, size_t new_size); +char* arena_strncpy__(const char *file, const char *func, int line, Arena *arena, const char *start, size_t len); + // NOTE(yukkop): This macro is used to define procedures so that `__LINE__` and `__FILE__` // in `raise_debug` reflect the location where the macro is called, not where it's defined. #define arena_alloc_or_null(arena, size) \ - arena_alloc_or_null__(__FILE__, __func__, __LINE__, arena, size) + arena_alloc_or_null__(__FILE__, __func__, __LINE__, arena, size, false) #define arena_init(size) \ arena_init__(__FILE__, __func__, __LINE__, size) @@ -317,6 +320,8 @@ void* arena_realloc_copy__(const char *file, const char *func, int line, Arena * #define arena_realloc_copy(arena, old_ptr, old_size, new_size) \ arena_realloc_copy__(__FILE__, __func__, __LINE__, arena, old_ptr, old_size, new_size) +#define arena_strncpy(arena, src, len) \ + arena_strncpy__(__FILE__, __func__, __LINE__, arena, src, len) static Arena disposable_arena __attribute__((unused)) = {0}; @@ -329,6 +334,28 @@ static Arena disposable_arena __attribute__((unused)) = {0}; &disposable_arena; \ }) +// ------------ +// -- Debug -- +// ------------ + +#define DEBUGSTR(arena, type, value) DEBUGSTR_##type(arena, value) + +#define DEBUGSTR_Slice(arena, value) slice_to_debug_str(arena, value) +#define DEBUGSTR_Json(arena, value) json_to_debug_str(arena, value) + +/** + * Print all current logging rules to stderr for debugging + */ +void logger_print_rules(); + +/** + * Dump all active logging rules into a string + * + * @param arena Memory arena to allocate the string in + * @return String representation of all rules, or NULL on error + */ +char* logger_rules_to_string(Arena *arena); + // ---------- // -- Json -- // ---------- @@ -373,6 +400,10 @@ char *json_to_string_with_opts__(const char* file, const char* func, int line, A #define json_get_object_item(object, key) json_get_object_item__(__FILE__, __func__, __LINE__, object, key) Json *json_get_object_item__(const char* file, const char* func, int line, const Json * const object, const char * const key); +char* json_to_debug_str__(const char* file, const char* func, int line, Arena *arena, Json json); + +#define json_to_debug_str(arena, json) json_to_debug_str__(__FILE__, __func__, __LINE__, arena, json) + // ----------- // -- Slice -- // ----------- @@ -419,31 +450,9 @@ int* arena_slice_copy__(const char *file, const char *func, int line, Arena *are buf; \ }) -// ------------ -// -- Debug -- -// ------------ +char* slice_to_debug_str__(const char* file, const char* func, int line, Arena *arena, Slice slice); -// Utility functions for debug output of Slice and Json structures -char* slice_to_debug_str(Arena *arena, Slice slice); -char* json_to_debug_str(Arena *arena, Json json); - -#define DEBUGSTR(arena, type, value) DEBUGSTR_##type(arena, value) - -#define DEBUGSTR_Slice(arena, value) slice_to_debug_str(arena, value) -#define DEBUGSTR_Json(arena, value) json_to_debug_str(arena, value) - -/** - * Print all current logging rules to stderr for debugging - */ -void logger_print_rules(); - -/** - * Dump all active logging rules into a string - * - * @param arena Memory arena to allocate the string in - * @return String representation of all rules, or NULL on error - */ -char* logger_rules_to_string(Arena *arena); +#define slice_to_debug_str(arena, slice) slice_to_debug_str__(__FILE__, __func__, __LINE__, arena, slice) // --------------- // -- Templater -- @@ -454,43 +463,53 @@ typedef enum { TEMPLATE_NODE_INTERPOLATE, // Variable interpolation TEMPLATE_NODE_SECTION, // Section (for loops) TEMPLATE_NODE_INCLUDE, // Include other templates - TEMPLATE_NODE_FUNCTION // Function call (for future use) + TEMPLATE_NODE_EXECUTE, // Execute code } TemplateNodeType; #define TEMPLATE_MAX_PREFIX_LEN 16 typedef struct { - const char *open_brace; // Default: "{%" - const char *close_brace; // Default: "%}" - const char *null_handler; // Default: "%%" - const char *section_prefix; // default: "for " - const char *section_suffix; // default: " in " - const char *section_optional_suffix; // default: " join " - const char *section_post_suffix; // default: " do " - const char *interpolation_prefix; // default: "" - const char *include_prefix; // default: "include " - const char *function_prefix; // default: "call " + struct { + struct { + const char *open; // Default: "{%" + const char *close; // Default: "%}" + } Braces; + struct { + const char *control; // default: "for " + const char *source; // default: " in " + const char *begin; // default: " do " + } Section; + struct { + const char *invoke; // default: "" + } Interpolate; + struct { + const char *invoke; // default: "include " + } Include; + struct { + const char *invoke; // default: "exec " + } Execute; + const char *nesting; // default: "->" + } Syntax; } TemplateConfig; +typedef struct TemplateNode TemplateNode; // forward declaration + typedef struct { - char *variable; + char *iterator; char *collection; - char *join; - struct TemplateNode *null_block; + TemplateNode *body; } TemplateSectionValue; typedef struct { - char *variable; - struct TemplateNode *null_block; + char *key; } TemplateInterpolateValue; typedef struct { - char *name; - char *args; -} TemplateFunctionValue; + char *code; +} TemplateExecuteValue; typedef struct { - char *name; + char *key; } TemplateIncludeValue; typedef struct { @@ -500,17 +519,49 @@ typedef struct { typedef union { TemplateSectionValue section; TemplateInterpolateValue interpolate; - TemplateFunctionValue function; + TemplateExecuteValue execute; TemplateIncludeValue include; TemplateTextValue text; } TemplateValue; -// template node structure -typedef struct TemplateNode { +typedef enum { + TEMPLATE_ERROR_NONE, + TEMPLATE_ERROR_UNKNOWN_TAG, + TEMPLATE_ERROR_NESTED_INTERPOLATION, + TEMPLATE_ERROR_NESTED_SECTION_ITERATOR, + TEMPLATE_ERROR_UNEXPECTED_SECTION_END, + TEMPLATE_ERROR_NESTED_INCLUDE, + TEMPLATE_ERROR_NESTED_EXECUTE, +} TemplateErrorCode; + +typedef struct { + TemplateErrorCode code; + char *message; +} TemplateError; + +struct TemplateNode { + TemplateError error; TemplateNodeType type; TemplateValue value; - struct TemplateNode *children; // child nodes - struct TemplateNode *next; // sibling nodes -} TemplateNode; + TemplateNode *children; // child nodes + TemplateNode *next; // sibling nodes +}; + +typedef union { + TemplateError error; + TemplateNode node; +} TemplateResult; + +TemplateResult *template_parse__(const char *file, const char *func, int line, Arena *arena, const char **s, const TemplateConfig *config); + +char *template_node_to_debug_str__(const char *file, const char *func, int line, Arena *arena, const TemplateNode *node, int depth); + +TemplateConfig template_default_config__(const char *file, const char *func, int line); + +#define template_parse(arena, s, config) template_parse__(__FILE__, __func__, __LINE__, arena, s, config) + +#define template_node_to_debug_str(arena, node) template_node_to_debug_str__(__FILE__, __func__, __LINE__, arena, node, 0) + +#define template_default_config() template_default_config__(__FILE__, __func__, __LINE__) #endif // EPRINTF_H \ No newline at end of file diff --git a/package/c/hectic/make.sh b/package/c/hectic/make.sh index 5e10c95..d678631 100644 --- a/package/c/hectic/make.sh +++ b/package/c/hectic/make.sh @@ -51,7 +51,7 @@ esac # Default flags RUN_TESTS=1 OPTFLAGS="-O2" -CFLAGS="-Wall -Wextra -Werror -pedantic -fsanitize=address" +CFLAGS="-Wall -Wextra -Werror -pedantic -fsanitize=address " LDFLAGS="-lhectic" STD_FLAGS="-std=c99" COLOR_FLAG="" @@ -71,7 +71,7 @@ while [ $# -gt 0 ]; do echo "Error: Required dependency '$dep' not found." >&2 exit 1 fi - OPTFLAGS="-O0 -gdwarf-2 -g3" + OPTFLAGS="-O0 -gdwarf-2 -g3 -Wno-error" DEBUG=1 ;; --color) @@ -108,7 +108,7 @@ case "$MODE" in for test_file in test/*.c; do exe="target/test/$(basename "${test_file%.c}")" # shellcheck disable=SC2086 - cc $CFLAGS $OPTFLAGS -pedantic -I. "$test_file" -Ltarget -lhectic $LDFLAGS -o "$exe" + cc $CFLAGS $OPTFLAGS -I. "$test_file" -Ltarget -lhectic $LDFLAGS -o "$exe" if [ "$?" -ne 0 ]; then exit 1 fi diff --git a/package/c/hectic/test/04-templater.c b/package/c/hectic/test/04-templater.c index 9a79564..a954154 100755 --- a/package/c/hectic/test/04-templater.c +++ b/package/c/hectic/test/04-templater.c @@ -3,178 +3,87 @@ #include #include #include "hectic.h" -#include "templater.h" #define ARENA_SIZE 1024 * 1024 -// Test 1: Basic interpolation -static void test_basic_interpolation(void) { - Arena arena = arena_init(ARENA_SIZE); - - // Initialize template config - TemplateConfig *config = template_config_init(&arena); - assert(config != NULL); - - // Create test data - const char *json_str = "{\"name\":\"John\",\"age\":30}"; - Json *data = json_parse(&arena, &json_str); - assert(data != NULL); - - // Create template context - TemplateContext *ctx = template_context_init(&arena, data, config); - assert(ctx != NULL); - - // Parse template - const char *template = "Hello {% name %}, you are {% age %} years old."; - TemplateNode *root = template_parse(&arena, template, config); - assert(root != NULL); - - // Render template - char *result = template_render(&arena, root, ctx); - assert(result != NULL); - assert(strcmp(result, "Hello John, you are 30 years old.") == 0); - - arena_free(&arena); +static char *remove_all_spaces(char *s) { + char *new_s = NULL; + while (*s) { + if (*s != ' ' && *s != '\t' && *s != '\n') { + new_s = s; + } + s++; + } + return new_s; } -// Test 2: Section (loop) with join -static void test_section_with_join(void) { - Arena arena = arena_init(ARENA_SIZE); - - // Initialize template config - TemplateConfig *config = template_config_init(&arena); - assert(config != NULL); - - // Create test data - const char *json_str = "{\"items\":[\"apple\",\"banana\",\"orange\"]}"; - Json *data = json_parse(&arena, &json_str); - assert(data != NULL); - - // Create template context - TemplateContext *ctx = template_context_init(&arena, data, config); - assert(ctx != NULL); - - // Parse template - const char *template = "{% for item in items join ', ' do %}{% item %}{% %}"; - TemplateNode *root = template_parse(&arena, template, config); - assert(root != NULL); - - // Render template - char *result = template_render(&arena, root, ctx); - assert(result != NULL); - assert(strcmp(result, "apple, banana, orange") == 0); - - arena_free(&arena); +static void test_template_node_to_debug_str(Arena *arena) { + TemplateNode *root = arena_alloc(arena, sizeof(TemplateNode)); + root->type = TEMPLATE_NODE_TEXT; + root->value.text.content = arena_strncpy(arena, "Hello", 5); + + root->next = arena_alloc(arena, sizeof(TemplateNode)); + root->next->type = TEMPLATE_NODE_INTERPOLATE; + root->next->value.interpolate.key = arena_strncpy(arena, "name", 4); + + root->next->next = arena_alloc(arena, sizeof(TemplateNode)); + root->next->next->type = TEMPLATE_NODE_TEXT; + root->next->next->value.text.content = arena_strncpy(arena, "!", 1); + + char *debug_str = template_node_to_debug_str(arena, root); + + raise_notice("debug_str: %s", debug_str); + assert(strcmp( + remove_all_spaces(debug_str), + remove_all_spaces("" \ + "[" \ + " {" \ + " \"type\":\"TEXT\"," \ + " \"content\":{" \ + " \"content\":\"Hello\"" \ + " }" \ + " }," \ + " {" \ + " \"type\":\"INTERPOLATE\"," \ + " \"content\":{" \ + " \"key\":\"name\"" \ + " }" \ + " }," \ + " {" \ + " \"type\":\"TEXT\"," \ + " \"content\":{" \ + " \"content\":\"!\"" \ + " }" \ + " }" \ + "]")) == 0); } -// Test 3: Nested sections -static void test_nested_sections(void) { - Arena arena = arena_init(ARENA_SIZE); - - // Initialize template config - TemplateConfig *config = template_config_init(&arena); - assert(config != NULL); - - // Create test data - const char *json_str = "{\"users\":[{\"name\":\"John\",\"roles\":[\"admin\",\"user\"]},{\"name\":\"Jane\",\"roles\":[\"user\"]}]}"; - Json *data = json_parse(&arena, &json_str); - assert(data != NULL); - - // Create template context - TemplateContext *ctx = template_context_init(&arena, data, config); - assert(ctx != NULL); - - // Parse template - const char *template = "{% for user in users do %}{% user.name %}: {% for role in user.roles join ', ' do %}{% role %}{% %}\n{% %}"; - TemplateNode *root = template_parse(&arena, template, config); - assert(root != NULL); - - // Render template - char *result = template_render(&arena, root, ctx); - assert(result != NULL); - assert(strcmp(result, "John: admin, user\nJane: user\n") == 0); - - arena_free(&arena); -} +static void test_template_parse(Arena *arena, TemplateConfig *config) { + const char *template = "Hello {% name %}!"; + TemplateResult *result = template_parse(arena, &template, config); -// Test 4: Null handling -static void test_null_handling(void) { - Arena arena = arena_init(ARENA_SIZE); - - // Initialize template config - TemplateConfig *config = template_config_init(&arena); - assert(config != NULL); - - // Create test data - const char *json_str = "{\"name\":\"John\",\"age\":null}"; - Json *data = json_parse(&arena, &json_str); - assert(data != NULL); - - // Create template context - TemplateContext *ctx = template_context_init(&arena, data, config); - assert(ctx != NULL); - - // Parse template - const char *template = "Name: {% name %}\nAge: {% age %%}unknown{% %}"; - TemplateNode *root = template_parse(&arena, template, config); - assert(root != NULL); - - // Render template - char *result = template_render(&arena, root, ctx); - assert(result != NULL); - assert(strcmp(result, "Name: John\nAge: unknown") == 0); - - arena_free(&arena); -} - -// Test 5: Complex template with mixed content -static void test_complex_template(void) { - Arena arena = arena_init(ARENA_SIZE); - - // Initialize template config - TemplateConfig *config = template_config_init(&arena); - assert(config != NULL); - - // Create test data - const char *json_str = "{\"title\":\"Shopping List\",\"items\":[{\"name\":\"Milk\",\"quantity\":2},{\"name\":\"Bread\",\"quantity\":1}],\"notes\":\"Don't forget the eggs!\"}"; - Json *data = json_parse(&arena, &json_str); - assert(data != NULL); - - // Create template context - TemplateContext *ctx = template_context_init(&arena, data, config); - assert(ctx != NULL); - - // Parse template - const char *template = "Title: {% title %}\n\nItems:\n{% for item in items do %}- {% item.name %} ({% item.quantity %})\n{% %}\n\nNotes: {% notes %}"; - TemplateNode *root = template_parse(&arena, template, config); - assert(root != NULL); - - // Render template - char *result = template_render(&arena, root, ctx); - assert(result != NULL); - assert(strcmp(result, "Title: Shopping List\n\nItems:\n- Milk (2)\n- Bread (1)\n\nNotes: Don't forget the eggs!") == 0); - - arena_free(&arena); + raise_notice("result: %s", template_node_to_debug_str(DISPOSABLE_ARENA, &result->node)); + assert(result->error.code == TEMPLATE_ERROR_NONE); } int main(void) { - printf("Running template parser tests...\n"); - - test_basic_interpolation(); - printf("Test 1: Basic interpolation passed\n"); - - test_section_with_join(); - printf("Test 2: Section with join passed\n"); - - test_nested_sections(); - printf("Test 3: Nested sections passed\n"); - - test_null_handling(); - printf("Test 4: Null handling passed\n"); - - test_complex_template(); - printf("Test 5: Complex template passed\n"); - - printf("All tests passed!\n"); + init_logger(); + + Arena arena = arena_init(ARENA_SIZE); + + TemplateConfig config = template_default_config(); + + printf("%sRunning template parser tests...%s\n", OPTIONAL_COLOR(COLOR_GREEN), OPTIONAL_COLOR(COLOR_RESET)); + + test_template_node_to_debug_str(&arena); + printf("%sTest 0: template_node_to_debug_str passed%s\n", OPTIONAL_COLOR(COLOR_GREEN), OPTIONAL_COLOR(COLOR_RESET)); + arena_reset(&arena); + + test_template_parse(&arena, &config); + printf("%sTest 1: template_parse passed%s\n", OPTIONAL_COLOR(COLOR_GREEN), OPTIONAL_COLOR(COLOR_RESET)); + arena_reset(&arena); + + arena_free(&arena); + printf("%s%s all tests passed.%s\n", OPTIONAL_COLOR(COLOR_GREEN), __FILE__, OPTIONAL_COLOR(COLOR_RESET)); return 0; } \ No newline at end of file