summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormnik01 <maksimgrinberg01@gmail.com>2025-09-10 05:11:33 +0500
committermnik01 <maksimgrinberg01@gmail.com>2025-09-10 05:11:33 +0500
commitd8a7a5d5aeabdcb1e45ef4de3714d55b6b7e7cf7 (patch)
tree44ecd3082ea7f7d550a4457ebfc4c5dbf3ae75af
initial
-rw-r--r--.gitignore1
-rw-r--r--Makefile37
-rw-r--r--README.md337
-rw-r--r--draft.md22
-rw-r--r--main.c601
5 files changed, 998 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3ae7a33
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+jelly-cms
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..4bd922c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,37 @@
+CC=gcc
+CFLAGS=-Wall -Wextra -std=c99 -O2
+TARGET=jelly-cms
+SOURCE=main.c
+
+# Default target
+$(TARGET): $(SOURCE)
+ $(CC) $(CFLAGS) -o $(TARGET) $(SOURCE)
+
+# Clean build artifacts
+clean:
+ rm -f $(TARGET)
+ rm -rf build
+
+# Install (copy to /usr/local/bin)
+install: $(TARGET)
+ sudo cp $(TARGET) /usr/local/bin/
+
+# Uninstall
+uninstall:
+ sudo rm -f /usr/local/bin/$(TARGET)
+
+# Test build (run build command)
+test: $(TARGET)
+ ./$(TARGET) build
+
+# Help
+help:
+ @echo "Available targets:"
+ @echo " $(TARGET) - Build the jelly-cms executable"
+ @echo " clean - Remove build artifacts and executable"
+ @echo " install - Install jelly-cms to /usr/local/bin"
+ @echo " uninstall - Remove jelly-cms from /usr/local/bin"
+ @echo " test - Build and run jelly-cms build command"
+ @echo " help - Show this help message"
+
+.PHONY: clean install uninstall test help \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cd6dc62
--- /dev/null
+++ b/README.md
@@ -0,0 +1,337 @@
+# Jelly CMS
+
+A fast, lightweight static site generator written in C that supports multilingual content, template partials, and asset management.
+
+## Features
+
+- [x] Multilingual Support**: Automatic locale detection and site generation
+- [x] Template Partials**: Reusable HTML components with includes
+- [x] Zero Dependencies**: Single executable with no external requirements
+- [ ] Markdown support for content
+- [ ] CGI support for auth, captcha, comments features
+- [ ] Watch mode
+- [ ] Serve mode
+- [ ] Help command that shows readme
+
+## Quick Start
+
+### Installation
+
+1. Clone or download the project files (`main.c` and `Makefile`)
+2. Build the executable:
+ ```bash
+ make
+ ```
+
+### Project Structure
+
+```
+your-project/
+├── assets/ # Static assets (images, fonts, etc.)
+│ └── images/
+│ └── logo.png
+├── locale/ # Locale files (optional)
+│ ├── en.json
+│ ├── ru.json
+│ └── kk.json
+├── public/ # Public files copied as-is
+│ ├── robots.txt
+│ ├── sitemap.xml
+│ └── main.css
+├── src/
+│ ├── pages/ # Your HTML pages
+│ │ ├── index.html
+│ │ └── about/
+│ │ └── contacts.html
+│ └── partials/ # Reusable components
+│ ├── header.html
+│ └── footer.html
+├── vendor/ # Third-party libraries
+│ └── alpinejs.min.js
+└── jelly-cms # The built executable
+```
+
+### Build Your Site
+
+```bash
+./jelly-cms build
+```
+
+This creates a `build/` directory with your generated site.
+
+## Templating
+
+### Including Partials
+
+Use the include syntax to embed reusable components:
+
+```html
+<!DOCTYPE html>
+<html>
+<head>
+ <title>My Site</title>
+</head>
+<body>
+ <!-- %include.header% -->
+
+ <main>
+ <h1>Welcome to my site</h1>
+ </main>
+
+ <!-- %include.footer% -->
+</body>
+</html>
+```
+
+### Locale Variables
+
+Reference locale-specific content using the locale syntax:
+
+```html
+<h1>%locale.welcome_title%</h1>
+<p>Contact us: %locale.phone%</p>
+<address>%locale.address%</address>
+```
+
+## Localization
+
+### Setting Up Locales
+
+1. Create a `locale/` directory
+2. Add JSON files for each language (e.g., `en.json`, `ru.json`, `kk.json`)
+
+**Example `locale/en.json`:**
+```json
+{
+ "welcome_title": "Welcome to Our Website",
+ "phone": "+1 555 123 4567",
+ "address": "123 Main St, City, Country",
+ "nav_home": "Home",
+ "nav_about": "About",
+ "footer_copyright": "© 2024 My Company"
+}
+```
+
+**Example `locale/ru.json`:**
+```json
+{
+ "welcome_title": "Добро пожаловать на наш сайт",
+ "phone": "+7 123 456 7890",
+ "address": "ул. Главная 123, Город, Страна",
+ "nav_home": "Главная",
+ "nav_about": "О нас",
+ "footer_copyright": "© 2024 Моя Компания"
+}
+```
+
+### Build Output
+
+With locales, Jelly CMS generates:
+```
+build/
+├── en/
+│ ├── index.html
+│ └── about/
+│ └── contacts.html
+├── ru/
+│ ├── index.html
+│ └── about/
+│ └── contacts.html
+├── assets/
+│ └── images/
+│ └── logo.png
+├── vendor/
+│ └── alpinejs.min.js
+├── robots.txt
+├── sitemap.xml
+└── main.css
+```
+
+Without locales:
+```
+build/
+├── index.html
+├── about/
+│ └── contacts.html
+├── assets/
+├── vendor/
+├── robots.txt
+├── sitemap.xml
+└── main.css
+```
+
+## Directory Behavior
+
+| Source Directory | Build Behavior |
+|-----------------|----------------|
+| `assets/` | Copied to `build/assets/` |
+| `public/` | Contents copied directly to `build/` root |
+| `vendor/` | Copied to `build/vendor/` |
+| `src/pages/` | Processed as templates, structure preserved |
+| `src/partials/` | Used for includes, not copied to build |
+| `locale/` | Used for templating, not copied to build |
+
+## Example Templates
+
+### Main Page (`src/pages/index.html`)
+```html
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>%locale.site_title%</title>
+ <link rel="stylesheet" href="/main.css">
+</head>
+<body>
+ <!-- %include.header% -->
+
+ <main>
+ <h1>%locale.welcome_title%</h1>
+ <p>%locale.welcome_message%</p>
+ </main>
+
+ <!-- %include.footer% -->
+ <script src="/vendor/alpinejs.min.js"></script>
+</body>
+</html>
+```
+
+### Header Partial (`src/partials/header.html`)
+```html
+<header>
+ <nav>
+ <img src="/assets/images/logo.png" alt="Logo">
+ <ul>
+ <li><a href="/">%locale.nav_home%</a></li>
+ <li><a href="/about/">%locale.nav_about%</a></li>
+ </ul>
+ </nav>
+</header>
+```
+
+### Footer Partial (`src/partials/footer.html`)
+```html
+<footer>
+ <p>%locale.footer_copyright%</p>
+ <p>%locale.contact_info%</p>
+</footer>
+```
+
+## Build Commands
+
+### Basic Commands
+```bash
+# Build the site
+./jelly-cms build
+
+# Clean build artifacts
+make clean
+
+# Rebuild everything
+make clean && make && ./jelly-cms build
+```
+
+### Make Targets
+```bash
+make # Build jelly-cms executable
+make clean # Remove build artifacts
+make install # Install to /usr/local/bin (requires sudo)
+make uninstall # Remove from /usr/local/bin (requires sudo)
+make test # Build and run test
+make help # Show available targets
+```
+
+## Advanced Usage
+
+### Nested Partials
+Partials can include other partials:
+
+**`src/partials/layout.html`:**
+```html
+<!DOCTYPE html>
+<html>
+<head>
+ <!-- %include.meta% -->
+</head>
+<body>
+ <!-- %include.header% -->
+ <main>
+ <!-- Content will be here -->
+ </main>
+ <!-- %include.footer% -->
+</body>
+</html>
+```
+
+### Complex Directory Structures
+```
+src/pages/
+├── index.html
+├── blog/
+│ ├── index.html
+│ └── posts/
+│ ├── first-post.html
+│ └── second-post.html
+└── products/
+ ├── index.html
+ └── category/
+ └── item.html
+```
+
+This structure is preserved in the build output for each locale.
+
+## Troubleshooting
+
+### Common Issues
+
+**Segmentation Fault:**
+- Check that all referenced partials exist in `src/partials/`
+- Ensure locale JSON files are valid JSON
+- Verify file permissions for all source directories
+
+**Missing Files in Build:**
+- Ensure source directories exist (`src/pages/`, etc.)
+- Check file paths are correct (case-sensitive)
+- Verify JSON syntax in locale files
+
+**Locale Variables Not Replaced:**
+- Check JSON syntax in locale files
+- Ensure locale keys match exactly (case-sensitive)
+- Verify locale files are in `locale/` directory
+
+### Debug Mode
+Run with verbose output to see detailed processing:
+```bash
+./jelly-cms build 2>&1 | tee build.log
+```
+
+### File Size Limits
+- Maximum file size: 64KB per template
+- Maximum locale entries: 100 per language
+- Maximum locales: 10
+
+## Performance
+
+- **Build Speed**: Processes hundreds of pages in milliseconds
+- **Memory Usage**: Minimal RAM footprint (~1-2MB)
+- **File Size**: Single ~50KB executable
+- **Dependencies**: None (statically linked)
+
+## License
+
+This project is provided as-is. Feel free to modify and distribute according to your needs.
+
+## Contributing
+
+Since this is a simple C program, contributions are welcome:
+1. Fork the project
+2. Make your changes to `main.c`
+3. Test thoroughly
+4. Submit a pull request
+
+For bug reports or feature requests, please provide:
+- Your project structure
+- Locale files (if using multilingual features)
+- Error messages or unexpected behavior
+- Operating system and compiler version
diff --git a/draft.md b/draft.md
new file mode 100644
index 0000000..8402b52
--- /dev/null
+++ b/draft.md
@@ -0,0 +1,22 @@
+conflict betwee content and pages then do not build and show error
+on templates files *.html
+with syntax
+
+<title>%content.title%</title>
+
+
+if markdown file in frontmatter table dont have this meta info then fail build with error
+markdown example:
+
+---
+title: Телефоны
+
+---
+
+# Hello
+
+![img](/assets/images/car.png)
+
+
+TODO: продумать локали (локаль по папкам файлов мд или по названию файла или в контенте во фронтматтере) и конфликты и возможно есть локаль в страницах но нет в контенте
+мб сделать два режима без локали и с локалью
diff --git a/main.c b/main.c
new file mode 100644
index 0000000..2e90104
--- /dev/null
+++ b/main.c
@@ -0,0 +1,601 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <dirent.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <unistd.h>
+
+#define MAX_PATH 1024
+#define MAX_CONTENT 65536
+#define MAX_LOCALES 10
+#define MAX_LOCALE_ENTRIES 100
+
+typedef struct {
+ char key[256];
+ char value[1024];
+} LocaleEntry;
+
+typedef struct {
+ char code[8];
+ LocaleEntry entries[MAX_LOCALE_ENTRIES];
+ int count;
+} Locale;
+
+typedef struct {
+ Locale locales[MAX_LOCALES];
+ int count;
+ int has_locales;
+} LocaleData;
+
+// Safe string copy
+void safe_strcpy(char *dest, const char *src, size_t dest_size) {
+ if (!dest || !src || dest_size == 0) return;
+ strncpy(dest, src, dest_size - 1);
+ dest[dest_size - 1] = '\0';
+}
+
+// Utility functions
+int create_directory(const char *path) {
+ if (!path) return -1;
+
+ char tmp[MAX_PATH];
+ char *p = NULL;
+ size_t len;
+
+ safe_strcpy(tmp, path, sizeof(tmp));
+ len = strlen(tmp);
+ if (len > 0 && tmp[len - 1] == '/')
+ tmp[len - 1] = 0;
+
+ for (p = tmp + 1; *p; p++) {
+ if (*p == '/') {
+ *p = 0;
+ if (mkdir(tmp, 0755) != 0 && errno != EEXIST) {
+ return -1;
+ }
+ *p = '/';
+ }
+ }
+ if (mkdir(tmp, 0755) != 0 && errno != EEXIST) {
+ return -1;
+ }
+ return 0;
+}
+
+int copy_file(const char *src, const char *dest) {
+ if (!src || !dest) return -1;
+
+ FILE *source = fopen(src, "rb");
+ if (!source) {
+ printf("Warning: Could not open source file: %s\n", src);
+ return -1;
+ }
+
+ // Create directory for destination file
+ char dest_dir[MAX_PATH];
+ safe_strcpy(dest_dir, dest, sizeof(dest_dir));
+ char *last_slash = strrchr(dest_dir, '/');
+ if (last_slash) {
+ *last_slash = '\0';
+ create_directory(dest_dir);
+ }
+
+ FILE *target = fopen(dest, "wb");
+ if (!target) {
+ printf("Warning: Could not create destination file: %s\n", dest);
+ fclose(source);
+ return -1;
+ }
+
+ char buffer[4096];
+ size_t bytes;
+ while ((bytes = fread(buffer, 1, sizeof(buffer), source)) > 0) {
+ fwrite(buffer, 1, bytes, target);
+ }
+
+ fclose(source);
+ fclose(target);
+ return 0;
+}
+
+int copy_directory_recursive(const char *src, const char *dest) {
+ if (!src || !dest) return -1;
+
+ DIR *dir = opendir(src);
+ if (!dir) {
+ printf("Warning: Could not open directory: %s\n", src);
+ return -1;
+ }
+
+ if (create_directory(dest) != 0) {
+ printf("Warning: Could not create directory: %s\n", dest);
+ closedir(dir);
+ return -1;
+ }
+
+ struct dirent *entry;
+ while ((entry = readdir(dir)) != NULL) {
+ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
+ continue;
+
+ char src_path[MAX_PATH];
+ char dest_path[MAX_PATH];
+ snprintf(src_path, sizeof(src_path), "%s/%s", src, entry->d_name);
+ snprintf(dest_path, sizeof(dest_path), "%s/%s", dest, entry->d_name);
+
+ struct stat statbuf;
+ if (stat(src_path, &statbuf) == 0) {
+ if (S_ISDIR(statbuf.st_mode)) {
+ copy_directory_recursive(src_path, dest_path);
+ } else {
+ copy_file(src_path, dest_path);
+ }
+ }
+ }
+
+ closedir(dir);
+ return 0;
+}
+
+int load_locale_file(const char *filepath, Locale *locale) {
+ if (!filepath || !locale) return -1;
+
+ FILE *file = fopen(filepath, "r");
+ if (!file) {
+ printf("Warning: Could not open locale file: %s\n", filepath);
+ return -1;
+ }
+
+ fseek(file, 0, SEEK_END);
+ long length = ftell(file);
+ fseek(file, 0, SEEK_SET);
+
+ if (length <= 0 || length > 100000) {
+ printf("Warning: Invalid file size for locale: %s\n", filepath);
+ fclose(file);
+ return -1;
+ }
+
+ char *content = malloc(length + 1);
+ if (!content) {
+ printf("Error: Memory allocation failed\n");
+ fclose(file);
+ return -1;
+ }
+
+ size_t read_bytes = fread(content, 1, length, file);
+ content[read_bytes] = '\0';
+ fclose(file);
+
+ // Simple JSON parsing - extract key-value pairs
+ locale->count = 0;
+ char *pos = content;
+
+ while (*pos && locale->count < MAX_LOCALE_ENTRIES) {
+ // Find opening quote for key
+ char *quote1 = strchr(pos, '"');
+ if (!quote1) break;
+ quote1++;
+
+ // Find closing quote for key
+ char *quote2 = strchr(quote1, '"');
+ if (!quote2) break;
+
+ size_t key_len = quote2 - quote1;
+ if (key_len >= sizeof(locale->entries[0].key) || key_len == 0) {
+ pos = quote2 + 1;
+ continue;
+ }
+
+ // Copy key
+ strncpy(locale->entries[locale->count].key, quote1, key_len);
+ locale->entries[locale->count].key[key_len] = '\0';
+
+ // Find colon
+ char *colon = strchr(quote2, ':');
+ if (!colon) {
+ pos = quote2 + 1;
+ continue;
+ }
+
+ // Find opening quote for value
+ char *quote3 = strchr(colon, '"');
+ if (!quote3) {
+ pos = quote2 + 1;
+ continue;
+ }
+ quote3++;
+
+ // Find closing quote for value
+ char *quote4 = strchr(quote3, '"');
+ if (!quote4) {
+ pos = quote3;
+ continue;
+ }
+
+ size_t value_len = quote4 - quote3;
+ if (value_len >= sizeof(locale->entries[0].value)) {
+ pos = quote4 + 1;
+ continue;
+ }
+
+ // Copy value
+ strncpy(locale->entries[locale->count].value, quote3, value_len);
+ locale->entries[locale->count].value[value_len] = '\0';
+
+ locale->count++;
+ pos = quote4 + 1;
+ }
+
+ free(content);
+ printf("Loaded locale with %d entries from %s\n", locale->count, filepath);
+ return 0;
+}
+
+int load_locales(LocaleData *locale_data) {
+ if (!locale_data) return -1;
+
+ locale_data->has_locales = 0;
+ locale_data->count = 0;
+
+ DIR *dir = opendir("locale");
+ if (!dir) {
+ printf("No locale directory found\n");
+ return 0;
+ }
+
+ locale_data->has_locales = 1;
+
+ struct dirent *entry;
+ while ((entry = readdir(dir)) != NULL && locale_data->count < MAX_LOCALES) {
+ if (strstr(entry->d_name, ".json")) {
+ char filepath[MAX_PATH];
+ snprintf(filepath, sizeof(filepath), "locale/%s", entry->d_name);
+
+ // Extract locale code from filename
+ char locale_code[256];
+ safe_strcpy(locale_code, entry->d_name, sizeof(locale_code));
+ char *dot = strrchr(locale_code, '.');
+ if (dot) *dot = '\0';
+
+ if (strlen(locale_code) < sizeof(locale_data->locales[0].code)) {
+ safe_strcpy(locale_data->locales[locale_data->count].code, locale_code,
+ sizeof(locale_data->locales[locale_data->count].code));
+
+ if (load_locale_file(filepath, &locale_data->locales[locale_data->count]) == 0) {
+ locale_data->count++;
+ }
+ }
+ }
+ }
+
+ closedir(dir);
+ return 0;
+}
+
+char* get_locale_value(const Locale *locale, const char *key) {
+ if (!locale || !key) return NULL;
+
+ for (int i = 0; i < locale->count; i++) {
+ if (strcmp(locale->entries[i].key, key) == 0) {
+ return locale->entries[i].value;
+ }
+ }
+ return NULL;
+}
+
+char* load_file(const char *path) {
+ if (!path) return NULL;
+
+ FILE *file = fopen(path, "r");
+ if (!file) return NULL;
+
+ fseek(file, 0, SEEK_END);
+ long length = ftell(file);
+ fseek(file, 0, SEEK_SET);
+
+ if (length <= 0 || length > MAX_CONTENT) {
+ fclose(file);
+ return NULL;
+ }
+
+ char *content = malloc(length + 1);
+ if (!content) {
+ fclose(file);
+ return NULL;
+ }
+
+ size_t read_bytes = fread(content, 1, length, file);
+ content[read_bytes] = '\0';
+ fclose(file);
+
+ return content;
+}
+
+char* load_partial(const char *name) {
+ if (!name) return NULL;
+
+ char path[MAX_PATH];
+ snprintf(path, sizeof(path), "src/partials/%s.html", name);
+ return load_file(path);
+}
+
+char* process_template(const char *content, const Locale *locale) {
+ if (!content) return NULL;
+
+ size_t content_len = strlen(content);
+ char *result = malloc(MAX_CONTENT);
+ if (!result) return NULL;
+
+ char *output = result;
+ const char *input = content;
+ size_t remaining = MAX_CONTENT - 1;
+
+ while (*input && remaining > 1) {
+ if (strncmp(input, "<!-- %include.", 14) == 0) {
+ // Handle includes
+ input += 14;
+ const char *end = strstr(input, "% -->");
+ if (end) {
+ size_t name_len = end - input;
+ if (name_len < 255) {
+ char partial_name[256];
+ strncpy(partial_name, input, name_len);
+ partial_name[name_len] = '\0';
+
+ char *partial_content = load_partial(partial_name);
+ if (partial_content) {
+ char *processed_partial = process_template(partial_content, locale);
+ if (processed_partial) {
+ size_t partial_len = strlen(processed_partial);
+ if (partial_len < remaining) {
+ strcpy(output, processed_partial);
+ output += partial_len;
+ remaining -= partial_len;
+ }
+ free(processed_partial);
+ }
+ free(partial_content);
+ }
+ }
+ input = end + 5;
+ } else {
+ *output++ = *input++;
+ remaining--;
+ }
+ } else if (strncmp(input, "%locale.", 8) == 0) {
+ // Handle locale variables
+ input += 8;
+ const char *end = strchr(input, '%');
+ if (end && locale) {
+ size_t key_len = end - input;
+ if (key_len < 255) {
+ char key[256];
+ strncpy(key, input, key_len);
+ key[key_len] = '\0';
+
+ char *value = get_locale_value(locale, key);
+ if (value) {
+ size_t value_len = strlen(value);
+ if (value_len < remaining) {
+ strcpy(output, value);
+ output += value_len;
+ remaining -= value_len;
+ }
+ }
+ }
+ input = end + 1;
+ } else {
+ *output++ = *input++;
+ remaining--;
+ }
+ } else {
+ *output++ = *input++;
+ remaining--;
+ }
+ }
+
+ *output = '\0';
+ return result;
+}
+
+int process_pages_directory(const char *src_dir, const char *build_dir,
+ const char *relative_path, const LocaleData *locale_data) {
+ if (!src_dir || !build_dir || !relative_path || !locale_data) return -1;
+
+ char full_src_path[MAX_PATH];
+ if (strlen(relative_path) > 0) {
+ snprintf(full_src_path, sizeof(full_src_path), "%s/%s", src_dir, relative_path);
+ } else {
+ safe_strcpy(full_src_path, src_dir, sizeof(full_src_path));
+ }
+
+ DIR *dir = opendir(full_src_path);
+ if (!dir) {
+ printf("Warning: Could not open pages directory: %s\n", full_src_path);
+ return -1;
+ }
+
+ struct dirent *entry;
+ while ((entry = readdir(dir)) != NULL) {
+ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
+ continue;
+
+ char src_path[MAX_PATH];
+ char rel_path[MAX_PATH];
+
+ snprintf(src_path, sizeof(src_path), "%s/%s", full_src_path, entry->d_name);
+
+ if (strlen(relative_path) > 0) {
+ snprintf(rel_path, sizeof(rel_path), "%s/%s", relative_path, entry->d_name);
+ } else {
+ safe_strcpy(rel_path, entry->d_name, sizeof(rel_path));
+ }
+
+ struct stat statbuf;
+ if (stat(src_path, &statbuf) == 0) {
+ if (S_ISDIR(statbuf.st_mode)) {
+ process_pages_directory(src_dir, build_dir, rel_path, locale_data);
+ } else if (strstr(entry->d_name, ".html")) {
+ printf("Processing: %s\n", rel_path);
+
+ char *content = load_file(src_path);
+ if (!content) {
+ printf("Warning: Could not load file: %s\n", src_path);
+ continue;
+ }
+
+ if (locale_data->has_locales && locale_data->count > 0) {
+ for (int i = 0; i < locale_data->count; i++) {
+ char output_path[MAX_PATH];
+ snprintf(output_path, sizeof(output_path), "%s/%s/%s",
+ build_dir, locale_data->locales[i].code, rel_path);
+
+ // Create directory structure
+ char output_dir[MAX_PATH];
+ safe_strcpy(output_dir, output_path, sizeof(output_dir));
+ char *last_slash = strrchr(output_dir, '/');
+ if (last_slash) {
+ *last_slash = '\0';
+ create_directory(output_dir);
+ }
+
+ char *processed = process_template(content, &locale_data->locales[i]);
+ if (processed) {
+ FILE *output_file = fopen(output_path, "w");
+ if (output_file) {
+ fputs(processed, output_file);
+ fclose(output_file);
+ } else {
+ printf("Warning: Could not write file: %s\n", output_path);
+ }
+ free(processed);
+ }
+ }
+ } else {
+ char output_path[MAX_PATH];
+ snprintf(output_path, sizeof(output_path), "%s/%s", build_dir, rel_path);
+
+ // Create directory structure
+ char output_dir[MAX_PATH];
+ safe_strcpy(output_dir, output_path, sizeof(output_dir));
+ char *last_slash = strrchr(output_dir, '/');
+ if (last_slash) {
+ *last_slash = '\0';
+ create_directory(output_dir);
+ }
+
+ char *processed = process_template(content, NULL);
+ if (processed) {
+ FILE *output_file = fopen(output_path, "w");
+ if (output_file) {
+ fputs(processed, output_file);
+ fclose(output_file);
+ } else {
+ printf("Warning: Could not write file: %s\n", output_path);
+ }
+ free(processed);
+ }
+ }
+
+ free(content);
+ }
+ }
+ }
+
+ closedir(dir);
+ return 0;
+}
+
+int main(int argc, char *argv[]) {
+ if (argc != 2 || strcmp(argv[1], "build") != 0) {
+ printf("Usage: %s build\n", argv[0]);
+ return 1;
+ }
+
+ printf("Building Jelly CMS...\n");
+
+ // Create build directory
+ if (create_directory("build") != 0) {
+ printf("Error: Could not create build directory\n");
+ return 1;
+ }
+
+ // Copy vendor directory
+ if (access("vendor", F_OK) == 0) {
+ printf("Copying vendor directory...\n");
+ copy_directory_recursive("vendor", "build/vendor");
+ } else {
+ printf("No vendor directory found\n");
+ }
+
+ // Copy public directory contents
+ if (access("public", F_OK) == 0) {
+ printf("Copying public directory...\n");
+ DIR *public_dir = opendir("public");
+ if (public_dir) {
+ struct dirent *entry;
+ while ((entry = readdir(public_dir)) != NULL) {
+ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
+ continue;
+
+ char src_path[MAX_PATH];
+ char dest_path[MAX_PATH];
+ snprintf(src_path, sizeof(src_path), "public/%s", entry->d_name);
+ snprintf(dest_path, sizeof(dest_path), "build/%s", entry->d_name);
+
+ struct stat statbuf;
+ if (stat(src_path, &statbuf) == 0) {
+ if (S_ISDIR(statbuf.st_mode)) {
+ copy_directory_recursive(src_path, dest_path);
+ } else {
+ copy_file(src_path, dest_path);
+ }
+ }
+ }
+ closedir(public_dir);
+ }
+ } else {
+ printf("No public directory found\n");
+ }
+
+ // Copy assets directory
+ if (access("assets", F_OK) == 0) {
+ printf("Copying assets directory...\n");
+ copy_directory_recursive("assets", "build/assets");
+ } else {
+ printf("No assets directory found\n");
+ }
+
+ // Load locales
+ LocaleData locale_data;
+ if (load_locales(&locale_data) != 0) {
+ printf("Error loading locales\n");
+ return 1;
+ }
+
+ if (locale_data.has_locales && locale_data.count > 0) {
+ printf("Found %d locales: ", locale_data.count);
+ for (int i = 0; i < locale_data.count; i++) {
+ printf("%s ", locale_data.locales[i].code);
+ }
+ printf("\n");
+ } else {
+ printf("No locales found, building single language version\n");
+ }
+
+ // Process pages
+ if (access("src/pages", F_OK) == 0) {
+ printf("Processing pages...\n");
+ if (process_pages_directory("src/pages", "build", "", &locale_data) != 0) {
+ printf("Error processing pages\n");
+ return 1;
+ }
+ } else {
+ printf("No src/pages directory found\n");
+ }
+
+ printf("Build completed successfully!\n");
+ return 0;
+} \ No newline at end of file