From cda65a56eb8b4aac83ec39f7ea3589b3cabf4565 Mon Sep 17 00:00:00 2001 From: yukkop Date: Mon, 7 Apr 2025 17:57:44 +0000 Subject: [PATCH] docs: `hectic C`: types for template parser --- package/c/hectic/docs/templater.md | 155 +++++++++++++++++++++++ package/c/hectic/hectic.c | 119 +++++++++++++++++- package/c/hectic/hectic.h | 68 ++++++++++ package/c/hectic/test/03-slice.c | 2 +- package/c/hectic/test/04-templater.c | 180 +++++++++++++++++++++++++++ 5 files changed, 519 insertions(+), 5 deletions(-) create mode 100755 package/c/hectic/docs/templater.md create mode 100755 package/c/hectic/test/04-templater.c diff --git a/package/c/hectic/docs/templater.md b/package/c/hectic/docs/templater.md new file mode 100755 index 0000000..ab542eb --- /dev/null +++ b/package/c/hectic/docs/templater.md @@ -0,0 +1,155 @@ +# Templater Configuration Documentation + +The templating engine supports flexible customization of tag syntax parameters. Each parameter can be overridden in the configuration file. Below are the main groups of parameters with usage examples and configuration notes. + +## Legend + +--- + +## General Parameters + +- **Open Brace** + A non-empty string marking the beginning of a tag. + *Example:* `{%` + +- **Close Brace** + 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 + +Parameters defining syntax for blocks controlling loops or nested structures. + +- **Prefix** + Marks the start of a section (e.g., loops). + *Example:* `for ` | *(Empty)* + +- **Suffix** + 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 ` | `:` + +*Section Example:* +```tpl +{% for item in items do + {% item.name %} + some text + {% for inner_item in item.inner_items join '\n' do +

some other text

+ {% inner_item %} + %% +

Sorry, list is empty

+ %} + \n +%} +``` +*Context Example:* +```json +{ + "items": [ + { + "name": "some name", + "inner_items": ["value1", "value2"] + } + ] +} +``` + +## Interpolation Tags +Inserts variable values or expression results directly into templates. +- **Prefix** + Indicates an interpolation operation. + *Example:* `$` | *(Empty)* + +*Interpolation Example:* +```tpl + {% interpolation_field %} + {% interpolation_field_null %% Sorry, empty %} + {% interpolation_field_null %% {% interpolation_field %} %} +``` +*Context Example:* +```json +{ + "interpolation_field": "some value", + "interpolation_field_null": null +} +``` + +## Include Tags +Includes content from other templates. +- **Prefix** + Marks the tag as an inclusion operation. + *Example:* `include ` | `+` | `<` + +*Include Example:* +```tpl + text before + {% include inner_template %} + +``` +*Context Examples:* +```json +// Separate context +{ + "include inner_template": [ + { + "template": "{% field %}", + "context": { "field": "value" } + } + ] +} + +// Shared root context +{ + "field": "value", + "include inner_template": [ + { + "template": "{% field %}" + } + ] +} + +// Plain text inclusion +{ + "field": "value", + "include inner_template": [ + { + "content": "

value

" + } + ] +} +``` + +## Function Tags +**Note:** Currently not included in C library; implemented as a wrapper on applicable platforms. +Enables calling functions with arguments. +- **Prefix** + Denotes a function call. + *Example:* `call` | *(Empty)* + +*Function Example:* +```tpl + {% call my_function(arg1, arg2, 'literal') %} +``` + +## Notes +- **Unique Tags:** `Open Brace`, `Close Brace`, and `Null Handler` must be distinct. +- **Nested Constructs:** Supported. +- **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. + diff --git a/package/c/hectic/hectic.c b/package/c/hectic/hectic.c index ee7a955..a32267c 100644 --- a/package/c/hectic/hectic.c +++ b/package/c/hectic/hectic.c @@ -61,6 +61,8 @@ void set_output_color_mode(ColorMode mode) { color_mode = mode; } +#define POSITION_INFO file, func, line + // ------------ // -- Logger -- // ------------ @@ -497,6 +499,8 @@ static const char *skip_whitespace(const char *s) { return s; } +static Json *json_parse_value__(const char *file, const char *func, int line, const char **s, Arena *arena); + /* Parse a JSON string (does not handle full escaping) */ static char *json_parse_string__(const char *file, const char *func, int line, const char **s_ptr, Arena *arena) { const char *s = *s_ptr; @@ -542,9 +546,6 @@ static double json_parse_number__(const char *file, const char *func, int line, return num; } -/* Forward declaration */ -static Json *json_parse_value__(const char *file, const char *func, int line, const char **s, Arena *arena); - /* Parse a JSON array: [ value, value, ... ] */ static Json *json_parse_array__(const char *file, const char *func, int line, const char **s, Arena *arena) { raise_message(LOG_LEVEL_DEBUG, file, func, line, "Entering json_parse_array__ at position: %p", *s); @@ -1419,4 +1420,114 @@ char* logger_rules_to_string(Arena *arena) { } return buffer; -} \ No newline at end of file +} + +// --------------- +// -- Templater -- +// --------------- + +// Look at package\c\hectic\docs\templater.md + +TemplateConfig *template_default_config__(const char *file, const char *func, int line, Arena *arena) { + TemplateConfig *config = arena_alloc__(file, func, line, 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 "; + + return config; +} + +static TemplateNode *template_node_create__(const char *file, const char *func, int line, Arena *arena, TemplateNodeType type, TemplateValue *value) { + TemplateNode *node = arena_alloc__(file, func, line, arena, sizeof(TemplateNode)); + if (!node) { + raise_message(LOG_LEVEL_EXCEPTION, file, func, line, "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, file, func, line, "CONFIG: " name " is NULL"); \ + return false; \ + } \ + if (strlen(config->field) > TEMPLATE_MAX_PREFIX_LEN) { \ + raise_message(LOG_LEVEL_EXCEPTION, file, func, line, "CONFIG: " name " is too long"); \ + return false; \ + } \ +} while (0) + +bool template_validate_config__(const char *file, const char *func, int line, TemplateConfig *config) { + if (!config) { + raise_message(LOG_LEVEL_EXCEPTION, file, func, line, "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"); + + return true; +} + +TemplateNode *template_parse__(const char *file, const char *func, int line, Arena *arena, const char *template, TemplateConfig *config) { + if (!arena) { + raise_message(LOG_LEVEL_EXCEPTION, file, func, line, "Arena is NULL"); + } + + if (!config) { + raise_message(LOG_LEVEL_EXCEPTION, file, func, line, "Config is NULL"); + } + + if (!template) { + raise_message(LOG_LEVEL_EXCEPTION, file, func, line, "Template is NULL"); + } + + // Find the first open brace + const char *open_brace = strstr(template, config->open_brace); + if (!open_brace) { + raise_message(LOG_LEVEL_LOG, file, func, line, "No open brace found"); + TemplateValue val = {.text = {.content = (char *)template}}; + return template_node_create__(file, func, line, arena, + TEMPLATE_NODE_TEXT, &val); + } + + // 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, file, func, line, "Unknown tag prefix: %s", slice_create__(file, func, line, 1, (char *)tag_prefix, strlen(tag_prefix), 0, TEMPLATE_MAX_PREFIX_LEN)); + return NULL; + } + + return NULL; +} diff --git a/package/c/hectic/hectic.h b/package/c/hectic/hectic.h index 266ea16..124dd7a 100644 --- a/package/c/hectic/hectic.h +++ b/package/c/hectic/hectic.h @@ -445,4 +445,72 @@ void logger_print_rules(); */ char* logger_rules_to_string(Arena *arena); +// --------------- +// -- Templater -- +// --------------- + +typedef enum { + TEMPLATE_NODE_TEXT, // Plain text content + 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) +} 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 " +} TemplateConfig; + +typedef struct { + char *variable; + char *collection; + char *join; + struct TemplateNode *null_block; +} TemplateSectionValue; + +typedef struct { + char *variable; + struct TemplateNode *null_block; +} TemplateInterpolateValue; + +typedef struct { + char *name; + char *args; +} TemplateFunctionValue; + +typedef struct { + char *name; +} TemplateIncludeValue; + +typedef struct { + char *content; +} TemplateTextValue; + +typedef union { + TemplateSectionValue section; + TemplateInterpolateValue interpolate; + TemplateFunctionValue function; + TemplateIncludeValue include; + TemplateTextValue text; +} TemplateValue; + +// template node structure +typedef struct TemplateNode { + TemplateNodeType type; + TemplateValue value; + struct TemplateNode *children; // child nodes + struct TemplateNode *next; // sibling nodes +} TemplateNode; + #endif // EPRINTF_H \ No newline at end of file diff --git a/package/c/hectic/test/03-slice.c b/package/c/hectic/test/03-slice.c index 7d34c22..27c031e 100644 --- a/package/c/hectic/test/03-slice.c +++ b/package/c/hectic/test/03-slice.c @@ -93,4 +93,4 @@ int main() { 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 diff --git a/package/c/hectic/test/04-templater.c b/package/c/hectic/test/04-templater.c new file mode 100755 index 0000000..9a79564 --- /dev/null +++ b/package/c/hectic/test/04-templater.c @@ -0,0 +1,180 @@ +#include +#include +#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); +} + +// 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); +} + +// 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); +} + +// 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) { + 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"); + return 0; +} \ No newline at end of file