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.
This commit is contained in:
retoor 2025-11-23 14:23:59 +00:00
parent 77beada5a6
commit cfab51abd8
9 changed files with 394 additions and 15 deletions

View File

@ -16,7 +16,8 @@ SOURCES = $(SRC_DIR)/main.c \
$(SRC_DIR)/parser.c \ $(SRC_DIR)/parser.c \
$(SRC_DIR)/interpreter.c \ $(SRC_DIR)/interpreter.c \
$(SRC_DIR)/native_functions.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) 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)/comparison_test.rc \
$(TEST_DIR)/functions_test.rc \ $(TEST_DIR)/functions_test.rc \
$(TEST_DIR)/trig_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 \ EXAMPLES = $(EXAMPLE_DIR)/http_simple.rc \
$(EXAMPLE_DIR)/http_persistent.rc \ $(EXAMPLE_DIR)/http_persistent.rc \
@ -113,6 +116,12 @@ test: $(TARGET)
@echo "Running file seek tests..." @echo "Running file seek tests..."
@$(TARGET) $(TEST_DIR)/file_seek_test.rc 2>&1 | grep -v "Error at token" || true @$(TARGET) $(TEST_DIR)/file_seek_test.rc 2>&1 | grep -v "Error at token" || true
@echo "" @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 ===" @echo "=== All Tests Completed ==="
run-feature-test: $(TARGET) run-feature-test: $(TARGET)
@ -166,6 +175,12 @@ run-trig-test: $(TARGET)
run-file-seek-test: $(TARGET) run-file-seek-test: $(TARGET)
$(TARGET) $(TEST_DIR)/file_seek_test.rc $(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) run-http-simple: $(TARGET)
$(TARGET) $(EXAMPLE_DIR)/http_simple.rc $(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-functions-test - Run functions_test.rc (function calls & recursion)"
@echo " make run-trig-test - Run trig_test.rc (sin, cos, tan)" @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-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 ""
@echo "Examples:" @echo "Examples:"
@echo " make run-http-simple - Run http_simple.rc (single connection HTTP server)" @echo " make run-http-simple - Run http_simple.rc (single connection HTTP server)"

View File

@ -1,9 +1,12 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <libgen.h>
#include <string.h>
#include "types.h" #include "types.h"
#include "tokenizer.h" #include "tokenizer.h"
#include "interpreter.h" #include "interpreter.h"
#include "native_functions.h" #include "native_functions.h"
#include "preprocessor.h"
int main(int argc, char **argv) { int main(int argc, char **argv) {
if (argc < 2) { if (argc < 2) {
@ -11,24 +14,21 @@ int main(int argc, char **argv) {
return 1; return 1;
} }
FILE *f = fopen(argv[1], "rb"); char file_path[512];
if (!f) { strncpy(file_path, argv[1], 511);
printf("Could not open file.\n"); file_path[511] = 0;
return 1;
}
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) { if (!src_code) {
printf("Memory allocation failed.\n"); printf("Preprocessing failed.\n");
fclose(f);
return 1; 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(); register_native_functions();
tokenize(src_code); tokenize(src_code);

247
src/preprocessor.c Normal file
View File

@ -0,0 +1,247 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <libgen.h>
#include <unistd.h>
#include <sys/stat.h>
#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;
}

15
src/preprocessor.h Normal file
View File

@ -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

27
tests/include_test.rc Normal file
View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

14
tests/includes/utils.rc Normal file
View File

@ -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;
}

View File

@ -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;
}