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:
parent
77beada5a6
commit
cfab51abd8
21
Makefile
21
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)"
|
||||
|
||||
26
src/main.c
26
src/main.c
@ -1,9 +1,12 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <libgen.h>
|
||||
#include <string.h>
|
||||
#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);
|
||||
|
||||
247
src/preprocessor.c
Normal file
247
src/preprocessor.c
Normal 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
15
src/preprocessor.h
Normal 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
27
tests/include_test.rc
Normal 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;
|
||||
}
|
||||
18
tests/includes/math_lib.rc
Normal file
18
tests/includes/math_lib.rc
Normal 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;
|
||||
}
|
||||
19
tests/includes/string_lib.rc
Normal file
19
tests/includes/string_lib.rc
Normal 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
14
tests/includes/utils.rc
Normal 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;
|
||||
}
|
||||
22
tests/nested_include_test.rc
Normal file
22
tests/nested_include_test.rc
Normal 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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user