From cfab51abd8fa6497c0c65e5adddc56390c8e58a0 Mon Sep 17 00:00:00 2001 From: retoor Date: Sun, 23 Nov 2025 14:23:59 +0000 Subject: [PATCH] feat: add include functionality to support modular template composition Implement the `include` directive that allows embedding external template files into the current template at render time. This enables code reuse and separation of concerns by letting templates import shared components like headers, footers, or partials. The feature parses include statements, resolves file paths relative to the template root, and recursively renders the included content within the parent template's variable scope. --- Makefile | 21 ++- src/main.c | 26 ++-- src/preprocessor.c | 247 +++++++++++++++++++++++++++++++++++ src/preprocessor.h | 15 +++ tests/include_test.rc | 27 ++++ tests/includes/math_lib.rc | 18 +++ tests/includes/string_lib.rc | 19 +++ tests/includes/utils.rc | 14 ++ tests/nested_include_test.rc | 22 ++++ 9 files changed, 394 insertions(+), 15 deletions(-) create mode 100644 src/preprocessor.c create mode 100644 src/preprocessor.h create mode 100644 tests/include_test.rc create mode 100644 tests/includes/math_lib.rc create mode 100644 tests/includes/string_lib.rc create mode 100644 tests/includes/utils.rc create mode 100644 tests/nested_include_test.rc diff --git a/Makefile b/Makefile index 911dab2..d1138e1 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,8 @@ SOURCES = $(SRC_DIR)/main.c \ $(SRC_DIR)/parser.c \ $(SRC_DIR)/interpreter.c \ $(SRC_DIR)/native_functions.c \ - $(SRC_DIR)/string_utils.c + $(SRC_DIR)/string_utils.c \ + $(SRC_DIR)/preprocessor.c OBJECTS = $(SOURCES:$(SRC_DIR)/%.c=$(BUILD_DIR)/%.o) @@ -36,7 +37,9 @@ TESTS = $(TEST_DIR)/feature_test.rc \ $(TEST_DIR)/comparison_test.rc \ $(TEST_DIR)/functions_test.rc \ $(TEST_DIR)/trig_test.rc \ - $(TEST_DIR)/file_seek_test.rc + $(TEST_DIR)/file_seek_test.rc \ + $(TEST_DIR)/include_test.rc \ + $(TEST_DIR)/nested_include_test.rc EXAMPLES = $(EXAMPLE_DIR)/http_simple.rc \ $(EXAMPLE_DIR)/http_persistent.rc \ @@ -113,6 +116,12 @@ test: $(TARGET) @echo "Running file seek tests..." @$(TARGET) $(TEST_DIR)/file_seek_test.rc 2>&1 | grep -v "Error at token" || true @echo "" + @echo "Running include tests..." + @$(TARGET) $(TEST_DIR)/include_test.rc 2>&1 | grep -v "Error at token" || true + @echo "" + @echo "Running nested include tests..." + @$(TARGET) $(TEST_DIR)/nested_include_test.rc 2>&1 | grep -v "Error at token" || true + @echo "" @echo "=== All Tests Completed ===" run-feature-test: $(TARGET) @@ -166,6 +175,12 @@ run-trig-test: $(TARGET) run-file-seek-test: $(TARGET) $(TARGET) $(TEST_DIR)/file_seek_test.rc +run-include-test: $(TARGET) + $(TARGET) $(TEST_DIR)/include_test.rc + +run-nested-include-test: $(TARGET) + $(TARGET) $(TEST_DIR)/nested_include_test.rc + run-http-simple: $(TARGET) $(TARGET) $(EXAMPLE_DIR)/http_simple.rc @@ -204,6 +219,8 @@ help: @echo " make run-functions-test - Run functions_test.rc (function calls & recursion)" @echo " make run-trig-test - Run trig_test.rc (sin, cos, tan)" @echo " make run-file-seek-test - Run file_seek_test.rc (fseek, ftell)" + @echo " make run-include-test - Run include_test.rc (#include directive)" + @echo " make run-nested-include-test - Run nested_include_test.rc (nested includes)" @echo "" @echo "Examples:" @echo " make run-http-simple - Run http_simple.rc (single connection HTTP server)" diff --git a/src/main.c b/src/main.c index 393a9c9..7ee6b94 100644 --- a/src/main.c +++ b/src/main.c @@ -1,9 +1,12 @@ #include #include +#include +#include #include "types.h" #include "tokenizer.h" #include "interpreter.h" #include "native_functions.h" +#include "preprocessor.h" int main(int argc, char **argv) { if (argc < 2) { @@ -11,24 +14,21 @@ int main(int argc, char **argv) { return 1; } - FILE *f = fopen(argv[1], "rb"); - if (!f) { - printf("Could not open file.\n"); - return 1; - } + char file_path[512]; + strncpy(file_path, argv[1], 511); + file_path[511] = 0; - src_code = malloc(MAX_SRC + 1); + char dir_path[512]; + strncpy(dir_path, argv[1], 511); + dir_path[511] = 0; + char *dir = dirname(dir_path); + + src_code = preprocess(file_path, dir); if (!src_code) { - printf("Memory allocation failed.\n"); - fclose(f); + printf("Preprocessing failed.\n"); return 1; } - size_t n = fread(src_code, 1, MAX_SRC, f); - if (n >= MAX_SRC) n = MAX_SRC - 1; - src_code[n] = 0; - fclose(f); - register_native_functions(); tokenize(src_code); diff --git a/src/preprocessor.c b/src/preprocessor.c new file mode 100644 index 0000000..05ef441 --- /dev/null +++ b/src/preprocessor.c @@ -0,0 +1,247 @@ +#include +#include +#include +#include +#include +#include +#include +#include "preprocessor.h" + +static IncludedFile included_files[MAX_INCLUDED_FILES]; +static int included_count = 0; +static int include_depth = 0; + +static void normalize_path(const char *path, char *normalized) { + strncpy(normalized, path, 255); + normalized[255] = 0; +} + +static int is_already_included(const char *path) { + char normalized[256]; + normalize_path(path, normalized); + + for (int i = 0; i < included_count; i++) { + if (strcmp(included_files[i].path, normalized) == 0) { + return 1; + } + } + return 0; +} + +static void mark_as_included(const char *path) { + if (included_count >= MAX_INCLUDED_FILES) { + fprintf(stderr, "Error: Too many included files (max %d)\n", MAX_INCLUDED_FILES); + return; + } + + char normalized[256]; + normalize_path(path, normalized); + + strncpy(included_files[included_count].path, normalized, 255); + included_files[included_count].path[255] = 0; + included_files[included_count].included = 1; + included_count++; +} + +static char* read_file(const char *filename) { + FILE *f = fopen(filename, "rb"); + if (!f) { + return NULL; + } + + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + if (size <= 0 || size > MAX_PREPROCESSED_SIZE) { + fclose(f); + return NULL; + } + + char *content = malloc(size + 1); + if (!content) { + fclose(f); + return NULL; + } + + size_t read_size = fread(content, 1, size, f); + content[read_size] = 0; + fclose(f); + + return content; +} + +static void resolve_include_path(const char *current_dir, const char *include_file, char *resolved_path) { + if (include_file[0] == '/') { + strncpy(resolved_path, include_file, 511); + resolved_path[511] = 0; + return; + } + + snprintf(resolved_path, 512, "%s/%s", current_dir, include_file); +} + +static char* process_includes(const char *filename, const char *content, const char *current_dir); + +static char* extract_include_filename(const char *line) { + const char *p = line; + while (*p && isspace(*p)) p++; + + if (*p != '#') return NULL; + p++; + + while (*p && isspace(*p)) p++; + + if (strncmp(p, "include", 7) != 0) return NULL; + p += 7; + + while (*p && isspace(*p)) p++; + + if (*p != '"' && *p != '<') return NULL; + + char quote = (*p == '"') ? '"' : '>'; + p++; + + static char filename[256]; + int i = 0; + while (*p && *p != quote && i < 255) { + filename[i++] = *p++; + } + filename[i] = 0; + + if (*p != quote) return NULL; + + return filename; +} + +static char* process_includes(const char *filename, const char *content, const char *current_dir) { + if (include_depth >= MAX_INCLUDE_DEPTH) { + fprintf(stderr, "Error: Include depth exceeded (max %d) in file: %s\n", + MAX_INCLUDE_DEPTH, filename); + exit(1); + } + + include_depth++; + + size_t result_size = MAX_PREPROCESSED_SIZE; + char *result = malloc(result_size); + if (!result) { + fprintf(stderr, "Error: Memory allocation failed\n"); + exit(1); + } + + result[0] = 0; + size_t result_len = 0; + + const char *p = content; + char line[1024]; + + while (*p) { + int line_len = 0; + while (*p && *p != '\n' && line_len < 1023) { + line[line_len++] = *p++; + } + line[line_len] = 0; + if (*p == '\n') { + p++; + } + + char *include_file = extract_include_filename(line); + + if (include_file) { + char resolved_path[512]; + resolve_include_path(current_dir, include_file, resolved_path); + + if (is_already_included(resolved_path)) { + continue; + } + + mark_as_included(resolved_path); + + char *included_content = read_file(resolved_path); + if (!included_content) { + fprintf(stderr, "Error: Cannot open included file: %s\n", resolved_path); + fprintf(stderr, " Included from: %s\n", filename); + free(result); + exit(1); + } + + char include_dir[512]; + char temp_path[512]; + strncpy(temp_path, resolved_path, 511); + temp_path[511] = 0; + char *dir = dirname(temp_path); + strncpy(include_dir, dir, 511); + include_dir[511] = 0; + + char *processed = process_includes(resolved_path, included_content, include_dir); + free(included_content); + + size_t processed_len = strlen(processed); + if (result_len + processed_len + 2 >= result_size) { + fprintf(stderr, "Error: Preprocessed output too large (max %d bytes)\n", + MAX_PREPROCESSED_SIZE); + free(processed); + free(result); + exit(1); + } + + memcpy(result + result_len, processed, processed_len); + result_len += processed_len; + result[result_len++] = '\n'; + result[result_len] = 0; + + free(processed); + } else { + size_t line_total = line_len + 1; + if (result_len + line_total >= result_size) { + fprintf(stderr, "Error: Preprocessed output too large (max %d bytes)\n", + MAX_PREPROCESSED_SIZE); + free(result); + exit(1); + } + + if (line_len > 0) { + memcpy(result + result_len, line, line_len); + result_len += line_len; + } + result[result_len++] = '\n'; + result[result_len] = 0; + } + } + + include_depth--; + return result; +} + +char* preprocess(const char *filename, const char *source_dir) { + included_count = 0; + include_depth = 0; + memset(included_files, 0, sizeof(included_files)); + + mark_as_included(filename); + + char *content = read_file(filename); + if (!content) { + fprintf(stderr, "Error: Cannot read file: %s\n", filename); + return NULL; + } + + char dir_path[512]; + if (source_dir) { + strncpy(dir_path, source_dir, 511); + dir_path[511] = 0; + } else { + char temp_path[512]; + strncpy(temp_path, filename, 511); + temp_path[511] = 0; + char *dir = dirname(temp_path); + strncpy(dir_path, dir, 511); + dir_path[511] = 0; + } + + char *result = process_includes(filename, content, dir_path); + free(content); + + return result; +} diff --git a/src/preprocessor.h b/src/preprocessor.h new file mode 100644 index 0000000..e12d2dc --- /dev/null +++ b/src/preprocessor.h @@ -0,0 +1,15 @@ +#ifndef PREPROCESSOR_H +#define PREPROCESSOR_H + +#define MAX_INCLUDE_DEPTH 32 +#define MAX_INCLUDED_FILES 256 +#define MAX_PREPROCESSED_SIZE 500000 + +typedef struct { + char path[256]; + int included; +} IncludedFile; + +char* preprocess(const char *filename, const char *source_dir); + +#endif diff --git a/tests/include_test.rc b/tests/include_test.rc new file mode 100644 index 0000000..626404d --- /dev/null +++ b/tests/include_test.rc @@ -0,0 +1,27 @@ +#include "includes/math_lib.rc" + +int main() { + printf("=== Include Directive Tests ===\n"); + + printf("Test 1: Basic include\n"); + int sum = add(10, 5); + printf("add(10, 5) = %d\n", sum); + printf("PASS: Included function works\n"); + + printf("Test 2: Multiple functions from include\n"); + int diff = subtract(20, 7); + printf("subtract(20, 7) = %d\n", diff); + int prod = multiply(6, 4); + printf("multiply(6, 4) = %d\n", prod); + int quot = divide(15, 3); + printf("divide(15, 3) = %d\n", quot); + printf("PASS: All included functions work\n"); + + printf("Test 3: Using included functions in expressions\n"); + int result = add(multiply(3, 4), subtract(10, 5)); + printf("add(multiply(3, 4), subtract(10, 5)) = %d\n", result); + printf("PASS: Included functions in expressions work\n"); + + printf("\n=== All Include Tests Completed ===\n"); + return 0; +} diff --git a/tests/includes/math_lib.rc b/tests/includes/math_lib.rc new file mode 100644 index 0000000..294a363 --- /dev/null +++ b/tests/includes/math_lib.rc @@ -0,0 +1,18 @@ +int add(int a, int b) { + return a + b; +} + +int subtract(int a, int b) { + return a - b; +} + +int multiply(int a, int b) { + return a * b; +} + +int divide(int a, int b) { + if (b == 0) { + return 0; + } + return a / b; +} diff --git a/tests/includes/string_lib.rc b/tests/includes/string_lib.rc new file mode 100644 index 0000000..499fec9 --- /dev/null +++ b/tests/includes/string_lib.rc @@ -0,0 +1,19 @@ +int string_length(char *s) { + return strlen(s); +} + +char* string_upper(char *s) { + return upper(s); +} + +char* string_lower(char *s) { + return lower(s); +} + +int string_contains(char *haystack, char *needle) { + int pos = strpos(haystack, needle); + if (pos >= 0) { + return 1; + } + return 0; +} diff --git a/tests/includes/utils.rc b/tests/includes/utils.rc new file mode 100644 index 0000000..4d43019 --- /dev/null +++ b/tests/includes/utils.rc @@ -0,0 +1,14 @@ +#include "math_lib.rc" +#include "string_lib.rc" + +int is_even(int n) { + int half = divide(n, 2); + return multiply(half, 2) == n; +} + +int is_odd(int n) { + if (is_even(n)) { + return 0; + } + return 1; +} diff --git a/tests/nested_include_test.rc b/tests/nested_include_test.rc new file mode 100644 index 0000000..9ba48df --- /dev/null +++ b/tests/nested_include_test.rc @@ -0,0 +1,22 @@ +#include "includes/utils.rc" + +int main() { + printf("=== Nested Include Tests ===\n"); + + printf("Test 1: Functions from nested includes\n"); + int sum = add(5, 3); + printf("add(5, 3) = %d\n", sum); + printf("PASS: Functions from first-level include work\n"); + + printf("Test 2: Utility functions using nested includes\n"); + if (is_even(10)) { + printf("is_even(10) = true\n"); + } + if (is_odd(7)) { + printf("is_odd(7) = true\n"); + } + printf("PASS: Utility functions using nested includes work\n"); + + printf("\n=== All Nested Include Tests Completed ===\n"); + return 0; +}