feat: hectic C: template parser, template to json
This commit is contained in:
@@ -16,10 +16,6 @@ The templating engine supports flexible customization of tag syntax parameters.
|
|||||||
A non-empty string marking the end of a tag.
|
A non-empty string marking the end of a tag.
|
||||||
*Example:* `%}`
|
*Example:* `%}`
|
||||||
|
|
||||||
- **Null Handler**
|
|
||||||
A non-empty string used to specify alternative content when a value is null.
|
|
||||||
*Example:* `%%`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Section Tags
|
## Section Tags
|
||||||
@@ -34,10 +30,6 @@ Parameters defining syntax for blocks controlling loops or nested structures.
|
|||||||
Delimiter between variables and collections.
|
Delimiter between variables and collections.
|
||||||
*Example:* ` in ` | `#`
|
*Example:* ` in ` | `#`
|
||||||
|
|
||||||
- **Optional Suffix**
|
|
||||||
Additional modifier, e.g., for joining collections.
|
|
||||||
*Example:* ` join ` | `#`
|
|
||||||
|
|
||||||
- **Post-Suffix**
|
- **Post-Suffix**
|
||||||
Finalizes the section declaration block.
|
Finalizes the section declaration block.
|
||||||
*Example:* `do ` | `:`
|
*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
|
{% for inner_item in item.inner_items join '\n' do
|
||||||
<p>some other text</p>
|
<p>some other text</p>
|
||||||
{% inner_item %}
|
{% inner_item %}
|
||||||
%%
|
|
||||||
<h1>Sorry, list is empty</h1>
|
|
||||||
%}
|
%}
|
||||||
\n
|
\n
|
||||||
%}
|
%}
|
||||||
@@ -77,8 +67,6 @@ Inserts variable values or expression results directly into templates.
|
|||||||
*Interpolation Example:*
|
*Interpolation Example:*
|
||||||
```tpl
|
```tpl
|
||||||
{% interpolation_field %}
|
{% interpolation_field %}
|
||||||
{% interpolation_field_null %% Sorry, empty %}
|
|
||||||
{% interpolation_field_null %% {% interpolation_field %} %}
|
|
||||||
```
|
```
|
||||||
*Context Example:*
|
*Context Example:*
|
||||||
```json
|
```json
|
||||||
@@ -138,11 +126,12 @@ Includes content from other templates.
|
|||||||
Enables calling functions with arguments.
|
Enables calling functions with arguments.
|
||||||
- **Prefix**
|
- **Prefix**
|
||||||
Denotes a function call.
|
Denotes a function call.
|
||||||
*Example:* `call` | *(Empty)*
|
*Example:* `exec` | *(Empty)*
|
||||||
|
|
||||||
*Function Example:*
|
*Function Example:*
|
||||||
```tpl
|
```tpl
|
||||||
{% call my_function(arg1, arg2, 'literal') %}
|
{% exec my_function(arg1, arg2, 'literal') %}
|
||||||
|
{% exec RETURN 'aaaaa' %}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
@@ -152,4 +141,3 @@ Enables calling functions with arguments.
|
|||||||
- **Missing Fields/Functions/Templates:** Configurable to either return an error or warning.
|
- **Missing Fields/Functions/Templates:** Configurable to either return an error or warning.
|
||||||
- **Circular Includes:** Detect when possible.
|
- **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.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "hectic.h"
|
#include "hectic.h"
|
||||||
#include <fnmatch.h>
|
#include <fnmatch.h>
|
||||||
#include <string.h> // For strdup, strchr, etc.
|
#include <string.h>
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
// On systems without strsep, provide a custom implementation
|
// On systems without strsep, provide a custom implementation
|
||||||
#ifndef _GNU_SOURCE
|
#ifndef _GNU_SOURCE
|
||||||
@@ -170,14 +171,16 @@ char* raise_message(
|
|||||||
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &tm_info);
|
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &tm_info);
|
||||||
|
|
||||||
// Print timestamp, log level with color, location 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,
|
timeStr,
|
||||||
log_level_to_color(level),
|
log_level_to_color(level),
|
||||||
log_level_to_string(level),
|
log_level_to_string(level),
|
||||||
OPTIONAL_COLOR(COLOR_RESET),
|
OPTIONAL_COLOR(COLOR_RESET),
|
||||||
file,
|
file,
|
||||||
func,
|
func,
|
||||||
line);
|
OPTIONAL_COLOR(COLOR_GREEN),
|
||||||
|
line,
|
||||||
|
OPTIONAL_COLOR(COLOR_RESET));
|
||||||
|
|
||||||
// Print the actual message with variable arguments
|
// Print the actual message with variable arguments
|
||||||
va_list args;
|
va_list args;
|
||||||
@@ -219,35 +222,58 @@ Arena arena_init__(POSITION_INFO_DECLARATION, size_t size) {
|
|||||||
return arena;
|
return arena;
|
||||||
}
|
}
|
||||||
|
|
||||||
void* arena_alloc_or_null__(POSITION_INFO_DECLARATION, Arena *arena, size_t size) {
|
void* arena_alloc_or_null__(POSITION_INFO_DECLARATION, Arena *arena, size_t size, bool expand) {
|
||||||
// Function entry at TRACE level
|
|
||||||
raise_message(LOG_LEVEL_TRACE, POSITION_INFO,
|
raise_message(LOG_LEVEL_TRACE, POSITION_INFO,
|
||||||
"ALLOC: Requesting memory from arena (arena: %p, size: %zu bytes)", arena, size);
|
"ALLOC: Requesting memory from arena (arena: %p, size: %zu bytes)", arena, size);
|
||||||
|
|
||||||
void *mem = NULL;
|
|
||||||
if (arena->begin == 0) {
|
if (arena->begin == 0) {
|
||||||
raise_message(LOG_LEVEL_DEBUG, POSITION_INFO,
|
raise_message(LOG_LEVEL_DEBUG, POSITION_INFO,
|
||||||
"ALLOC: Arena not initialized, creating new arena");
|
"ALLOC: Arena not initialized, creating new arena");
|
||||||
*arena = arena_init__(POSITION_INFO, 1024); // ARENA_DEFAULT_SIZE assumed as 1024
|
*arena = arena_init__(POSITION_INFO, 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t current = (size_t)arena->current - (size_t)arena->begin;
|
// align size to 8
|
||||||
if (arena->capacity <= current || arena->capacity - current < size) {
|
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,
|
raise_message(LOG_LEVEL_WARN, POSITION_INFO,
|
||||||
"ALLOC: Insufficient memory in arena (address: %p, capacity: %zu bytes, used: %zu bytes, requested: %zu bytes)",
|
"ALLOC: Insufficient memory in arena (address: %p, capacity: %zu bytes, used: %zu bytes, requested: %zu bytes)",
|
||||||
arena->begin, arena->capacity, current, size);
|
arena->begin, arena->capacity, used, size);
|
||||||
return NULL;
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success logging
|
void *mem = arena->current;
|
||||||
|
arena->current = (char*)arena->current + size;
|
||||||
|
|
||||||
raise_message(LOG_LEVEL_DEBUG, POSITION_INFO,
|
raise_message(LOG_LEVEL_DEBUG, POSITION_INFO,
|
||||||
"ALLOC: Memory allocated successfully (address: %p, size: %zu bytes)", mem, size);
|
"ALLOC: Memory allocated (address: %p, size: %zu)", mem, size);
|
||||||
return mem;
|
return mem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +282,7 @@ void* arena_alloc__(POSITION_INFO_DECLARATION, Arena *arena, size_t size) {
|
|||||||
raise_message(LOG_LEVEL_DEBUG, POSITION_INFO,
|
raise_message(LOG_LEVEL_DEBUG, POSITION_INFO,
|
||||||
"ALLOC: Allocating memory (arena: %p, size: %zu bytes)", arena, size);
|
"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) {
|
if (!mem) {
|
||||||
raise_message(LOG_LEVEL_DEBUG, POSITION_INFO,
|
raise_message(LOG_LEVEL_DEBUG, POSITION_INFO,
|
||||||
"ALLOC: Allocation failed (arena: %p, requested: %zu bytes)", arena, size);
|
"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;
|
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,
|
char* arena_repstr__(POSITION_INFO_DECLARATION, Arena *arena,
|
||||||
const char *src, size_t start, size_t len, const char *rep) {
|
const char *src, size_t start, size_t len, const char *rep) {
|
||||||
// Function entry logging
|
// Function entry logging
|
||||||
@@ -413,6 +472,7 @@ char* arena_repstr__(POSITION_INFO_DECLARATION, Arena *arena,
|
|||||||
return new_str;
|
return new_str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME(yukkop): this is who
|
||||||
void* arena_realloc_copy__(POSITION_INFO_DECLARATION, Arena *arena,
|
void* arena_realloc_copy__(POSITION_INFO_DECLARATION, Arena *arena,
|
||||||
void *old_ptr, size_t old_size, size_t new_size) {
|
void *old_ptr, size_t old_size, size_t new_size) {
|
||||||
void *new_ptr = NULL;
|
void *new_ptr = NULL;
|
||||||
@@ -421,7 +481,7 @@ void* arena_realloc_copy__(POSITION_INFO_DECLARATION, Arena *arena,
|
|||||||
} else if (new_size <= old_size) {
|
} else if (new_size <= old_size) {
|
||||||
new_ptr = old_ptr;
|
new_ptr = old_ptr;
|
||||||
} else {
|
} 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)
|
if (new_ptr)
|
||||||
memcpy(new_ptr, old_ptr, old_size);
|
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;
|
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 --
|
// -- slice --
|
||||||
// -----------
|
// -----------
|
||||||
@@ -1001,75 +1126,9 @@ int* arena_slice_copy__(POSITION_INFO_DECLARATION, Arena *arena, Slice s) {
|
|||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------
|
char* json_to_debug_str__(POSITION_INFO_DECLARATION, Arena *arena, Json json) {
|
||||||
// -- debug --
|
raise_message(LOG_LEVEL_TRACE, POSITION_INFO, "json_to_debug_str(<optimized>, <optimized>)");
|
||||||
// ------------
|
|
||||||
|
|
||||||
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
|
// Add information about the JSON structure itself
|
||||||
char meta_buffer[256];
|
char meta_buffer[256];
|
||||||
|
|
||||||
@@ -1429,106 +1488,415 @@ char* logger_rules_to_string(Arena *arena) {
|
|||||||
|
|
||||||
// Look at package\c\hectic\docs\templater.md
|
// Look at package\c\hectic\docs\templater.md
|
||||||
|
|
||||||
TemplateConfig *template_default_config__(POSITION_INFO_DECLARATION, Arena *arena) {
|
TemplateConfig template_default_config__(POSITION_INFO_DECLARATION) {
|
||||||
TemplateConfig *config = arena_alloc__(POSITION_INFO, arena, sizeof(TemplateConfig));
|
raise_message(LOG_LEVEL_TRACE, POSITION_INFO, "TEMPLATE: Default config");
|
||||||
if (!config) return NULL;
|
TemplateConfig config;
|
||||||
|
|
||||||
config->open_brace = "{%";
|
config.Syntax.Braces.open = "{%";
|
||||||
config->close_brace = "%}";
|
config.Syntax.Braces.close = "%}";
|
||||||
config->null_handler = "%%";
|
config.Syntax.Section.control = "for ";
|
||||||
config->section_prefix = "for ";
|
config.Syntax.Section.source = " in ";
|
||||||
config->section_suffix = " in ";
|
config.Syntax.Section.begin = " do ";
|
||||||
config->section_optional_suffix = " join ";
|
config.Syntax.Interpolate.invoke = "";
|
||||||
config->section_post_suffix = " do ";
|
config.Syntax.Include.invoke = "include ";
|
||||||
config->interpolation_prefix = "";
|
config.Syntax.Execute.invoke = "exec ";
|
||||||
config->include_prefix = "include ";
|
config.Syntax.nesting = "->";
|
||||||
config->function_prefix = "call ";
|
|
||||||
|
|
||||||
return config;
|
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) \
|
#define CHECK_CONFIG_STR(field, name) \
|
||||||
do { \
|
do { \
|
||||||
if (!(config->field)) { \
|
if (config->Syntax.field == NULL) { \
|
||||||
raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "CONFIG: " name " is NULL"); \
|
raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "VALIDATE: " name " is NULL"); \
|
||||||
return false; \
|
return false; \
|
||||||
} \
|
} \
|
||||||
if (strlen(config->field) > TEMPLATE_MAX_PREFIX_LEN) { \
|
if (strlen(config->Syntax.field) > TEMPLATE_MAX_PREFIX_LEN) { \
|
||||||
raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "CONFIG: " name " is too long"); \
|
raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "VALIDATE: " name " is too long"); \
|
||||||
return false; \
|
return false; \
|
||||||
} \
|
} \
|
||||||
} while (0)
|
} 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) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
CHECK_CONFIG_STR(open_brace, "Open brace");
|
assert(config->Syntax.Braces.open != NULL);
|
||||||
CHECK_CONFIG_STR(close_brace, "Close brace");
|
assert(config->Syntax.Braces.close != NULL);
|
||||||
CHECK_CONFIG_STR(null_handler, "Null handler");
|
assert(config->Syntax.Section.control != NULL);
|
||||||
CHECK_CONFIG_STR(section_prefix, "Section prefix");
|
assert(config->Syntax.Section.source != NULL);
|
||||||
CHECK_CONFIG_STR(section_suffix, "Section suffix");
|
assert(config->Syntax.Section.begin != NULL);
|
||||||
CHECK_CONFIG_STR(section_optional_suffix, "Section optional suffix");
|
assert(config->Syntax.Interpolate.invoke != NULL);
|
||||||
CHECK_CONFIG_STR(section_post_suffix, "Section post suffix");
|
assert(config->Syntax.Include.invoke != NULL);
|
||||||
CHECK_CONFIG_STR(interpolation_prefix, "Interpolation prefix");
|
assert(config->Syntax.Execute.invoke != NULL);
|
||||||
CHECK_CONFIG_STR(include_prefix, "Include prefix");
|
assert(config->Syntax.nesting != NULL);
|
||||||
CHECK_CONFIG_STR(function_prefix, "Function prefix");
|
|
||||||
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
TemplateNode *template_parse__(POSITION_INFO_DECLARATION, Arena *arena, const char *template, TemplateConfig *config) {
|
#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; \
|
||||||
|
}
|
||||||
|
|
||||||
|
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)++;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
if (!arena) {
|
if (!arena) {
|
||||||
raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "Arena is NULL");
|
raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "PARSE: Arena is NULL");
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config) {
|
assert(config->Syntax.Braces.open != NULL);
|
||||||
raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "Config is NULL");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!template) {
|
const char *start = *s;
|
||||||
raise_message(LOG_LEVEL_EXCEPTION, POSITION_INFO, "Template is NULL");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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
|
// Find the first open brace
|
||||||
const char *open_brace = strstr(template, config->open_brace);
|
if (strncmp(*s, config->Syntax.Braces.open, open_brace_len) == 0) {
|
||||||
if (!open_brace) {
|
// Add text node if there is any text before the tag
|
||||||
raise_message(LOG_LEVEL_LOG, POSITION_INFO, "No open brace found");
|
if (start != *s) {
|
||||||
TemplateValue val = {.text = {.content = (char *)template}};
|
raise_message(LOG_LEVEL_TRACE, POSITION_INFO, "PARSE: Text node: %s", arena_strncpy__(POSITION_INFO, DISPOSABLE_ARENA, start, *s - start));
|
||||||
return template_node_create__(POSITION_INFO, arena,
|
current->type = TEMPLATE_NODE_TEXT;
|
||||||
TEMPLATE_NODE_TEXT, &val);
|
current->value.text.content = arena_strncpy__(POSITION_INFO, arena, start, *s - start);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deside tag type by prefix
|
// Deside tag type by prefix
|
||||||
const char *tag_prefix = open_brace + strlen(config->open_brace);
|
TemplateResult *current_result = arena_alloc__(POSITION_INFO, arena, sizeof(TemplateResult));
|
||||||
if (strncmp(tag_prefix, config->section_prefix, strlen(config->section_prefix)) == 0) {
|
{
|
||||||
// Section tag
|
raise_message(LOG_LEVEL_TRACE, POSITION_INFO, "PARSE: Found tag");
|
||||||
} else if (strncmp(tag_prefix, config->interpolation_prefix, strlen(config->interpolation_prefix)) == 0) {
|
|
||||||
// Interpolation tag
|
const char *tag_prefix = *s + open_brace_len;
|
||||||
} else if (strncmp(tag_prefix, config->include_prefix, strlen(config->include_prefix)) == 0) {
|
tag_prefix = skip_whitespace(tag_prefix);
|
||||||
// Include tag
|
raise_trace("tag_prefix: %p", tag_prefix);
|
||||||
} else if (strncmp(tag_prefix, config->function_prefix, strlen(config->function_prefix)) == 0) {
|
assert(tag_prefix != NULL);
|
||||||
// Function tag
|
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 {
|
} 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));
|
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));
|
||||||
return NULL;
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NULL;
|
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
|
||||||
|
|||||||
@@ -275,8 +275,9 @@ typedef struct {
|
|||||||
|
|
||||||
Arena arena_init__(const char *file, const char *func, int line, size_t size);
|
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_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);
|
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* arena_realloc_copy__(const char *file, const char *func, int line, Arena *arena,
|
||||||
void *old_ptr, size_t old_size, size_t new_size);
|
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__`
|
// 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.
|
// in `raise_debug` reflect the location where the macro is called, not where it's defined.
|
||||||
#define arena_alloc_or_null(arena, size) \
|
#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) \
|
#define arena_init(size) \
|
||||||
arena_init__(__FILE__, __func__, __LINE__, 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) \
|
#define arena_realloc_copy(arena, old_ptr, old_size, new_size) \
|
||||||
arena_realloc_copy__(__FILE__, __func__, __LINE__, 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};
|
static Arena disposable_arena __attribute__((unused)) = {0};
|
||||||
|
|
||||||
@@ -329,6 +334,28 @@ static Arena disposable_arena __attribute__((unused)) = {0};
|
|||||||
&disposable_arena; \
|
&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 --
|
// -- 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)
|
#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);
|
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 --
|
// -- Slice --
|
||||||
// -----------
|
// -----------
|
||||||
@@ -419,31 +450,9 @@ int* arena_slice_copy__(const char *file, const char *func, int line, Arena *are
|
|||||||
buf; \
|
buf; \
|
||||||
})
|
})
|
||||||
|
|
||||||
// ------------
|
char* slice_to_debug_str__(const char* file, const char* func, int line, Arena *arena, Slice slice);
|
||||||
// -- Debug --
|
|
||||||
// ------------
|
|
||||||
|
|
||||||
// Utility functions for debug output of Slice and Json structures
|
#define slice_to_debug_str(arena, slice) slice_to_debug_str__(__FILE__, __func__, __LINE__, arena, slice)
|
||||||
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);
|
|
||||||
|
|
||||||
// ---------------
|
// ---------------
|
||||||
// -- Templater --
|
// -- Templater --
|
||||||
@@ -454,43 +463,53 @@ typedef enum {
|
|||||||
TEMPLATE_NODE_INTERPOLATE, // Variable interpolation
|
TEMPLATE_NODE_INTERPOLATE, // Variable interpolation
|
||||||
TEMPLATE_NODE_SECTION, // Section (for loops)
|
TEMPLATE_NODE_SECTION, // Section (for loops)
|
||||||
TEMPLATE_NODE_INCLUDE, // Include other templates
|
TEMPLATE_NODE_INCLUDE, // Include other templates
|
||||||
TEMPLATE_NODE_FUNCTION // Function call (for future use)
|
TEMPLATE_NODE_EXECUTE, // Execute code
|
||||||
} TemplateNodeType;
|
} TemplateNodeType;
|
||||||
|
|
||||||
#define TEMPLATE_MAX_PREFIX_LEN 16
|
#define TEMPLATE_MAX_PREFIX_LEN 16
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const char *open_brace; // Default: "{%"
|
struct {
|
||||||
const char *close_brace; // Default: "%}"
|
struct {
|
||||||
const char *null_handler; // Default: "%%"
|
const char *open; // Default: "{%"
|
||||||
const char *section_prefix; // default: "for "
|
const char *close; // Default: "%}"
|
||||||
const char *section_suffix; // default: " in "
|
} Braces;
|
||||||
const char *section_optional_suffix; // default: " join "
|
struct {
|
||||||
const char *section_post_suffix; // default: " do "
|
const char *control; // default: "for "
|
||||||
const char *interpolation_prefix; // default: ""
|
const char *source; // default: " in "
|
||||||
const char *include_prefix; // default: "include "
|
const char *begin; // default: " do "
|
||||||
const char *function_prefix; // default: "call "
|
} 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;
|
} TemplateConfig;
|
||||||
|
|
||||||
|
typedef struct TemplateNode TemplateNode; // forward declaration
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char *variable;
|
char *iterator;
|
||||||
char *collection;
|
char *collection;
|
||||||
char *join;
|
TemplateNode *body;
|
||||||
struct TemplateNode *null_block;
|
|
||||||
} TemplateSectionValue;
|
} TemplateSectionValue;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char *variable;
|
char *key;
|
||||||
struct TemplateNode *null_block;
|
|
||||||
} TemplateInterpolateValue;
|
} TemplateInterpolateValue;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char *name;
|
char *code;
|
||||||
char *args;
|
} TemplateExecuteValue;
|
||||||
} TemplateFunctionValue;
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char *name;
|
char *key;
|
||||||
} TemplateIncludeValue;
|
} TemplateIncludeValue;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -500,17 +519,49 @@ typedef struct {
|
|||||||
typedef union {
|
typedef union {
|
||||||
TemplateSectionValue section;
|
TemplateSectionValue section;
|
||||||
TemplateInterpolateValue interpolate;
|
TemplateInterpolateValue interpolate;
|
||||||
TemplateFunctionValue function;
|
TemplateExecuteValue execute;
|
||||||
TemplateIncludeValue include;
|
TemplateIncludeValue include;
|
||||||
TemplateTextValue text;
|
TemplateTextValue text;
|
||||||
} TemplateValue;
|
} TemplateValue;
|
||||||
|
|
||||||
// template node structure
|
typedef enum {
|
||||||
typedef struct TemplateNode {
|
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;
|
TemplateNodeType type;
|
||||||
TemplateValue value;
|
TemplateValue value;
|
||||||
struct TemplateNode *children; // child nodes
|
TemplateNode *children; // child nodes
|
||||||
struct TemplateNode *next; // sibling nodes
|
TemplateNode *next; // sibling nodes
|
||||||
} TemplateNode;
|
};
|
||||||
|
|
||||||
|
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
|
#endif // EPRINTF_H
|
||||||
@@ -71,7 +71,7 @@ while [ $# -gt 0 ]; do
|
|||||||
echo "Error: Required dependency '$dep' not found." >&2
|
echo "Error: Required dependency '$dep' not found." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
OPTFLAGS="-O0 -gdwarf-2 -g3"
|
OPTFLAGS="-O0 -gdwarf-2 -g3 -Wno-error"
|
||||||
DEBUG=1
|
DEBUG=1
|
||||||
;;
|
;;
|
||||||
--color)
|
--color)
|
||||||
@@ -108,7 +108,7 @@ case "$MODE" in
|
|||||||
for test_file in test/*.c; do
|
for test_file in test/*.c; do
|
||||||
exe="target/test/$(basename "${test_file%.c}")"
|
exe="target/test/$(basename "${test_file%.c}")"
|
||||||
# shellcheck disable=SC2086
|
# 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
|
if [ "$?" -ne 0 ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -3,178 +3,87 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include "hectic.h"
|
#include "hectic.h"
|
||||||
#include "templater.h"
|
|
||||||
|
|
||||||
#define ARENA_SIZE 1024 * 1024
|
#define ARENA_SIZE 1024 * 1024
|
||||||
|
|
||||||
// Test 1: Basic interpolation
|
static char *remove_all_spaces(char *s) {
|
||||||
static void test_basic_interpolation(void) {
|
char *new_s = NULL;
|
||||||
Arena arena = arena_init(ARENA_SIZE);
|
while (*s) {
|
||||||
|
if (*s != ' ' && *s != '\t' && *s != '\n') {
|
||||||
// Initialize template config
|
new_s = s;
|
||||||
TemplateConfig *config = template_config_init(&arena);
|
}
|
||||||
assert(config != NULL);
|
s++;
|
||||||
|
}
|
||||||
// Create test data
|
return new_s;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 2: Section (loop) with join
|
static void test_template_node_to_debug_str(Arena *arena) {
|
||||||
static void test_section_with_join(void) {
|
TemplateNode *root = arena_alloc(arena, sizeof(TemplateNode));
|
||||||
Arena arena = arena_init(ARENA_SIZE);
|
root->type = TEMPLATE_NODE_TEXT;
|
||||||
|
root->value.text.content = arena_strncpy(arena, "Hello", 5);
|
||||||
|
|
||||||
// Initialize template config
|
root->next = arena_alloc(arena, sizeof(TemplateNode));
|
||||||
TemplateConfig *config = template_config_init(&arena);
|
root->next->type = TEMPLATE_NODE_INTERPOLATE;
|
||||||
assert(config != NULL);
|
root->next->value.interpolate.key = arena_strncpy(arena, "name", 4);
|
||||||
|
|
||||||
// Create test data
|
root->next->next = arena_alloc(arena, sizeof(TemplateNode));
|
||||||
const char *json_str = "{\"items\":[\"apple\",\"banana\",\"orange\"]}";
|
root->next->next->type = TEMPLATE_NODE_TEXT;
|
||||||
Json *data = json_parse(&arena, &json_str);
|
root->next->next->value.text.content = arena_strncpy(arena, "!", 1);
|
||||||
assert(data != NULL);
|
|
||||||
|
|
||||||
// Create template context
|
char *debug_str = template_node_to_debug_str(arena, root);
|
||||||
TemplateContext *ctx = template_context_init(&arena, data, config);
|
|
||||||
assert(ctx != NULL);
|
|
||||||
|
|
||||||
// Parse template
|
raise_notice("debug_str: %s", debug_str);
|
||||||
const char *template = "{% for item in items join ', ' do %}{% item %}{% %}";
|
assert(strcmp(
|
||||||
TemplateNode *root = template_parse(&arena, template, config);
|
remove_all_spaces(debug_str),
|
||||||
assert(root != NULL);
|
remove_all_spaces("" \
|
||||||
|
"[" \
|
||||||
// Render template
|
" {" \
|
||||||
char *result = template_render(&arena, root, ctx);
|
" \"type\":\"TEXT\"," \
|
||||||
assert(result != NULL);
|
" \"content\":{" \
|
||||||
assert(strcmp(result, "apple, banana, orange") == 0);
|
" \"content\":\"Hello\"" \
|
||||||
|
" }" \
|
||||||
arena_free(&arena);
|
" }," \
|
||||||
|
" {" \
|
||||||
|
" \"type\":\"INTERPOLATE\"," \
|
||||||
|
" \"content\":{" \
|
||||||
|
" \"key\":\"name\"" \
|
||||||
|
" }" \
|
||||||
|
" }," \
|
||||||
|
" {" \
|
||||||
|
" \"type\":\"TEXT\"," \
|
||||||
|
" \"content\":{" \
|
||||||
|
" \"content\":\"!\"" \
|
||||||
|
" }" \
|
||||||
|
" }" \
|
||||||
|
"]")) == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 3: Nested sections
|
static void test_template_parse(Arena *arena, TemplateConfig *config) {
|
||||||
static void test_nested_sections(void) {
|
const char *template = "Hello {% name %}!";
|
||||||
Arena arena = arena_init(ARENA_SIZE);
|
TemplateResult *result = template_parse(arena, &template, config);
|
||||||
|
|
||||||
// Initialize template config
|
raise_notice("result: %s", template_node_to_debug_str(DISPOSABLE_ARENA, &result->node));
|
||||||
TemplateConfig *config = template_config_init(&arena);
|
assert(result->error.code == TEMPLATE_ERROR_NONE);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
printf("Running template parser tests...\n");
|
init_logger();
|
||||||
|
|
||||||
test_basic_interpolation();
|
Arena arena = arena_init(ARENA_SIZE);
|
||||||
printf("Test 1: Basic interpolation passed\n");
|
|
||||||
|
|
||||||
test_section_with_join();
|
TemplateConfig config = template_default_config();
|
||||||
printf("Test 2: Section with join passed\n");
|
|
||||||
|
|
||||||
test_nested_sections();
|
printf("%sRunning template parser tests...%s\n", OPTIONAL_COLOR(COLOR_GREEN), OPTIONAL_COLOR(COLOR_RESET));
|
||||||
printf("Test 3: Nested sections passed\n");
|
|
||||||
|
|
||||||
test_null_handling();
|
test_template_node_to_debug_str(&arena);
|
||||||
printf("Test 4: Null handling passed\n");
|
printf("%sTest 0: template_node_to_debug_str passed%s\n", OPTIONAL_COLOR(COLOR_GREEN), OPTIONAL_COLOR(COLOR_RESET));
|
||||||
|
arena_reset(&arena);
|
||||||
|
|
||||||
test_complex_template();
|
test_template_parse(&arena, &config);
|
||||||
printf("Test 5: Complex template passed\n");
|
printf("%sTest 1: template_parse passed%s\n", OPTIONAL_COLOR(COLOR_GREEN), OPTIONAL_COLOR(COLOR_RESET));
|
||||||
|
arena_reset(&arena);
|
||||||
|
|
||||||
printf("All tests passed!\n");
|
arena_free(&arena);
|
||||||
|
printf("%s%s all tests passed.%s\n", OPTIONAL_COLOR(COLOR_GREEN), __FILE__, OPTIONAL_COLOR(COLOR_RESET));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user