feat: hectic C: logging settings
This commit is contained in:
@@ -140,7 +140,7 @@
|
|||||||
shells = self.devShells.${system};
|
shells = self.devShells.${system};
|
||||||
in {
|
in {
|
||||||
c = pkgs.mkShell {
|
c = pkgs.mkShell {
|
||||||
buildInputs = (with pkgs; [ inotify-tools gdb gcc ]) ++ (with self.packages.${system}; [ hectic nvim-pager watch ]);
|
buildInputs = (with pkgs; [ inotify-tools gdb gcc ]) ++ (with self.packages.${system}; [ c-hectic nvim-pager watch ]);
|
||||||
PAGER = "${self.packages.${system}.nvim-pager}/bin/pager";
|
PAGER = "${self.packages.${system}.nvim-pager}/bin/pager";
|
||||||
};
|
};
|
||||||
default = pkgs.mkShell {
|
default = pkgs.mkShell {
|
||||||
|
|||||||
@@ -1,8 +1,46 @@
|
|||||||
#include "hectic.h"
|
#include "hectic.h"
|
||||||
|
#include <fnmatch.h>
|
||||||
|
#include <string.h> // For strdup, strchr, etc.
|
||||||
|
|
||||||
|
// On systems without strsep, provide a custom implementation
|
||||||
|
#ifndef _GNU_SOURCE
|
||||||
|
#define _GNU_SOURCE 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef HAVE_STRSEP
|
||||||
|
char *strsep(char **stringp, const char *delim) {
|
||||||
|
char *start = *stringp;
|
||||||
|
char *p;
|
||||||
|
|
||||||
|
if (!start)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
p = start;
|
||||||
|
while (*p && !strchr(delim, *p))
|
||||||
|
p++;
|
||||||
|
|
||||||
|
if (*p) {
|
||||||
|
*p++ = '\0';
|
||||||
|
*stringp = p;
|
||||||
|
} else {
|
||||||
|
*stringp = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
void free_log_rules();
|
||||||
|
const char* json_type_to_string(JsonType type);
|
||||||
|
|
||||||
// Global color mode variable definition
|
// Global color mode variable definition
|
||||||
ColorMode color_mode = COLOR_MODE_AUTO;
|
ColorMode color_mode = COLOR_MODE_AUTO;
|
||||||
|
|
||||||
|
// Global logging variables
|
||||||
|
LogLevel current_log_level = LOG_LEVEL_INFO;
|
||||||
|
LogRule *log_rules = NULL; // Linked list of log rules
|
||||||
|
|
||||||
const char* color_mode_to_string(ColorMode mode) {
|
const char* color_mode_to_string(ColorMode mode) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case COLOR_MODE_AUTO: return "AUTO";
|
case COLOR_MODE_AUTO: return "AUTO";
|
||||||
@@ -74,25 +112,35 @@ LogLevel log_level_from_string(const char *level_str) {
|
|||||||
return LOG_LEVEL_INFO;
|
return LOG_LEVEL_INFO;
|
||||||
}
|
}
|
||||||
|
|
||||||
LogLevel current_log_level = LOG_LEVEL_INFO;
|
|
||||||
|
|
||||||
void logger_level_reset() {
|
void logger_level_reset() {
|
||||||
current_log_level = LOG_LEVEL_INFO;
|
current_log_level = LOG_LEVEL_INFO;
|
||||||
|
free_log_rules();
|
||||||
}
|
}
|
||||||
|
|
||||||
void logger_level(LogLevel level) {
|
void logger_level(LogLevel level) {
|
||||||
current_log_level = level;
|
current_log_level = level;
|
||||||
|
free_log_rules(); // Clear any complex rules
|
||||||
}
|
}
|
||||||
|
|
||||||
void init_logger(void) {
|
void init_logger(void) {
|
||||||
// Read log level from environment
|
// Read log level or rules from environment
|
||||||
const char* env_level = getenv("LOG_LEVEL");
|
const char* env_level = getenv("LOG_LEVEL");
|
||||||
current_log_level = log_level_from_string(env_level);
|
|
||||||
|
|
||||||
// Log initialization with appropriate message
|
|
||||||
if (env_level) {
|
if (env_level) {
|
||||||
fprintf(stderr, "INIT: Logger initialized with level %s from environment\n",
|
// Check if it's a complex rule format (contains '=' or ',')
|
||||||
log_level_to_string(current_log_level));
|
if (strchr(env_level, '=') || strchr(env_level, ',')) {
|
||||||
|
if (logger_parse_rules(env_level)) {
|
||||||
|
fprintf(stderr, "INIT: Logger initialized with complex rules from environment\n");
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "INIT: Failed to parse complex log rules, using default level INFO\n");
|
||||||
|
current_log_level = LOG_LEVEL_INFO;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Simple log level
|
||||||
|
current_log_level = log_level_from_string(env_level);
|
||||||
|
fprintf(stderr, "INIT: Logger initialized with level %s from environment\n",
|
||||||
|
log_level_to_string(current_log_level));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fprintf(stderr, "INIT: Logger initialized with default level %s\n",
|
fprintf(stderr, "INIT: Logger initialized with default level %s\n",
|
||||||
log_level_to_string(current_log_level));
|
log_level_to_string(current_log_level));
|
||||||
@@ -106,7 +154,9 @@ char* raise_message(
|
|||||||
int line,
|
int line,
|
||||||
const char *format,
|
const char *format,
|
||||||
...) {
|
...) {
|
||||||
if (level < current_log_level) {
|
// Check against the effective log level for this context
|
||||||
|
LogLevel effective_level = logger_get_effective_level(file, func, line);
|
||||||
|
if (level < effective_level) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1087,3 +1137,286 @@ char* json_to_debug_str(Arena *arena, Json json) {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up existing log rules
|
||||||
|
void free_log_rules() {
|
||||||
|
LogRule *rule = log_rules;
|
||||||
|
while (rule) {
|
||||||
|
LogRule *next = rule->next;
|
||||||
|
if (rule->file_pattern) free(rule->file_pattern);
|
||||||
|
if (rule->function_pattern) free(rule->function_pattern);
|
||||||
|
free(rule);
|
||||||
|
rule = next;
|
||||||
|
}
|
||||||
|
log_rules = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new log rule to the rule chain
|
||||||
|
LogRule* add_log_rule(LogLevel level, const char *file_pattern, const char *function_pattern,
|
||||||
|
int line_start, int line_end) {
|
||||||
|
LogRule *rule = (LogRule*)malloc(sizeof(LogRule));
|
||||||
|
if (!rule) return NULL;
|
||||||
|
|
||||||
|
rule->level = level;
|
||||||
|
rule->file_pattern = file_pattern ? strdup(file_pattern) : NULL;
|
||||||
|
rule->function_pattern = function_pattern ? strdup(function_pattern) : NULL;
|
||||||
|
rule->line_start = line_start;
|
||||||
|
rule->line_end = line_end;
|
||||||
|
rule->next = NULL;
|
||||||
|
|
||||||
|
// Add to the end of the list
|
||||||
|
if (!log_rules) {
|
||||||
|
log_rules = rule;
|
||||||
|
} else {
|
||||||
|
LogRule *last = log_rules;
|
||||||
|
while (last->next) {
|
||||||
|
last = last->next;
|
||||||
|
}
|
||||||
|
last->next = rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a line range specification (start:end)
|
||||||
|
void parse_line_range(const char *range_str, int *start, int *end) {
|
||||||
|
if (!range_str) {
|
||||||
|
*start = -1;
|
||||||
|
*end = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *endptr;
|
||||||
|
*start = strtol(range_str, &endptr, 10);
|
||||||
|
|
||||||
|
if (*endptr == ':') {
|
||||||
|
*end = strtol(endptr + 1, NULL, 10);
|
||||||
|
} else {
|
||||||
|
*end = *start;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*start <= 0) *start = -1;
|
||||||
|
if (*end <= 0) *end = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a complex rule string and set up log rules
|
||||||
|
int logger_parse_rules(const char *rules_str) {
|
||||||
|
if (!rules_str || !*rules_str) return 0;
|
||||||
|
|
||||||
|
// Clean up existing rules
|
||||||
|
free_log_rules();
|
||||||
|
|
||||||
|
// Make a copy of the rules string since we'll be modifying it
|
||||||
|
char *rules_copy = strdup(rules_str);
|
||||||
|
if (!rules_copy) return 0;
|
||||||
|
|
||||||
|
// First rule sets the default level
|
||||||
|
char *next_rule = rules_copy;
|
||||||
|
char *token = strsep(&next_rule, ",");
|
||||||
|
current_log_level = log_level_from_string(token);
|
||||||
|
|
||||||
|
// Process the remaining rules
|
||||||
|
while (next_rule && *next_rule) {
|
||||||
|
// Extract rule definition: pattern=level
|
||||||
|
char *rule_def = strsep(&next_rule, ",");
|
||||||
|
char *level_str = strchr(rule_def, '=');
|
||||||
|
|
||||||
|
if (!level_str) continue; // Invalid rule
|
||||||
|
|
||||||
|
*level_str = '\0'; // Split pattern and level
|
||||||
|
level_str++;
|
||||||
|
|
||||||
|
// Parse the rule pattern
|
||||||
|
char *pattern = rule_def;
|
||||||
|
char *file_pattern = NULL;
|
||||||
|
char *function_pattern = NULL;
|
||||||
|
char *line_range = NULL;
|
||||||
|
|
||||||
|
// Check for line range in file pattern
|
||||||
|
char *at_sign = strchr(pattern, '@');
|
||||||
|
if (at_sign) {
|
||||||
|
*at_sign = '\0';
|
||||||
|
file_pattern = pattern;
|
||||||
|
pattern = at_sign + 1;
|
||||||
|
|
||||||
|
// Check for line range or another @ for function
|
||||||
|
char *colon = strchr(pattern, ':');
|
||||||
|
char *second_at = strchr(pattern, '@');
|
||||||
|
|
||||||
|
if (second_at && (!colon || second_at < colon)) {
|
||||||
|
// Format: file@function@line_range
|
||||||
|
*second_at = '\0';
|
||||||
|
function_pattern = pattern;
|
||||||
|
line_range = second_at + 1;
|
||||||
|
} else if (colon) {
|
||||||
|
// Format: file@line_range
|
||||||
|
line_range = pattern;
|
||||||
|
} else {
|
||||||
|
// Format: file@function
|
||||||
|
function_pattern = pattern;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Just file pattern
|
||||||
|
file_pattern = pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If file pattern is empty, set to NULL
|
||||||
|
if (file_pattern && !*file_pattern) file_pattern = NULL;
|
||||||
|
|
||||||
|
// If function pattern is empty, set to NULL
|
||||||
|
if (function_pattern && !*function_pattern) function_pattern = NULL;
|
||||||
|
|
||||||
|
// Parse line range
|
||||||
|
int line_start = -1, line_end = -1;
|
||||||
|
parse_line_range(line_range, &line_start, &line_end);
|
||||||
|
|
||||||
|
// Create a new rule
|
||||||
|
LogLevel level = log_level_from_string(level_str);
|
||||||
|
add_log_rule(level, file_pattern, function_pattern, line_start, line_end);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(rules_copy);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a file matches a pattern
|
||||||
|
static int match_file_pattern(const char *file, const char *pattern) {
|
||||||
|
if (!pattern) return 1; // NULL pattern matches any file
|
||||||
|
|
||||||
|
// Extract the filename part without the path
|
||||||
|
const char *filename = strrchr(file, '/');
|
||||||
|
if (!filename) filename = file;
|
||||||
|
else filename++; // Skip the '/'
|
||||||
|
|
||||||
|
return fnmatch(pattern, filename, 0) == 0 || fnmatch(pattern, file, 0) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a function matches a pattern
|
||||||
|
static int match_function_pattern(const char *func, const char *pattern) {
|
||||||
|
if (!pattern) return 1; // NULL pattern matches any function
|
||||||
|
return fnmatch(pattern, func, 0) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the effective log level for a specific context
|
||||||
|
LogLevel logger_get_effective_level(const char *file, const char *func, int line) {
|
||||||
|
// If no rules are defined, use the global level
|
||||||
|
if (!log_rules) return current_log_level;
|
||||||
|
|
||||||
|
// Default to the global log level
|
||||||
|
LogLevel effective_level = current_log_level;
|
||||||
|
|
||||||
|
// Check each rule in order
|
||||||
|
for (LogRule *rule = log_rules; rule; rule = rule->next) {
|
||||||
|
int file_match = match_file_pattern(file, rule->file_pattern);
|
||||||
|
int function_match = match_function_pattern(func, rule->function_pattern);
|
||||||
|
int line_match = (rule->line_start == -1 || (line >= rule->line_start &&
|
||||||
|
(rule->line_end == -1 || line <= rule->line_end)));
|
||||||
|
|
||||||
|
// If all conditions match, use this rule's level
|
||||||
|
if (file_match && function_match && line_match) {
|
||||||
|
effective_level = rule->level;
|
||||||
|
// Don't break here - later rules can override earlier ones
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return effective_level;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new log rule programmatically
|
||||||
|
int logger_add_rule(LogLevel level, const char *file_pattern, const char *function_pattern,
|
||||||
|
int line_start, int line_end) {
|
||||||
|
return add_log_rule(level, file_pattern, function_pattern, line_start, line_end) != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print all current logging rules to stderr
|
||||||
|
void logger_print_rules() {
|
||||||
|
fprintf(stderr, "Current logging rules:\n");
|
||||||
|
fprintf(stderr, " Default level: %s\n", log_level_to_string(current_log_level));
|
||||||
|
|
||||||
|
int rule_count = 0;
|
||||||
|
for (LogRule *rule = log_rules; rule; rule = rule->next) {
|
||||||
|
fprintf(stderr, " Rule %d: Level=%s, File=%s, Function=%s, Lines=%d:%d\n",
|
||||||
|
++rule_count,
|
||||||
|
log_level_to_string(rule->level),
|
||||||
|
rule->file_pattern ? rule->file_pattern : "<any>",
|
||||||
|
rule->function_pattern ? rule->function_pattern : "<any>",
|
||||||
|
rule->line_start, rule->line_end);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule_count == 0) {
|
||||||
|
fprintf(stderr, " No specific rules defined\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to format a rule as a string
|
||||||
|
static void format_rule_to_buffer(char *buffer, size_t size, LogRule *rule) {
|
||||||
|
char line_range[32] = "";
|
||||||
|
|
||||||
|
// Format line range if specified
|
||||||
|
if (rule->line_start > 0) {
|
||||||
|
if (rule->line_end > 0 && rule->line_end != rule->line_start) {
|
||||||
|
snprintf(line_range, sizeof(line_range), "%d:%d", rule->line_start, rule->line_end);
|
||||||
|
} else {
|
||||||
|
snprintf(line_range, sizeof(line_range), "%d", rule->line_start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the complete rule
|
||||||
|
if (rule->file_pattern && rule->function_pattern && line_range[0]) {
|
||||||
|
// File + function + line range
|
||||||
|
snprintf(buffer, size, "%s@%s@%s=%s",
|
||||||
|
rule->file_pattern, rule->function_pattern, line_range,
|
||||||
|
log_level_to_string(rule->level));
|
||||||
|
} else if (rule->file_pattern && rule->function_pattern) {
|
||||||
|
// File + function
|
||||||
|
snprintf(buffer, size, "%s@%s=%s",
|
||||||
|
rule->file_pattern, rule->function_pattern,
|
||||||
|
log_level_to_string(rule->level));
|
||||||
|
} else if (rule->file_pattern && line_range[0]) {
|
||||||
|
// File + line range
|
||||||
|
snprintf(buffer, size, "%s@%s=%s",
|
||||||
|
rule->file_pattern, line_range,
|
||||||
|
log_level_to_string(rule->level));
|
||||||
|
} else if (rule->file_pattern) {
|
||||||
|
// Just file
|
||||||
|
snprintf(buffer, size, "%s=%s",
|
||||||
|
rule->file_pattern,
|
||||||
|
log_level_to_string(rule->level));
|
||||||
|
} else {
|
||||||
|
// Empty rule (shouldn't happen)
|
||||||
|
snprintf(buffer, size, "EMPTY=%s", log_level_to_string(rule->level));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format all rules into a string
|
||||||
|
char* logger_rules_to_string(Arena *arena) {
|
||||||
|
if (!arena) return NULL;
|
||||||
|
|
||||||
|
// Allocate a buffer in the arena (estimate size needed)
|
||||||
|
size_t estimated_size = 1024; // Start with 1KB
|
||||||
|
char *buffer = arena_alloc(arena, estimated_size);
|
||||||
|
if (!buffer) return NULL;
|
||||||
|
|
||||||
|
// Initialize with default level
|
||||||
|
int pos = snprintf(buffer, estimated_size, "%s", log_level_to_string(current_log_level));
|
||||||
|
|
||||||
|
// Add each rule
|
||||||
|
for (LogRule *rule = log_rules; rule; rule = rule->next) {
|
||||||
|
// Format the rule
|
||||||
|
char rule_str[256];
|
||||||
|
format_rule_to_buffer(rule_str, sizeof(rule_str), rule);
|
||||||
|
|
||||||
|
// Check buffer space and add to result
|
||||||
|
if (pos + strlen(rule_str) + 2 < estimated_size) {
|
||||||
|
buffer[pos++] = ',';
|
||||||
|
strcpy(buffer + pos, rule_str);
|
||||||
|
pos += strlen(rule_str);
|
||||||
|
} else {
|
||||||
|
// Buffer too small, just stop
|
||||||
|
strcat(buffer, ",...");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
@@ -129,6 +129,19 @@ typedef enum {
|
|||||||
LOG_LEVEL_EXCEPTION
|
LOG_LEVEL_EXCEPTION
|
||||||
} LogLevel;
|
} LogLevel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structure for complex log level rule
|
||||||
|
* Allows specifying log levels per file, function, and line range
|
||||||
|
*/
|
||||||
|
typedef struct LogRule {
|
||||||
|
LogLevel level; // Log level for this rule
|
||||||
|
char *file_pattern; // File pattern to match (can be NULL)
|
||||||
|
char *function_pattern; // Function pattern to match (can be NULL)
|
||||||
|
int line_start; // Start line number (-1 for any)
|
||||||
|
int line_end; // End line number (-1 for any)
|
||||||
|
struct LogRule *next; // Next rule in the chain
|
||||||
|
} LogRule;
|
||||||
|
|
||||||
void logger_level_reset();
|
void logger_level_reset();
|
||||||
|
|
||||||
void init_logger(void);
|
void init_logger(void);
|
||||||
@@ -137,6 +150,39 @@ void logger_level(LogLevel level);
|
|||||||
|
|
||||||
LogLevel log_level_from_string(const char *level_str);
|
LogLevel log_level_from_string(const char *level_str);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set complex logging rules from a string
|
||||||
|
* Format: DEFAULT_LEVEL,<file>@<function>=LEVEL,<file>@<line_start>:<line_end>=LEVEL,...
|
||||||
|
* Example: "INFO,main.c@main=DEBUG,helper.c@10:50=TRACE"
|
||||||
|
*
|
||||||
|
* @param rules_str The rule string to parse
|
||||||
|
* @return 1 on success, 0 on failure
|
||||||
|
*/
|
||||||
|
int logger_parse_rules(const char *rules_str);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set complex logging rule programmatically
|
||||||
|
*
|
||||||
|
* @param level Log level for this rule
|
||||||
|
* @param file_pattern File pattern to match (NULL for any file)
|
||||||
|
* @param function_pattern Function pattern to match (NULL for any function)
|
||||||
|
* @param line_start Start line number (-1 for any)
|
||||||
|
* @param line_end End line number (-1 for any)
|
||||||
|
* @return 1 on success, 0 on failure
|
||||||
|
*/
|
||||||
|
int logger_add_rule(LogLevel level, const char *file_pattern, const char *function_pattern,
|
||||||
|
int line_start, int line_end);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the effective log level for a message based on complex rules
|
||||||
|
*
|
||||||
|
* @param file Source file where log was generated
|
||||||
|
* @param func Function where log was generated
|
||||||
|
* @param line Line number where log was generated
|
||||||
|
* @return The effective log level for this context
|
||||||
|
*/
|
||||||
|
LogLevel logger_get_effective_level(const char *file, const char *func, int line);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core logging function that formats and outputs log messages.
|
* Core logging function that formats and outputs log messages.
|
||||||
*
|
*
|
||||||
@@ -386,4 +432,17 @@ char* json_to_debug_str(Arena *arena, Json json);
|
|||||||
#define DEBUGSTR_Slice(arena, value) slice_to_debug_str(arena, value)
|
#define DEBUGSTR_Slice(arena, value) slice_to_debug_str(arena, value)
|
||||||
#define DEBUGSTR_Json(arena, value) json_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);
|
||||||
|
|
||||||
#endif // EPRINTF_H
|
#endif // EPRINTF_H
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "hmpl.h"
|
#include "hmpl.h"
|
||||||
|
|
||||||
Json *eval_object(Arena *arena, const Json * const context, const char * const query) {
|
Json *eval_object(Arena *arena, const Json * const context, const char * const query) {
|
||||||
raise_debug("eval_object(%p, %s, %s)", arena, json_to_string(arena, context), query);
|
raise_debug("eval_object(%p, %s, %s)", arena, json_to_string(DISPOSABLE_ARENA, context), query);
|
||||||
if (!context || !query) return NULL;
|
if (!context || !query) return NULL;
|
||||||
|
|
||||||
const Json *res = context;
|
const Json *res = context;
|
||||||
@@ -9,14 +9,14 @@ Json *eval_object(Arena *arena, const Json * const context, const char * const q
|
|||||||
|
|
||||||
while ((dot = strchr(key, '.')) != NULL) {
|
while ((dot = strchr(key, '.')) != NULL) {
|
||||||
*dot = '\0';
|
*dot = '\0';
|
||||||
raise_debug("res: %s, key: %s, query: %s", json_to_string(arena, res), key, query);
|
raise_debug("eval_object: key: %s", key);
|
||||||
res = json_get_object_item(res, key);
|
res = json_get_object_item(res, key);
|
||||||
if (!res)
|
if (!res)
|
||||||
return NULL;
|
return NULL;
|
||||||
key = dot + 1;
|
key = dot + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
raise_debug("res: %s, key: %s, query: %s", json_to_string(arena, res), key, query);
|
raise_debug("eval_object: final key: %s", key);
|
||||||
return json_get_object_item(res, key);
|
return json_get_object_item(res, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,8 +58,8 @@ void hmpl_render_interpolation_tags(Arena *arena, char **text_ptr, const Json *
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вычисляем длину замены от начала {{[prefix] до конца }}
|
// Calculate the replacement length from the beginning of {{[prefix] to the end }}
|
||||||
int replace_length = (end - start) + 2; // +2 для "}}"
|
int replace_length = (end - start) + 2; // +2 for "}}"
|
||||||
|
|
||||||
char *new_text = arena_repstr(arena, current_text,
|
char *new_text = arena_repstr(arena, current_text,
|
||||||
start_index,
|
start_index,
|
||||||
@@ -74,6 +74,10 @@ void hmpl_render_interpolation_tags(Arena *arena, char **text_ptr, const Json *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void hmpl_render_interpolation_tags_opts(Arena *arena, char **text_ptr, const Json *context, const HmplInterpolationTagsOptions *options) {
|
||||||
|
hmpl_render_interpolation_tags(arena, text_ptr, context, options->prefix);
|
||||||
|
}
|
||||||
|
|
||||||
// {{item#array}}...{{/array}}
|
// {{item#array}}...{{/array}}
|
||||||
void hmpl_render_section_tags(Arena *arena, char **text_ptr, Json *context, const char * const prefix_start, const char * const prefix_end, const char * const separator_pattern) {
|
void hmpl_render_section_tags(Arena *arena, char **text_ptr, Json *context, const char * const prefix_start, const char * const prefix_end, const char * const separator_pattern) {
|
||||||
raise_debug("hmpl_render_section_tags(%p, %s, <optimized>, %s, %s, %s)", arena, *text_ptr, prefix_start, prefix_end, separator_pattern);
|
raise_debug("hmpl_render_section_tags(%p, %s, <optimized>, %s, %s, %s)", arena, *text_ptr, prefix_start, prefix_end, separator_pattern);
|
||||||
@@ -207,9 +211,15 @@ void hmpl_render_section_tags(Arena *arena, char **text_ptr, Json *context, cons
|
|||||||
char *prefix = arena_alloc(arena, element_name_length + 2);
|
char *prefix = arena_alloc(arena, element_name_length + 2);
|
||||||
snprintf(prefix, element_name_length + 2, "%s.", element_name);
|
snprintf(prefix, element_name_length + 2, "%s.", element_name);
|
||||||
|
|
||||||
|
raise_debug("Processing element with prefix: %s", prefix);
|
||||||
|
raise_debug("Block before processing: %s", block);
|
||||||
|
|
||||||
hmpl_render_interpolation_tags(arena, &block, elem, prefix);
|
hmpl_render_interpolation_tags(arena, &block, elem, prefix);
|
||||||
raise_trace("block after: %s", block);
|
raise_debug("Block after interpolation: %s", block);
|
||||||
|
|
||||||
|
// Recursively process nested sections
|
||||||
|
hmpl_render_section_tags(arena, &block, elem, prefix_start, prefix_end, separator_pattern);
|
||||||
|
raise_debug("Block after section processing: %s", block);
|
||||||
|
|
||||||
size_t block_len = strlen(block);
|
size_t block_len = strlen(block);
|
||||||
memcpy(replacement + replacement_offset, block, block_len);
|
memcpy(replacement + replacement_offset, block, block_len);
|
||||||
@@ -239,19 +249,25 @@ void hmpl_render_section_tags(Arena *arena, char **text_ptr, Json *context, cons
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void hmpl_render_with_arena(Arena *arena, char **text, const Json * const context) {
|
void hmpl_render_section_tags_opts(Arena *arena, char **text_ptr, const Json *context, const HmplSectionTagsOptions *options) {
|
||||||
|
// Create a copy of the context without const qualifier for compatibility with hmpl_render_section_tags
|
||||||
|
hmpl_render_section_tags(arena, text_ptr, (Json*)context, options->prefix_start, options->prefix_end, options->separator_pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
void hmpl_render_with_arena(Arena *arena, char **text, const Json * const context, const HmplOptions * const options) {
|
||||||
if (context->type != JSON_OBJECT) {
|
if (context->type != JSON_OBJECT) {
|
||||||
raise_exception("Malformed context: context is not json");
|
raise_exception("Malformed context: context is not json");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
hmpl_render_interpolation_tags(arena, text, context, "");
|
hmpl_render_interpolation_tags_opts(arena, text, context, &options->interpolation_tags_options);
|
||||||
|
hmpl_render_section_tags_opts(arena, text, context, &options->section_tags_options);
|
||||||
}
|
}
|
||||||
|
|
||||||
void hmpl_render(char **text, const Json * const context) {
|
void hmpl_render(char **text, const Json * const context, const HmplOptions * const options) {
|
||||||
Arena arena = arena_init(MEM_MiB);
|
Arena arena = arena_init(MEM_MiB);
|
||||||
|
|
||||||
hmpl_render_with_arena(&arena, text, context);
|
hmpl_render_with_arena(&arena, text, context, options);
|
||||||
|
|
||||||
arena_free(&arena);
|
arena_free(&arena);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,32 @@
|
|||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include "hectic.h"
|
#include "hectic.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char *prefix_start;
|
||||||
|
char *prefix_end;
|
||||||
|
char *separator_pattern;
|
||||||
|
} HmplSectionTagsOptions;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char *prefix;
|
||||||
|
} HmplInterpolationTagsOptions;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
HmplSectionTagsOptions section_tags_options;
|
||||||
|
HmplInterpolationTagsOptions interpolation_tags_options;
|
||||||
|
} HmplOptions;
|
||||||
|
|
||||||
|
static const HmplOptions DEFAULT_OPTIONS = {
|
||||||
|
.section_tags_options = {
|
||||||
|
.prefix_start = "",
|
||||||
|
.prefix_end = "/",
|
||||||
|
.separator_pattern = "#"
|
||||||
|
},
|
||||||
|
.interpolation_tags_options = {
|
||||||
|
.prefix = ""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
void init_cjson_with_arenas(Arena *arena);
|
void init_cjson_with_arenas(Arena *arena);
|
||||||
|
|
||||||
char *eval_string(Arena *arena, const Json * const context, const char * const key);
|
char *eval_string(Arena *arena, const Json * const context, const char * const key);
|
||||||
@@ -15,10 +41,14 @@ char *eval_string(Arena *arena, const Json * const context, const char * const k
|
|||||||
/* Modified: text is passed by reference so we can update it and free old allocations */
|
/* Modified: text is passed by reference so we can update it and free old allocations */
|
||||||
void hmpl_render_interpolation_tags(Arena *arena, char **text_ptr, const Json * const context, const char * const prefix);
|
void hmpl_render_interpolation_tags(Arena *arena, char **text_ptr, const Json * const context, const char * const prefix);
|
||||||
|
|
||||||
|
void hmpl_render_interpolation_tags_opts(Arena *arena, char **text_ptr, const Json *context, const HmplInterpolationTagsOptions *options);
|
||||||
|
|
||||||
void hmpl_render_section_tags(Arena *arena, char **text_ptr, Json *context, const char * const prefix_start, const char * const prefix_end, const char * const separator_pattern);
|
void hmpl_render_section_tags(Arena *arena, char **text_ptr, Json *context, const char * const prefix_start, const char * const prefix_end, const char * const separator_pattern);
|
||||||
|
|
||||||
void hmpl_render_with_arena(Arena *arena, char **text, const Json * const context);
|
void hmpl_render_section_tags_opts(Arena *arena, char **text_ptr, const Json *context, const HmplSectionTagsOptions *options);
|
||||||
|
|
||||||
void hmpl_render(char **text, const Json * const context);
|
void hmpl_render_with_arena(Arena *arena, char **text, const Json * const context, const HmplOptions * const options);
|
||||||
|
|
||||||
|
void hmpl_render(char **text, const Json * const context, const HmplOptions * const options);
|
||||||
|
|
||||||
#endif // EPRINTF_HMPL
|
#endif // EPRINTF_HMPL
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ int main(int argc, char *argv[]) {
|
|||||||
text = arena_strdup(&arena, "");
|
text = arena_strdup(&arena, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
hmpl_render_with_arena(&arena, &text, context);
|
hmpl_render_with_arena(&arena, &text, context, &DEFAULT_OPTIONS);
|
||||||
printf("%s", text);
|
printf("%s", text);
|
||||||
|
|
||||||
arena_free(&arena);
|
arena_free(&arena);
|
||||||
|
|||||||
313
package/c/hmpl/test/test_section_tags.c
Executable file
313
package/c/hmpl/test/test_section_tags.c
Executable file
@@ -0,0 +1,313 @@
|
|||||||
|
#include "../hmpl.h"
|
||||||
|
#include "hectic.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
const HmplOptions options = {
|
||||||
|
.section_tags_options = {
|
||||||
|
.prefix_start = "",
|
||||||
|
.prefix_end = "/",
|
||||||
|
.separator_pattern = "#"
|
||||||
|
},
|
||||||
|
.interpolation_tags_options = {
|
||||||
|
.prefix = ""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function for comparing results
|
||||||
|
void assert_rendered(const char* template, const char* expected, const char* json_str) {
|
||||||
|
Arena arena = arena_init(MEM_KiB);
|
||||||
|
|
||||||
|
// Parse JSON string into an object
|
||||||
|
Json *json = json_parse(&arena, &json_str);
|
||||||
|
if (!json) {
|
||||||
|
printf("ERROR: Failed to parse JSON: %s\n", json_str);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a copy of the template for modification
|
||||||
|
char *result = arena_strdup(&arena, template);
|
||||||
|
|
||||||
|
// Render the template
|
||||||
|
hmpl_render_with_arena(&arena, &result, json, &options);
|
||||||
|
|
||||||
|
// Check the result
|
||||||
|
if (strcmp(result, expected) != 0) {
|
||||||
|
printf("ERROR:\n");
|
||||||
|
printf("Template: %s\n", template);
|
||||||
|
printf("Expected: %s\n", expected);
|
||||||
|
printf("Received: %s\n", result);
|
||||||
|
exit(1);
|
||||||
|
} else {
|
||||||
|
printf("SUCCESS: Template correctly rendered\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
arena_free(&arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
// -- Simple section tag with surrounding text --
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
#define TEST_SIMPLE_SECTION_CONTEXT \
|
||||||
|
"{" \
|
||||||
|
" \"users\": [" \
|
||||||
|
" {\"name\": \"John\", \"age\": 30}," \
|
||||||
|
" {\"name\": \"Mary\", \"age\": 25}," \
|
||||||
|
" {\"name\": \"Alex\", \"age\": 35}" \
|
||||||
|
" ]," \
|
||||||
|
" \"count\": 3" \
|
||||||
|
"}"
|
||||||
|
|
||||||
|
#define TEST_SIMPLE_SECTION_TEMPLATE \
|
||||||
|
"User list:\n" \
|
||||||
|
"{{item#users}}<li>{{item.name}}, age: {{item.age}}</li>{{/users}}\n" \
|
||||||
|
"Total users: {{count}}"
|
||||||
|
|
||||||
|
#define TEST_SIMPLE_SECTION_RESULT \
|
||||||
|
"User list:\n" \
|
||||||
|
"<li>John, age: 30</li><li>Mary, age: 25</li><li>Alex, age: 35</li>\n" \
|
||||||
|
"Total users: 3"
|
||||||
|
|
||||||
|
void test_simple_section_tags(Arena *arena) {
|
||||||
|
raise_notice("Testing simple section tag with surrounding text");
|
||||||
|
const char *context_text = arena_strdup(arena, TEST_SIMPLE_SECTION_CONTEXT);
|
||||||
|
Json *context = json_parse(arena, &context_text);
|
||||||
|
if (!context) { raise_exception("Malformed json"); exit(1); }
|
||||||
|
|
||||||
|
char *text = arena_strdup(arena, TEST_SIMPLE_SECTION_TEMPLATE);
|
||||||
|
raise_notice("Template:\n%s", text);
|
||||||
|
raise_notice("Context: %s", json_to_string(arena, context));
|
||||||
|
|
||||||
|
hmpl_render_with_arena(arena, &text, context, &options);
|
||||||
|
raise_notice("Result:\n%s", text);
|
||||||
|
|
||||||
|
assert(strcmp(text, TEST_SIMPLE_SECTION_RESULT) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------
|
||||||
|
// -- Nested section tags --
|
||||||
|
// -----------------------------------
|
||||||
|
|
||||||
|
#define TEST_NESTED_SECTION_CONTEXT \
|
||||||
|
"{" \
|
||||||
|
" \"department\": \"Development\"," \
|
||||||
|
" \"teams\": [" \
|
||||||
|
" {" \
|
||||||
|
" \"name\": \"Frontend\"," \
|
||||||
|
" \"members\": [" \
|
||||||
|
" {\"name\": \"John\", \"role\": \"Developer\"}," \
|
||||||
|
" {\"name\": \"Mary\", \"role\": \"Designer\"}" \
|
||||||
|
" ]" \
|
||||||
|
" }," \
|
||||||
|
" {" \
|
||||||
|
" \"name\": \"Backend\"," \
|
||||||
|
" \"members\": [" \
|
||||||
|
" {\"name\": \"Alex\", \"role\": \"Developer\"}," \
|
||||||
|
" {\"name\": \"Helen\", \"role\": \"Tester\"}" \
|
||||||
|
" ]" \
|
||||||
|
" }" \
|
||||||
|
" ]" \
|
||||||
|
"}"
|
||||||
|
|
||||||
|
#define TEST_NESTED_SECTION_TEMPLATE \
|
||||||
|
"Department: {{department}}\n" \
|
||||||
|
"{{item#teams}}Team: {{item.name}}\n" \
|
||||||
|
" {{item#item.members}}Member: {{item.name}} ({{item.role}}){{/item.members}}\n" \
|
||||||
|
"{{/teams}}"
|
||||||
|
|
||||||
|
#define TEST_NESTED_SECTION_RESULT \
|
||||||
|
"Department: Development\n" \
|
||||||
|
"Team: Frontend\n" \
|
||||||
|
" Member: John (Developer)Member: Mary (Designer)\n" \
|
||||||
|
"Team: Backend\n" \
|
||||||
|
" Member: Alex (Developer)Member: Helen (Tester)\n"
|
||||||
|
|
||||||
|
void test_nested_section_tags(Arena *arena) {
|
||||||
|
raise_notice("Testing nested section tags");
|
||||||
|
const char *context_text = arena_strdup(arena, TEST_NESTED_SECTION_CONTEXT);
|
||||||
|
Json *context = json_parse(arena, &context_text);
|
||||||
|
if (!context) { raise_exception("Malformed json"); exit(1); }
|
||||||
|
|
||||||
|
char *text = arena_strdup(arena, TEST_NESTED_SECTION_TEMPLATE);
|
||||||
|
raise_notice("Template:\n%s", text);
|
||||||
|
raise_notice("Context: %s", json_to_string(arena, context));
|
||||||
|
|
||||||
|
hmpl_render_with_arena(arena, &text, context, &options);
|
||||||
|
raise_notice("Result:\n%s", text);
|
||||||
|
|
||||||
|
assert(strcmp(text, TEST_NESTED_SECTION_RESULT) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------
|
||||||
|
// -- Empty array in section tag --
|
||||||
|
// -----------------------------------
|
||||||
|
|
||||||
|
#define TEST_EMPTY_ARRAY_CONTEXT \
|
||||||
|
"{" \
|
||||||
|
" \"tasks\": []" \
|
||||||
|
"}"
|
||||||
|
|
||||||
|
#define TEST_EMPTY_ARRAY_TEMPLATE \
|
||||||
|
"Tasks: {{item#tasks}}ID: {{item.id}} - {{item.description}}{{/tasks}}"
|
||||||
|
|
||||||
|
#define TEST_EMPTY_ARRAY_RESULT \
|
||||||
|
"Tasks: "
|
||||||
|
|
||||||
|
void test_empty_array_section_tags(Arena *arena) {
|
||||||
|
raise_notice("Testing empty array in section tag");
|
||||||
|
const char *context_text = arena_strdup(arena, TEST_EMPTY_ARRAY_CONTEXT);
|
||||||
|
Json *context = json_parse(arena, &context_text);
|
||||||
|
if (!context) { raise_exception("Malformed json"); exit(1); }
|
||||||
|
|
||||||
|
char *text = arena_strdup(arena, TEST_EMPTY_ARRAY_TEMPLATE);
|
||||||
|
raise_notice("Template:\n%s", text);
|
||||||
|
raise_notice("Context: %s", json_to_string(arena, context));
|
||||||
|
|
||||||
|
hmpl_render_with_arena(arena, &text, context, &options);
|
||||||
|
raise_notice("Result:\n%s", text);
|
||||||
|
|
||||||
|
assert(strcmp(text, TEST_EMPTY_ARRAY_RESULT) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------
|
||||||
|
// -- HTML template with section tags --
|
||||||
|
// -----------------------------------
|
||||||
|
|
||||||
|
#define TEST_HTML_CONTEXT \
|
||||||
|
"{" \
|
||||||
|
" \"title\": \"My List\"," \
|
||||||
|
" \"items\": [" \
|
||||||
|
" {\"name\": \"Item 1\", \"type\": \"important\"}," \
|
||||||
|
" {\"name\": \"Item 2\", \"type\": \"normal\"}," \
|
||||||
|
" {\"name\": \"Item 3\", \"type\": \"normal\"}" \
|
||||||
|
" ]," \
|
||||||
|
" \"footer\": \"© 2023\"" \
|
||||||
|
"}"
|
||||||
|
|
||||||
|
#define TEST_HTML_TEMPLATE \
|
||||||
|
"<!DOCTYPE html>\n" \
|
||||||
|
"<html>\n" \
|
||||||
|
"<head>\n" \
|
||||||
|
" <title>{{title}}</title>\n" \
|
||||||
|
"</head>\n" \
|
||||||
|
"<body>\n" \
|
||||||
|
" <h1>{{title}}</h1>\n" \
|
||||||
|
" <ul>\n" \
|
||||||
|
" {{item#items}}<li class=\"{{item.type}}\">{{item.name}}</li>{{/items}}\n" \
|
||||||
|
" </ul>\n" \
|
||||||
|
" <footer>{{footer}}</footer>\n" \
|
||||||
|
"</body>\n" \
|
||||||
|
"</html>"
|
||||||
|
|
||||||
|
#define TEST_HTML_RESULT \
|
||||||
|
"<!DOCTYPE html>\n" \
|
||||||
|
"<html>\n" \
|
||||||
|
"<head>\n" \
|
||||||
|
" <title>My List</title>\n" \
|
||||||
|
"</head>\n" \
|
||||||
|
"<body>\n" \
|
||||||
|
" <h1>My List</h1>\n" \
|
||||||
|
" <ul>\n" \
|
||||||
|
" <li class=\"important\">Item 1</li><li class=\"normal\">Item 2</li><li class=\"normal\">Item 3</li>\n" \
|
||||||
|
" </ul>\n" \
|
||||||
|
" <footer>© 2023</footer>\n" \
|
||||||
|
"</body>\n" \
|
||||||
|
"</html>"
|
||||||
|
|
||||||
|
void test_html_section_tags(Arena *arena) {
|
||||||
|
raise_notice("Testing HTML template with section tags");
|
||||||
|
const char *context_text = arena_strdup(arena, TEST_HTML_CONTEXT);
|
||||||
|
Json *context = json_parse(arena, &context_text);
|
||||||
|
if (!context) { raise_exception("Malformed json"); exit(1); }
|
||||||
|
|
||||||
|
char *text = arena_strdup(arena, TEST_HTML_TEMPLATE);
|
||||||
|
raise_notice("Template:\n%s", text);
|
||||||
|
raise_notice("Context: %s", json_to_string(arena, context));
|
||||||
|
|
||||||
|
hmpl_render_with_arena(arena, &text, context, &options);
|
||||||
|
raise_notice("Result:\n%s", text);
|
||||||
|
|
||||||
|
assert(strcmp(text, TEST_HTML_RESULT) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------
|
||||||
|
// -- Report with nested sections --
|
||||||
|
// -----------------------------------
|
||||||
|
|
||||||
|
#define TEST_REPORT_CONTEXT \
|
||||||
|
"{" \
|
||||||
|
" \"period\": \"March 2023\"," \
|
||||||
|
" \"data\": [" \
|
||||||
|
" {" \
|
||||||
|
" \"title\": \"Sales\"," \
|
||||||
|
" \"value\": 1000," \
|
||||||
|
" \"details\": [" \
|
||||||
|
" {\"name\": \"Product A\", \"value\": 500}," \
|
||||||
|
" {\"name\": \"Product B\", \"value\": 300}," \
|
||||||
|
" {\"name\": \"Product C\", \"value\": 200}" \
|
||||||
|
" ]" \
|
||||||
|
" }," \
|
||||||
|
" {" \
|
||||||
|
" \"title\": \"Expenses\"," \
|
||||||
|
" \"value\": 700," \
|
||||||
|
" \"details\": [" \
|
||||||
|
" {\"name\": \"Rent\", \"value\": 300}," \
|
||||||
|
" {\"name\": \"Salary\", \"value\": 400}" \
|
||||||
|
" ]" \
|
||||||
|
" }" \
|
||||||
|
" ]," \
|
||||||
|
" \"summary\": 300" \
|
||||||
|
"}"
|
||||||
|
|
||||||
|
#define TEST_REPORT_TEMPLATE \
|
||||||
|
"Report for {{period}}\n\n" \
|
||||||
|
"{{row#data}}* {{row.title}}: {{row.value}}\n" \
|
||||||
|
" {{detail#row.details}} - {{detail.name}}: {{detail.value}}\n{{/row.details}}\n" \
|
||||||
|
"{{/data}}\n" \
|
||||||
|
"Total: {{summary}}"
|
||||||
|
|
||||||
|
#define TEST_REPORT_RESULT \
|
||||||
|
"Report for March 2023\n\n" \
|
||||||
|
"* Sales: 1000\n" \
|
||||||
|
" - Product A: 500\n - Product B: 300\n - Product C: 200\n\n" \
|
||||||
|
"* Expenses: 700\n" \
|
||||||
|
" - Rent: 300\n - Salary: 400\n\n" \
|
||||||
|
"Total: 300"
|
||||||
|
|
||||||
|
void test_report_section_tags(Arena *arena) {
|
||||||
|
raise_notice("Testing report with nested sections");
|
||||||
|
const char *context_text = arena_strdup(arena, TEST_REPORT_CONTEXT);
|
||||||
|
Json *context = json_parse(arena, &context_text);
|
||||||
|
if (!context) { raise_exception("Malformed json"); exit(1); }
|
||||||
|
|
||||||
|
char *text = arena_strdup(arena, TEST_REPORT_TEMPLATE);
|
||||||
|
raise_notice("Template:\n%s", text);
|
||||||
|
raise_notice("Context: %s", json_to_string(arena, context));
|
||||||
|
|
||||||
|
hmpl_render_with_arena(arena, &text, context, &options);
|
||||||
|
raise_notice("Result:\n%s", text);
|
||||||
|
|
||||||
|
assert(strcmp(text, TEST_REPORT_RESULT) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
init_logger();
|
||||||
|
Arena arena = arena_init(MEM_MiB);
|
||||||
|
|
||||||
|
test_simple_section_tags(&arena);
|
||||||
|
arena_reset(&arena);
|
||||||
|
test_nested_section_tags(&arena);
|
||||||
|
//arena_reset(&arena);
|
||||||
|
//test_empty_array_section_tags(&arena);
|
||||||
|
//arena_reset(&arena);
|
||||||
|
//test_html_section_tags(&arena);
|
||||||
|
//arena_reset(&arena);
|
||||||
|
//test_report_section_tags(&arena);
|
||||||
|
|
||||||
|
printf("All tests passed successfully!\n");
|
||||||
|
|
||||||
|
arena_free(&arena);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user