aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Cli/Cli.c102
-rw-r--r--src/Cli/Cli.h30
-rw-r--r--src/DirectoryWalker/DirectoryWalker.c249
-rw-r--r--src/DirectoryWalker/DirectoryWalker.h8
-rw-r--r--src/Dom/Dom.c303
-rw-r--r--src/Dom/Dom.h10
-rw-r--r--src/FileProcessor/FileProcessor.c396
-rw-r--r--src/FileProcessor/FileProcessor.h8
-rw-r--r--src/Logger/Logger.c102
-rw-r--r--src/Logger/Logger.h31
-rw-r--r--src/Logger/LuaLogger.c60
-rw-r--r--src/Logger/LuaLogger.h8
-rw-r--r--src/LuaEngine/LuaEngine.c273
-rw-r--r--src/LuaEngine/LuaEngine.h8
-rw-r--r--src/Main.c52
-rw-r--r--src/Path/Path.c185
-rw-r--r--src/Path/Path.h10
-rw-r--r--src/Version/Version.c3
-rw-r--r--src/Version/Version.h6
19 files changed, 1844 insertions, 0 deletions
diff --git a/src/Cli/Cli.c b/src/Cli/Cli.c
new file mode 100644
index 0000000..3648493
--- /dev/null
+++ b/src/Cli/Cli.c
@@ -0,0 +1,102 @@
+#include "Cli/Cli.h"
+
+#include "Logger/Logger.h"
+#include "Version/Version.h"
+
+#include <getopt.h>
+#include <stdio.h>
+
+void cli_print_usage(const char *program_name) {
+ fprintf(
+ stderr,
+ "Usage:\n"
+ " %s FILE [-o OUTPUT]\n"
+ " %s -d DIRECTORY [-o OUTPUT] [-f]\n\n"
+ "Options:\n"
+ " -d, --directory DIRECTORY Process a directory recursively\n"
+ " -o, --output OUTPUT Set the output path\n"
+ " -f, --overwrite Overwrite an existing output directory\n"
+ " -l, --log-level LEVEL error, warning, info, debug, or silent\n"
+ " -v, --version Show the program version\n"
+ " -h, --help Show this help\n",
+ program_name, program_name);
+}
+
+static void cli_print_version(void) { printf("yapssg %s\n", yapssg_version); }
+
+static const char *strip_optional_equals(const char *value) {
+ return value != NULL && value[0] == '=' ? value + 1 : value;
+}
+
+CliParseResult cli_parse(int argc, char **argv, CliOptions *options) {
+ static const struct option long_options[] = {
+ {"directory", required_argument, NULL, 'd'},
+ {"output", required_argument, NULL, 'o'},
+ {"overwrite", no_argument, NULL, 'f'},
+ {"force", no_argument, NULL, 'f'},
+ {"log-level", required_argument, NULL, 'l'},
+ {"version", no_argument, NULL, 'v'},
+ {"help", no_argument, NULL, 'h'},
+ {NULL, 0, NULL, 0}};
+ const char *directory = NULL;
+ int option;
+
+ options->input_type = INPUT_TYPE_NONE;
+ options->input_path = NULL;
+ options->output_path = NULL;
+ options->overwrite_output = 0;
+ options->log_level = LOG_LEVEL_INFO;
+
+ while ((option = getopt_long(argc, argv, "d:o:fl:vh", long_options, NULL)) !=
+ -1) {
+ switch (option) {
+ case 'd':
+ directory = strip_optional_equals(optarg);
+ break;
+ case 'o':
+ options->output_path = strip_optional_equals(optarg);
+ break;
+ case 'f':
+ options->overwrite_output = 1;
+ break;
+ case 'l':
+ if (logger_parse_level(optarg, &options->log_level) != 0) {
+ logger_error("unknown log level '%s'", optarg);
+ return CLI_PARSE_ERROR;
+ }
+ break;
+ case 'v':
+ cli_print_version();
+ return CLI_PARSE_VERSION;
+ case 'h':
+ cli_print_usage(argv[0]);
+ return CLI_PARSE_HELP;
+ default:
+ cli_print_usage(argv[0]);
+ return CLI_PARSE_ERROR;
+ }
+ }
+
+ if (directory != NULL) {
+ if (optind != argc || directory[0] == '\0') {
+ logger_error("a directory cannot be combined with a file");
+ return CLI_PARSE_ERROR;
+ }
+ options->input_type = INPUT_TYPE_DIRECTORY;
+ options->input_path = directory;
+ } else {
+ if (optind + 1 != argc) {
+ cli_print_usage(argv[0]);
+ return CLI_PARSE_ERROR;
+ }
+ options->input_type = INPUT_TYPE_FILE;
+ options->input_path = argv[optind];
+ }
+
+ if (options->output_path != NULL && options->output_path[0] == '\0') {
+ logger_error("output path cannot be empty");
+ return CLI_PARSE_ERROR;
+ }
+
+ return CLI_PARSE_OK;
+}
diff --git a/src/Cli/Cli.h b/src/Cli/Cli.h
new file mode 100644
index 0000000..9695fd4
--- /dev/null
+++ b/src/Cli/Cli.h
@@ -0,0 +1,30 @@
+#ifndef YAPSSG_CLI_H
+#define YAPSSG_CLI_H
+
+#include "Logger/Logger.h"
+
+typedef enum {
+ INPUT_TYPE_NONE,
+ INPUT_TYPE_FILE,
+ INPUT_TYPE_DIRECTORY
+} InputType;
+
+typedef enum {
+ CLI_PARSE_ERROR = -1,
+ CLI_PARSE_OK,
+ CLI_PARSE_HELP,
+ CLI_PARSE_VERSION
+} CliParseResult;
+
+typedef struct {
+ InputType input_type;
+ const char *input_path;
+ const char *output_path;
+ int overwrite_output;
+ LogLevel log_level;
+} CliOptions;
+
+CliParseResult cli_parse(int argc, char **argv, CliOptions *options);
+void cli_print_usage(const char *program_name);
+
+#endif
diff --git a/src/DirectoryWalker/DirectoryWalker.c b/src/DirectoryWalker/DirectoryWalker.c
new file mode 100644
index 0000000..dc3729a
--- /dev/null
+++ b/src/DirectoryWalker/DirectoryWalker.c
@@ -0,0 +1,249 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "DirectoryWalker/DirectoryWalker.h"
+
+#include "Path/Path.h"
+
+#include <dirent.h>
+#include <errno.h>
+#include <lauxlib.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#define WALKER_METATABLE "yapssg.directory_walker"
+
+typedef struct {
+ DIR *directory;
+ char *absolute_path;
+ char *display_path;
+ size_t depth;
+} WalkFrame;
+
+typedef struct {
+ WalkFrame *frames;
+ size_t length;
+ size_t capacity;
+} DirectoryWalker;
+
+static void close_frame(WalkFrame *frame) {
+ if (frame->directory != NULL) {
+ closedir(frame->directory);
+ }
+ free(frame->absolute_path);
+ free(frame->display_path);
+}
+
+static void clear_walker(DirectoryWalker *walker) {
+ while (walker->length > 0) {
+ close_frame(&walker->frames[--walker->length]);
+ }
+ free(walker->frames);
+ walker->frames = NULL;
+ walker->capacity = 0;
+}
+
+static int walker_gc(lua_State *lua) {
+ DirectoryWalker *walker = lua_touserdata(lua, 1);
+
+ clear_walker(walker);
+ return 0;
+}
+
+static int grow_walker(DirectoryWalker *walker) {
+ size_t larger_capacity;
+ WalkFrame *larger_frames;
+
+ if (walker->capacity > SIZE_MAX / (2 * sizeof(*walker->frames))) {
+ errno = ENOMEM;
+ return -1;
+ }
+
+ larger_capacity = walker->capacity == 0 ? 8 : walker->capacity * 2;
+ larger_frames =
+ realloc(walker->frames, larger_capacity * sizeof(*walker->frames));
+ if (larger_frames == NULL) {
+ errno = ENOMEM;
+ return -1;
+ }
+
+ walker->frames = larger_frames;
+ walker->capacity = larger_capacity;
+ return 0;
+}
+
+static int push_frame(DirectoryWalker *walker, char *absolute_path,
+ char *display_path, size_t depth) {
+ DIR *directory = opendir(absolute_path);
+ WalkFrame *frame;
+
+ if (directory == NULL) {
+ return -1;
+ }
+ if (walker->length == walker->capacity && grow_walker(walker) != 0) {
+ int error_number = errno;
+
+ closedir(directory);
+ errno = error_number;
+ return -1;
+ }
+
+ frame = &walker->frames[walker->length++];
+ frame->directory = directory;
+ frame->absolute_path = absolute_path;
+ frame->display_path = display_path;
+ frame->depth = depth;
+ return 0;
+}
+
+static const char *entry_type(mode_t mode) {
+ if (S_ISREG(mode)) {
+ return "file";
+ }
+ if (S_ISDIR(mode)) {
+ return "directory";
+ }
+ if (S_ISLNK(mode)) {
+ return "symlink";
+ }
+ return "other";
+}
+
+static void push_entry(lua_State *lua, const char *path, const char *name,
+ const struct stat *status, size_t depth) {
+ lua_newtable(lua);
+
+ lua_pushstring(lua, path);
+ lua_setfield(lua, -2, "path");
+
+ lua_pushstring(lua, name);
+ lua_setfield(lua, -2, "name");
+
+ lua_pushstring(lua, entry_type(status->st_mode));
+ lua_setfield(lua, -2, "type");
+
+ lua_pushnumber(lua, (lua_Number)depth);
+ lua_setfield(lua, -2, "depth");
+
+ if (S_ISREG(status->st_mode)) {
+ lua_pushnumber(lua, (lua_Number)status->st_size);
+ lua_setfield(lua, -2, "size");
+ }
+}
+
+static int walker_next(lua_State *lua) {
+ DirectoryWalker *walker = lua_touserdata(lua, lua_upvalueindex(1));
+
+ while (walker->length > 0) {
+ WalkFrame *frame = &walker->frames[walker->length - 1];
+ struct dirent *entry;
+ char *absolute_path;
+ char *display_path;
+ struct stat status;
+ size_t depth;
+
+ errno = 0;
+ entry = readdir(frame->directory);
+ if (entry == NULL) {
+ int error_number = errno;
+
+ close_frame(frame);
+ --walker->length;
+ if (error_number != 0) {
+ clear_walker(walker);
+ return luaL_error(lua, "cannot read directory while walking: %s",
+ strerror(error_number));
+ }
+ continue;
+ }
+ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
+ continue;
+ }
+
+ absolute_path = path_join(frame->absolute_path, entry->d_name);
+ display_path = path_join(frame->display_path, entry->d_name);
+ if (absolute_path == NULL || display_path == NULL) {
+ free(absolute_path);
+ free(display_path);
+ clear_walker(walker);
+ return luaL_error(lua, "unable to allocate directory-walk path");
+ }
+ if (lstat(absolute_path, &status) != 0) {
+ int error_number = errno;
+
+ free(absolute_path);
+ free(display_path);
+ clear_walker(walker);
+ return luaL_error(lua, "cannot inspect directory entry: %s",
+ strerror(error_number));
+ }
+
+ depth = frame->depth + 1;
+ push_entry(lua, display_path, entry->d_name, &status, depth);
+
+ if (S_ISDIR(status.st_mode)) {
+ if (push_frame(walker, absolute_path, display_path, depth) != 0) {
+ int error_number = errno;
+
+ free(absolute_path);
+ free(display_path);
+ clear_walker(walker);
+ return luaL_error(lua, "cannot open directory while walking: %s",
+ strerror(error_number));
+ }
+ } else {
+ free(absolute_path);
+ free(display_path);
+ }
+ return 1;
+ }
+ clear_walker(walker);
+ return 0;
+}
+
+static int walk(lua_State *lua) {
+ const char *input_path = lua_tostring(lua, lua_upvalueindex(1));
+ const char *requested_path = luaL_checkstring(lua, 1);
+ char *absolute_path = path_resolve_from_file(input_path, requested_path);
+ char *display_path = path_join("", requested_path);
+ DirectoryWalker *walker;
+
+ if (absolute_path == NULL || display_path == NULL) {
+ free(absolute_path);
+ free(display_path);
+ return luaL_error(lua, "unable to allocate directory-walk root");
+ }
+
+ walker = lua_newuserdata(lua, sizeof(*walker));
+ memset(walker, 0, sizeof(*walker));
+ luaL_getmetatable(lua, WALKER_METATABLE);
+ lua_setmetatable(lua, -2);
+
+ if (push_frame(walker, absolute_path, display_path, 0) != 0) {
+ int error_number = errno;
+
+ free(absolute_path);
+ free(display_path);
+ return luaL_error(lua, "cannot open directory '%s': %s", requested_path,
+ strerror(error_number));
+ }
+
+ lua_pushcclosure(lua, walker_next, 1);
+ return 1;
+}
+
+void directory_walker_register(lua_State *lua, const char *input_path) {
+ if (luaL_newmetatable(lua, WALKER_METATABLE)) {
+ lua_pushcfunction(lua, walker_gc);
+ lua_setfield(lua, -2, "__gc");
+ }
+ lua_pop(lua, 1);
+
+ lua_newtable(lua);
+ lua_pushstring(lua, input_path);
+ lua_pushcclosure(lua, walk, 1);
+ lua_setfield(lua, -2, "walk");
+ lua_setglobal(lua, "fs");
+}
diff --git a/src/DirectoryWalker/DirectoryWalker.h b/src/DirectoryWalker/DirectoryWalker.h
new file mode 100644
index 0000000..94dcbf6
--- /dev/null
+++ b/src/DirectoryWalker/DirectoryWalker.h
@@ -0,0 +1,8 @@
+#ifndef YAPSSG_DIRECTORY_WALKER_H
+#define YAPSSG_DIRECTORY_WALKER_H
+
+#include <lua.h>
+
+void directory_walker_register(lua_State *lua, const char *input_path);
+
+#endif
diff --git a/src/Dom/Dom.c b/src/Dom/Dom.c
new file mode 100644
index 0000000..ede7ddd
--- /dev/null
+++ b/src/Dom/Dom.c
@@ -0,0 +1,303 @@
+#include "Dom/Dom.h"
+
+#include <lauxlib.h>
+#include <limits.h>
+#include <stddef.h>
+#include <string.h>
+#include <strings.h>
+
+static int absolute_index(lua_State *lua, int index) {
+ return index < 0 ? lua_gettop(lua) + index + 1 : index;
+}
+
+static int is_lua_element(const xmlNode *node) {
+ return node->type == XML_ELEMENT_NODE &&
+ strcasecmp((const char *)node->name, "lua") == 0;
+}
+
+static void set_string_field(lua_State *lua, int table_index, const char *name,
+ const char *value) {
+ lua_pushstring(lua, value);
+ lua_setfield(lua, table_index, name);
+}
+
+static int has_structured_children(const xmlNode *node) {
+ const xmlNode *child;
+
+ for (child = node->children; child != NULL; child = child->next) {
+ if (is_lua_element(child)) {
+ continue;
+ }
+ if (child->type != XML_TEXT_NODE && child->type != XML_CDATA_SECTION_NODE) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static void push_element(lua_State *lua, const xmlNode *node);
+
+static void push_special_node(lua_State *lua, const xmlNode *node) {
+ lua_newtable(lua);
+ if (node->type == XML_COMMENT_NODE) {
+ set_string_field(lua, -2, "kind", "comment");
+ } else {
+ set_string_field(lua, -2, "kind", "processing_instruction");
+ set_string_field(lua, -2, "target", (const char *)node->name);
+ }
+ set_string_field(lua, -2, "text",
+ node->content == NULL ? "" : (const char *)node->content);
+}
+
+static void add_child_alias(lua_State *lua, int element_index,
+ const xmlNode *child, int child_index) {
+ const char *name = (const char *)child->name;
+
+ lua_getfield(lua, element_index, name);
+ if (lua_isnil(lua, -1)) {
+ lua_pop(lua, 1);
+ lua_pushvalue(lua, child_index);
+ lua_setfield(lua, element_index, name);
+ return;
+ }
+ lua_pop(lua, 1);
+}
+
+static void push_children(lua_State *lua, const xmlNode *node,
+ int element_index, int include_text) {
+ const xmlNode *child;
+ int children_index;
+ int array_index = 1;
+
+ lua_newtable(lua);
+ children_index = lua_gettop(lua);
+
+ for (child = node->children; child != NULL; child = child->next) {
+ if (is_lua_element(child)) {
+ continue;
+ }
+
+ if (child->type == XML_ELEMENT_NODE) {
+ int child_index;
+
+ push_element(lua, child);
+ child_index = lua_gettop(lua);
+ lua_pushvalue(lua, child_index);
+ lua_rawseti(lua, children_index, array_index++);
+ add_child_alias(lua, element_index, child, child_index);
+ lua_pop(lua, 1);
+ } else if (include_text && (child->type == XML_TEXT_NODE ||
+ child->type == XML_CDATA_SECTION_NODE)) {
+ lua_pushstring(
+ lua, child->content == NULL ? "" : (const char *)child->content);
+ lua_rawseti(lua, children_index, array_index++);
+ } else if (child->type == XML_COMMENT_NODE || child->type == XML_PI_NODE) {
+ push_special_node(lua, child);
+ lua_rawseti(lua, children_index, array_index++);
+ }
+ }
+
+ lua_setfield(lua, element_index, "children");
+}
+
+static void push_attributes(lua_State *lua, const xmlNode *node,
+ int element_index) {
+ const xmlAttr *attribute;
+
+ lua_newtable(lua);
+ for (attribute = node->properties; attribute != NULL;
+ attribute = attribute->next) {
+ xmlChar *value = xmlNodeListGetString(node->doc, attribute->children, 1);
+
+ lua_pushstring(lua, value == NULL ? "" : (const char *)value);
+ lua_setfield(lua, -2, (const char *)attribute->name);
+ xmlFree(value);
+ }
+ lua_setfield(lua, element_index, "attributes");
+}
+
+static void push_text(lua_State *lua, const xmlNode *node, int element_index) {
+ const xmlNode *child;
+ luaL_Buffer buffer;
+
+ luaL_buffinit(lua, &buffer);
+ for (child = node->children; child != NULL; child = child->next) {
+ if (child->type == XML_TEXT_NODE || child->type == XML_CDATA_SECTION_NODE) {
+ luaL_addstring(
+ &buffer, child->content == NULL ? "" : (const char *)child->content);
+ }
+ }
+ luaL_pushresult(&buffer);
+ lua_setfield(lua, element_index, "text");
+}
+
+static void push_element(lua_State *lua, const xmlNode *node) {
+ int element_index;
+ int structured_children = has_structured_children(node);
+
+ lua_newtable(lua);
+ element_index = lua_gettop(lua);
+ set_string_field(lua, element_index, "tag", (const char *)node->name);
+ push_attributes(lua, node, element_index);
+ push_children(lua, node, element_index, structured_children);
+ if (!structured_children) {
+ push_text(lua, node, element_index);
+ }
+}
+
+void dom_push_page(lua_State *lua, const xmlNode *root) {
+ push_element(lua, root);
+}
+
+static xmlNodePtr node_from_table(lua_State *lua, int table_index, int *valid);
+
+static int add_attributes(lua_State *lua, int table_index, xmlNodePtr node) {
+ int attributes_index;
+ int valid = 1;
+
+ lua_getfield(lua, table_index, "attributes");
+ attributes_index = lua_gettop(lua);
+ if (lua_istable(lua, -1)) {
+ lua_pushnil(lua);
+ while (valid && lua_next(lua, -2) != 0) {
+ if (lua_type(lua, -2) != LUA_TSTRING ||
+ lua_type(lua, -1) != LUA_TSTRING ||
+ xmlNewProp(node, BAD_CAST lua_tostring(lua, -2),
+ BAD_CAST lua_tostring(lua, -1)) == NULL) {
+ valid = 0;
+ }
+ lua_pop(lua, 1);
+ }
+ }
+ lua_settop(lua, attributes_index - 1);
+ return valid ? 0 : -1;
+}
+
+static int append_lua_child(lua_State *lua, xmlNodePtr parent,
+ int child_index) {
+ xmlNodePtr child = NULL;
+ int valid = 1;
+
+ if (lua_type(lua, child_index) == LUA_TSTRING) {
+ child = xmlNewText(BAD_CAST lua_tostring(lua, child_index));
+ } else if (lua_istable(lua, child_index)) {
+ child = node_from_table(lua, child_index, &valid);
+ } else {
+ valid = 0;
+ }
+
+ if (!valid || child == NULL) {
+ xmlFreeNode(child);
+ return -1;
+ }
+ if (xmlAddChild(parent, child) == NULL) {
+ xmlFreeNode(child);
+ return -1;
+ }
+ return 0;
+}
+
+static int add_children(lua_State *lua, int table_index, xmlNodePtr node) {
+ size_t index;
+ size_t length;
+ int result = 0;
+
+ lua_getfield(lua, table_index, "children");
+ length = lua_istable(lua, -1) ? lua_objlen(lua, -1) : 0;
+
+ if (length > INT_MAX) {
+ lua_pop(lua, 1);
+ return -1;
+ }
+
+ for (index = 1; result == 0 && index <= length; ++index) {
+ lua_rawgeti(lua, -1, (int)index);
+ result = append_lua_child(lua, node, lua_gettop(lua));
+ lua_pop(lua, 1);
+ }
+ lua_pop(lua, 1);
+
+ if (result == 0 && length == 0) {
+ lua_getfield(lua, table_index, "text");
+ if (lua_type(lua, -1) == LUA_TSTRING) {
+ xmlNodePtr text = xmlNewText(BAD_CAST lua_tostring(lua, -1));
+
+ if (text == NULL || xmlAddChild(node, text) == NULL) {
+ xmlFreeNode(text);
+ result = -1;
+ }
+ }
+ lua_pop(lua, 1);
+ }
+ return result;
+}
+
+static xmlNodePtr special_node_from_table(lua_State *lua, int table_index,
+ const char *kind, int *valid) {
+ xmlNodePtr node;
+ const char *text;
+
+ lua_getfield(lua, table_index, "text");
+ text = lua_tostring(lua, -1);
+
+ if (strcmp(kind, "comment") == 0) {
+ node = xmlNewComment(BAD_CAST(text == NULL ? "" : text));
+ } else if (strcmp(kind, "processing_instruction") == 0) {
+ const char *target;
+
+ lua_getfield(lua, table_index, "target");
+ target = lua_tostring(lua, -1);
+ node = target == NULL || target[0] == '\0'
+ ? NULL
+ : xmlNewPI(BAD_CAST target, BAD_CAST(text == NULL ? "" : text));
+ lua_pop(lua, 1);
+ } else {
+ node = NULL;
+ }
+
+ lua_pop(lua, 1);
+ if (node == NULL) {
+ *valid = 0;
+ }
+ return node;
+}
+
+static xmlNodePtr node_from_table(lua_State *lua, int table_index, int *valid) {
+ const char *kind;
+ const char *tag;
+ xmlNodePtr node;
+
+ table_index = absolute_index(lua, table_index);
+ lua_getfield(lua, table_index, "kind");
+ kind = lua_tostring(lua, -1);
+ if (kind != NULL) {
+ node = special_node_from_table(lua, table_index, kind, valid);
+ lua_pop(lua, 1);
+ return node;
+ }
+ lua_pop(lua, 1);
+
+ lua_getfield(lua, table_index, "tag");
+ tag = lua_tostring(lua, -1);
+ node = tag == NULL || tag[0] == '\0' ? NULL : xmlNewNode(NULL, BAD_CAST tag);
+ lua_pop(lua, 1);
+
+ if (node == NULL || add_attributes(lua, table_index, node) != 0 ||
+ add_children(lua, table_index, node) != 0) {
+ xmlFreeNode(node);
+ *valid = 0;
+ return NULL;
+ }
+ return node;
+}
+
+xmlNodePtr dom_page_from_lua(lua_State *lua, int table_index) {
+ int valid = 1;
+ xmlNodePtr root = node_from_table(lua, table_index, &valid);
+
+ if (!valid) {
+ xmlFreeNode(root);
+ return NULL;
+ }
+ return root;
+}
diff --git a/src/Dom/Dom.h b/src/Dom/Dom.h
new file mode 100644
index 0000000..06bd1ff
--- /dev/null
+++ b/src/Dom/Dom.h
@@ -0,0 +1,10 @@
+#ifndef YAPSSG_DOM_H
+#define YAPSSG_DOM_H
+
+#include <libxml/tree.h>
+#include <lua.h>
+
+void dom_push_page(lua_State *lua, const xmlNode *root);
+xmlNodePtr dom_page_from_lua(lua_State *lua, int table_index);
+
+#endif
diff --git a/src/FileProcessor/FileProcessor.c b/src/FileProcessor/FileProcessor.c
new file mode 100644
index 0000000..d856fb4
--- /dev/null
+++ b/src/FileProcessor/FileProcessor.c
@@ -0,0 +1,396 @@
+#define _POSIX_C_SOURCE 200809L
+#define _XOPEN_SOURCE 700
+
+#include "FileProcessor/FileProcessor.h"
+
+#include "Logger/Logger.h"
+#include "LuaEngine/LuaEngine.h"
+#include "Path/Path.h"
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <libxml/HTMLparser.h>
+#include <libxml/HTMLtree.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#define COPY_BUFFER_SIZE 16384
+
+static int has_html_extension(const char *path) {
+ const char *extension = strrchr(path, '.');
+
+ return extension != NULL && (strcasecmp(extension, ".html") == 0 ||
+ strcasecmp(extension, ".htm") == 0);
+}
+
+static void report_copy_error(const char *input_path, const char *output_path,
+ int error_number) {
+ logger_error("cannot copy %s to %s: %s", input_path, output_path,
+ strerror(error_number));
+}
+
+static int write_all(int file, const char *buffer, size_t length) {
+ size_t offset = 0;
+
+ while (offset < length) {
+ ssize_t written = write(file, buffer + offset, length - offset);
+
+ if (written > 0) {
+ offset += (size_t)written;
+ } else if (written == 0) {
+ errno = EIO;
+ return -1;
+ } else if (errno != EINTR) {
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static int copy_file_data(int input, int output) {
+ char buffer[COPY_BUFFER_SIZE];
+
+ for (;;) {
+ ssize_t count = read(input, buffer, sizeof(buffer));
+
+ if (count > 0 && write_all(output, buffer, (size_t)count) != 0) {
+ return -1;
+ }
+ if (count == 0) {
+ return 0;
+ }
+ if (count < 0 && errno != EINTR) {
+ return -1;
+ }
+ }
+}
+
+static int copy_regular_file(const char *input_path, const char *output_path,
+ mode_t mode) {
+ int input = open(input_path, O_RDONLY);
+ int output;
+ int result;
+ int error_number;
+
+ logger_debug("copying file %s -> %s", input_path, output_path);
+
+ if (input < 0) {
+ report_copy_error(input_path, output_path, errno);
+ return -1;
+ }
+
+ output = open(output_path, O_WRONLY | O_CREAT | O_TRUNC, mode & 0777);
+ if (output < 0) {
+ error_number = errno;
+ close(input);
+ report_copy_error(input_path, output_path, error_number);
+ return -1;
+ }
+
+ result = copy_file_data(input, output);
+ error_number = result == 0 ? 0 : errno;
+
+ if (close(output) != 0 && result == 0) {
+ result = -1;
+ error_number = errno;
+ }
+ if (close(input) != 0 && result == 0) {
+ result = -1;
+ error_number = errno;
+ }
+
+ if (result != 0) {
+ report_copy_error(input_path, output_path, error_number);
+ }
+ return result;
+}
+
+int process_file(const char *input_path, const char *output_path) {
+ const int parse_options = HTML_PARSE_RECOVER | HTML_PARSE_NONET |
+ HTML_PARSE_NOERROR | HTML_PARSE_NOWARNING;
+ struct stat status;
+ htmlDocPtr document;
+ int saved;
+
+ if (stat(input_path, &status) != 0) {
+ logger_error("cannot inspect %s: %s", input_path, strerror(errno));
+ return -1;
+ }
+ if (!S_ISREG(status.st_mode)) {
+ logger_error("input is not a regular file: %s", input_path);
+ return -1;
+ }
+
+ logger_info("processing HTML %s -> %s", input_path, output_path);
+ document = htmlReadFile(input_path, NULL, parse_options);
+ if (document == NULL) {
+ logger_error("unable to parse HTML file: %s", input_path);
+ return -1;
+ }
+
+ if (process_html_document(document, input_path) != 0) {
+ xmlFreeDoc(document);
+ return -1;
+ }
+
+ saved = htmlSaveFileFormat(output_path, document, "UTF-8", 1);
+ xmlFreeDoc(document);
+ if (saved < 0) {
+ logger_error("unable to write HTML file: %s", output_path);
+ return -1;
+ }
+ return 0;
+}
+
+static int copy_symlink(const char *input_path, const char *output_path,
+ const struct stat *status) {
+ size_t capacity = status->st_size > 0 ? (size_t)status->st_size + 1 : 256;
+ char *target = malloc(capacity + 1);
+ ssize_t length = -1;
+
+ logger_debug("copying symbolic link %s -> %s", input_path, output_path);
+
+ if (target == NULL) {
+ logger_error("unable to allocate symbolic-link buffer");
+ return -1;
+ }
+
+ while (length < 0) {
+ ssize_t read_length = readlink(input_path, target, capacity);
+
+ if (read_length < 0) {
+ logger_error("cannot read symbolic link %s: %s", input_path,
+ strerror(errno));
+ free(target);
+ return -1;
+ }
+ if ((size_t)read_length < capacity) {
+ length = read_length;
+ } else {
+ size_t larger_capacity;
+ char *larger_target;
+
+ if (capacity > (SIZE_MAX - 1) / 2) {
+ logger_error("unable to allocate symbolic-link buffer");
+ free(target);
+ return -1;
+ }
+ larger_capacity = capacity * 2;
+ larger_target = realloc(target, larger_capacity + 1);
+ if (larger_target == NULL) {
+ logger_error("unable to allocate symbolic-link buffer");
+ free(target);
+ return -1;
+ }
+ target = larger_target;
+ capacity = larger_capacity;
+ }
+ }
+
+ target[length] = '\0';
+ if (symlink(target, output_path) != 0) {
+ logger_error("cannot copy symbolic link %s: %s", input_path,
+ strerror(errno));
+ free(target);
+ return -1;
+ }
+
+ free(target);
+ return 0;
+}
+
+static int copy_tree(const char *input_path, const char *output_path) {
+ struct stat status;
+ DIR *directory;
+ int result = 0;
+
+ if (lstat(input_path, &status) != 0) {
+ logger_error("cannot inspect %s: %s", input_path, strerror(errno));
+ return -1;
+ }
+ if (S_ISLNK(status.st_mode)) {
+ return copy_symlink(input_path, output_path, &status);
+ }
+ if (S_ISREG(status.st_mode)) {
+ return has_html_extension(input_path)
+ ? process_file(input_path, output_path)
+ : copy_regular_file(input_path, output_path, status.st_mode);
+ }
+ if (!S_ISDIR(status.st_mode)) {
+ logger_error("unsupported file type: %s", input_path);
+ return -1;
+ }
+
+ if (mkdir(output_path, status.st_mode & 0777) != 0) {
+ logger_error("cannot create directory %s: %s", output_path,
+ strerror(errno));
+ return -1;
+ }
+
+ directory = opendir(input_path);
+ if (directory == NULL) {
+ logger_error("cannot open directory %s: %s", input_path, strerror(errno));
+ return -1;
+ }
+
+ while (result == 0) {
+ struct dirent *entry;
+ char *input_child;
+ char *output_child;
+
+ errno = 0;
+ entry = readdir(directory);
+ if (entry == NULL) {
+ if (errno != 0) {
+ logger_error("cannot read directory %s: %s", input_path,
+ strerror(errno));
+ result = -1;
+ }
+ break;
+ }
+ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
+ continue;
+ }
+
+ input_child = path_join(input_path, entry->d_name);
+ output_child = path_join(output_path, entry->d_name);
+ if (input_child == NULL || output_child == NULL) {
+ logger_error("unable to allocate child path");
+ result = -1;
+ } else {
+ result = copy_tree(input_child, output_child);
+ }
+ free(input_child);
+ free(output_child);
+ }
+
+ if (closedir(directory) != 0 && result == 0) {
+ logger_error("cannot close directory %s: %s", input_path, strerror(errno));
+ result = -1;
+ }
+ return result;
+}
+
+static int remove_tree(const char *path) {
+ struct stat status;
+ DIR *directory;
+ int result = 0;
+
+ if (lstat(path, &status) != 0) {
+ if (errno == ENOENT) {
+ return 0;
+ }
+ logger_error("cannot inspect output %s: %s", path, strerror(errno));
+ return -1;
+ }
+
+ if (!S_ISDIR(status.st_mode) || S_ISLNK(status.st_mode)) {
+ if (unlink(path) != 0) {
+ logger_error("cannot remove output %s: %s", path, strerror(errno));
+ return -1;
+ }
+ return 0;
+ }
+
+ directory = opendir(path);
+ if (directory == NULL) {
+ logger_error("cannot open output directory %s: %s", path, strerror(errno));
+ return -1;
+ }
+
+ while (result == 0) {
+ struct dirent *entry;
+ char *child;
+
+ errno = 0;
+ entry = readdir(directory);
+ if (entry == NULL) {
+ if (errno != 0) {
+ logger_error("cannot read output directory %s: %s", path,
+ strerror(errno));
+ result = -1;
+ }
+ break;
+ }
+ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
+ continue;
+ }
+
+ child = path_join(path, entry->d_name);
+ if (child == NULL) {
+ logger_error("unable to allocate output child path");
+ result = -1;
+ } else {
+ result = remove_tree(child);
+ }
+ free(child);
+ }
+
+ if (closedir(directory) != 0 && result == 0) {
+ logger_error("cannot close output directory %s: %s", path, strerror(errno));
+ result = -1;
+ }
+ if (result == 0 && rmdir(path) != 0) {
+ logger_error("cannot remove output directory %s: %s", path,
+ strerror(errno));
+ result = -1;
+ }
+ return result;
+}
+
+int process_directory(const char *input_path, const char *output_path,
+ int overwrite_output) {
+ struct stat status;
+ struct stat output_status;
+ char *resolved_input;
+ char *resolved_output;
+ int result;
+
+ if (stat(input_path, &status) != 0) {
+ logger_error("cannot inspect %s: %s", input_path, strerror(errno));
+ return -1;
+ }
+ if (!S_ISDIR(status.st_mode)) {
+ logger_error("input is not a directory: %s", input_path);
+ return -1;
+ }
+
+ resolved_input = realpath(input_path, NULL);
+ if (lstat(output_path, &output_status) == 0 &&
+ !S_ISLNK(output_status.st_mode)) {
+ resolved_output = realpath(output_path, NULL);
+ } else {
+ resolved_output = path_resolve_new(output_path);
+ }
+ if (resolved_input == NULL || resolved_output == NULL) {
+ logger_error("cannot resolve input or output path");
+ free(resolved_input);
+ free(resolved_output);
+ return -1;
+ }
+
+ if (path_is_within(resolved_input, resolved_output)) {
+ logger_error("output directory cannot be inside input: %s", output_path);
+ result = -1;
+ } else if (overwrite_output &&
+ path_is_within(resolved_output, resolved_input)) {
+ logger_error("output directory cannot contain input: %s", output_path);
+ result = -1;
+ } else if (overwrite_output && remove_tree(output_path) != 0) {
+ result = -1;
+ } else {
+ logger_info("processing directory %s -> %s", input_path, output_path);
+ result = copy_tree(input_path, output_path);
+ }
+
+ free(resolved_input);
+ free(resolved_output);
+ return result;
+}
diff --git a/src/FileProcessor/FileProcessor.h b/src/FileProcessor/FileProcessor.h
new file mode 100644
index 0000000..90027d7
--- /dev/null
+++ b/src/FileProcessor/FileProcessor.h
@@ -0,0 +1,8 @@
+#ifndef YAPSSG_FILE_PROCESSOR_H
+#define YAPSSG_FILE_PROCESSOR_H
+
+int process_file(const char *input_path, const char *output_path);
+int process_directory(const char *input_path, const char *output_path,
+ int overwrite_output);
+
+#endif
diff --git a/src/Logger/Logger.c b/src/Logger/Logger.c
new file mode 100644
index 0000000..c43b24b
--- /dev/null
+++ b/src/Logger/Logger.c
@@ -0,0 +1,102 @@
+#include "Logger/Logger.h"
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+
+static LogLevel current_level = LOG_LEVEL_INFO;
+
+static const char *level_name(LogLevel level) {
+ switch (level) {
+ case LOG_LEVEL_ERROR:
+ return "error";
+ case LOG_LEVEL_WARNING:
+ return "warning";
+ case LOG_LEVEL_INFO:
+ return "info";
+ case LOG_LEVEL_DEBUG:
+ return "debug";
+ case LOG_LEVEL_SILENT:
+ return "silent";
+ }
+ return "unknown";
+}
+
+int logger_is_enabled(LogLevel level) {
+ return current_level != LOG_LEVEL_SILENT && level <= current_level;
+}
+
+static void log_message(LogLevel level, const char *format, va_list arguments)
+ YAPSSG_FORMAT_PRINTF(2, 0);
+
+static void log_message(LogLevel level, const char *format, va_list arguments) {
+ if (!logger_is_enabled(level)) {
+ return;
+ }
+
+ fprintf(stderr, "yapssg: %s: ", level_name(level));
+ vfprintf(stderr, format, arguments);
+ fputc('\n', stderr);
+ fflush(stderr);
+}
+
+void logger_set_level(LogLevel level) { current_level = level; }
+
+LogLevel logger_get_level(void) { return current_level; }
+
+int logger_parse_level(const char *name, LogLevel *level) {
+ if (strcmp(name, "error") == 0) {
+ *level = LOG_LEVEL_ERROR;
+ } else if (strcmp(name, "warning") == 0 || strcmp(name, "warn") == 0) {
+ *level = LOG_LEVEL_WARNING;
+ } else if (strcmp(name, "info") == 0) {
+ *level = LOG_LEVEL_INFO;
+ } else if (strcmp(name, "debug") == 0) {
+ *level = LOG_LEVEL_DEBUG;
+ } else if (strcmp(name, "silent") == 0 || strcmp(name, "quiet") == 0) {
+ *level = LOG_LEVEL_SILENT;
+ } else {
+ return -1;
+ }
+ return 0;
+}
+
+void logger_log(LogLevel level, const char *format, ...) {
+ va_list arguments;
+
+ va_start(arguments, format);
+ log_message(level, format, arguments);
+ va_end(arguments);
+}
+
+void logger_error(const char *format, ...) {
+ va_list arguments;
+
+ va_start(arguments, format);
+ log_message(LOG_LEVEL_ERROR, format, arguments);
+ va_end(arguments);
+}
+
+void logger_warning(const char *format, ...) {
+ va_list arguments;
+
+ va_start(arguments, format);
+ log_message(LOG_LEVEL_WARNING, format, arguments);
+ va_end(arguments);
+}
+
+void logger_info(const char *format, ...) {
+ va_list arguments;
+
+ va_start(arguments, format);
+ log_message(LOG_LEVEL_INFO, format, arguments);
+ va_end(arguments);
+}
+
+void logger_debug(const char *format, ...) {
+ va_list arguments;
+
+ va_start(arguments, format);
+ log_message(LOG_LEVEL_DEBUG, format, arguments);
+ va_end(arguments);
+}
diff --git a/src/Logger/Logger.h b/src/Logger/Logger.h
new file mode 100644
index 0000000..f0bef12
--- /dev/null
+++ b/src/Logger/Logger.h
@@ -0,0 +1,31 @@
+#ifndef YAPSSG_LOGGER_H
+#define YAPSSG_LOGGER_H
+
+#if defined(__GNUC__) || defined(__clang__)
+#define YAPSSG_FORMAT_PRINTF(format_index, arguments_index) \
+ __attribute__((format(printf, format_index, arguments_index)))
+#else
+#define YAPSSG_FORMAT_PRINTF(format_index, arguments_index)
+#endif
+
+typedef enum {
+ LOG_LEVEL_SILENT = -1,
+ LOG_LEVEL_ERROR,
+ LOG_LEVEL_WARNING,
+ LOG_LEVEL_INFO,
+ LOG_LEVEL_DEBUG
+} LogLevel;
+
+void logger_set_level(LogLevel level);
+LogLevel logger_get_level(void);
+int logger_is_enabled(LogLevel level);
+int logger_parse_level(const char *name, LogLevel *level);
+
+void logger_log(LogLevel level, const char *format, ...)
+ YAPSSG_FORMAT_PRINTF(2, 3);
+void logger_error(const char *format, ...) YAPSSG_FORMAT_PRINTF(1, 2);
+void logger_warning(const char *format, ...) YAPSSG_FORMAT_PRINTF(1, 2);
+void logger_info(const char *format, ...) YAPSSG_FORMAT_PRINTF(1, 2);
+void logger_debug(const char *format, ...) YAPSSG_FORMAT_PRINTF(1, 2);
+
+#endif
diff --git a/src/Logger/LuaLogger.c b/src/Logger/LuaLogger.c
new file mode 100644
index 0000000..5739791
--- /dev/null
+++ b/src/Logger/LuaLogger.c
@@ -0,0 +1,60 @@
+#include "Logger/LuaLogger.h"
+
+#include "Logger/Logger.h"
+
+#include <lauxlib.h>
+#include <lua.h>
+
+static void add_value(lua_State *lua, luaL_Buffer *buffer, int index) {
+ int type = lua_type(lua, index);
+
+ if (type == LUA_TSTRING || type == LUA_TNUMBER) {
+ luaL_addstring(buffer, lua_tostring(lua, index));
+ } else if (type == LUA_TBOOLEAN) {
+ luaL_addstring(buffer, lua_toboolean(lua, index) ? "true" : "false");
+ } else if (type == LUA_TNIL) {
+ luaL_addstring(buffer, "nil");
+ } else {
+ luaL_addchar(buffer, '<');
+ luaL_addstring(buffer, lua_typename(lua, type));
+ luaL_addchar(buffer, '>');
+ }
+}
+
+static int log_from_lua(lua_State *lua) {
+ LogLevel level = (LogLevel)lua_tointeger(lua, lua_upvalueindex(1));
+ int argument_count = lua_gettop(lua);
+ int index;
+ luaL_Buffer buffer;
+
+ if (!logger_is_enabled(level)) {
+ return 0;
+ }
+
+ luaL_buffinit(lua, &buffer);
+ for (index = 1; index <= argument_count; ++index) {
+ if (index > 1) {
+ luaL_addchar(&buffer, '\t');
+ }
+ add_value(lua, &buffer, index);
+ }
+ luaL_pushresult(&buffer);
+ logger_log(level, "%s", lua_tostring(lua, -1));
+ lua_pop(lua, 1);
+ return 0;
+}
+
+static void set_log_function(lua_State *lua, const char *name, LogLevel level) {
+ lua_pushinteger(lua, (lua_Integer)level);
+ lua_pushcclosure(lua, log_from_lua, 1);
+ lua_setfield(lua, -2, name);
+}
+
+void lua_logger_register(lua_State *lua) {
+ lua_newtable(lua);
+ set_log_function(lua, "error", LOG_LEVEL_ERROR);
+ set_log_function(lua, "warn", LOG_LEVEL_WARNING);
+ set_log_function(lua, "info", LOG_LEVEL_INFO);
+ set_log_function(lua, "debug", LOG_LEVEL_DEBUG);
+ lua_setglobal(lua, "log");
+}
diff --git a/src/Logger/LuaLogger.h b/src/Logger/LuaLogger.h
new file mode 100644
index 0000000..98c9a8f
--- /dev/null
+++ b/src/Logger/LuaLogger.h
@@ -0,0 +1,8 @@
+#ifndef YAPSSG_LUA_LOGGER_H
+#define YAPSSG_LUA_LOGGER_H
+
+#include <lua.h>
+
+void lua_logger_register(lua_State *lua);
+
+#endif
diff --git a/src/LuaEngine/LuaEngine.c b/src/LuaEngine/LuaEngine.c
new file mode 100644
index 0000000..319f408
--- /dev/null
+++ b/src/LuaEngine/LuaEngine.c
@@ -0,0 +1,273 @@
+#include "LuaEngine/LuaEngine.h"
+
+#include "DirectoryWalker/DirectoryWalker.h"
+#include "Dom/Dom.h"
+#include "Logger/Logger.h"
+#include "Logger/LuaLogger.h"
+#include "Path/Path.h"
+#include "Version/Version.h"
+
+#include <lauxlib.h>
+#include <lua.h>
+#include <lualib.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+
+typedef enum { SCRIPT_INLINE, SCRIPT_EXTERNAL } ScriptType;
+
+typedef struct {
+ char *source;
+ ScriptType type;
+ unsigned long line;
+} LuaScript;
+
+typedef struct {
+ LuaScript *items;
+ size_t length;
+ size_t capacity;
+} ScriptList;
+
+static int is_lua_element(const xmlNode *node) {
+ return node->type == XML_ELEMENT_NODE &&
+ strcasecmp((const char *)node->name, "lua") == 0;
+}
+
+static char *duplicate_string(const char *value) {
+ size_t length = strlen(value);
+ char *copy = malloc(length + 1);
+
+ if (copy != NULL) {
+ memcpy(copy, value, length + 1);
+ }
+ return copy;
+}
+
+static int grow_script_list(ScriptList *scripts) {
+ size_t larger_capacity;
+ LuaScript *larger_items;
+
+ if (scripts->capacity > SIZE_MAX / (2 * sizeof(*scripts->items))) {
+ return -1;
+ }
+
+ larger_capacity = scripts->capacity == 0 ? 4 : scripts->capacity * 2;
+ larger_items =
+ realloc(scripts->items, larger_capacity * sizeof(*scripts->items));
+ if (larger_items == NULL) {
+ return -1;
+ }
+
+ scripts->items = larger_items;
+ scripts->capacity = larger_capacity;
+ return 0;
+}
+
+static int append_script(ScriptList *scripts, const char *source,
+ ScriptType type, unsigned long line) {
+ LuaScript *script;
+
+ if (scripts->length == scripts->capacity && grow_script_list(scripts) != 0) {
+ return -1;
+ }
+
+ script = &scripts->items[scripts->length];
+ script->source = duplicate_string(source);
+ if (script->source == NULL) {
+ return -1;
+ }
+
+ script->type = type;
+ script->line = line;
+ ++scripts->length;
+ return 0;
+}
+
+static int collect_script(xmlNode *node, ScriptList *scripts) {
+ xmlChar *source_path = xmlGetProp(node, BAD_CAST "src");
+ xmlChar *code;
+ int result;
+
+ if (source_path != NULL) {
+ result = append_script(scripts, (const char *)source_path, SCRIPT_EXTERNAL,
+ node->line);
+ xmlFree(source_path);
+ return result;
+ }
+
+ code = xmlNodeGetContent(node);
+ result = append_script(scripts, code == NULL ? "" : (const char *)code,
+ SCRIPT_INLINE, node->line);
+ xmlFree(code);
+ return result;
+}
+
+static int collect_scripts(xmlNode *node, ScriptList *scripts) {
+ for (; node != NULL; node = node->next) {
+ if (is_lua_element(node)) {
+ if (collect_script(node, scripts) != 0) {
+ return -1;
+ }
+ } else if (collect_scripts(node->children, scripts) != 0) {
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static void free_scripts(ScriptList *scripts) {
+ size_t index;
+
+ for (index = 0; index < scripts->length; ++index) {
+ free(scripts->items[index].source);
+ }
+ free(scripts->items);
+}
+
+static char *make_chunk_name(const char *input_path, unsigned long line) {
+ size_t input_length = strlen(input_path);
+ size_t capacity;
+ char *chunk_name;
+
+ if (input_length > SIZE_MAX - 32) {
+ return NULL;
+ }
+ capacity = input_length + 32;
+ chunk_name = malloc(capacity);
+
+ if (chunk_name != NULL) {
+ snprintf(chunk_name, capacity, "@%s:%lu", input_path, line);
+ }
+ return chunk_name;
+}
+
+static int run_script(lua_State *lua, const LuaScript *script,
+ const char *input_path) {
+ char *source_path = NULL;
+ char *chunk_name = NULL;
+ const char *display_path;
+ int load_result;
+ int result;
+
+ if (script->type == SCRIPT_EXTERNAL) {
+ source_path = path_resolve_from_file(input_path, script->source);
+ if (source_path == NULL) {
+ logger_error("unable to allocate Lua source path");
+ return -1;
+ }
+ display_path = source_path;
+ load_result = luaL_loadfile(lua, source_path);
+ } else {
+ chunk_name = make_chunk_name(input_path, script->line);
+ if (chunk_name == NULL) {
+ logger_error("unable to allocate Lua chunk name");
+ return -1;
+ }
+ display_path = input_path;
+ load_result = luaL_loadbuffer(lua, script->source, strlen(script->source),
+ chunk_name);
+ }
+
+ result = load_result == 0 ? lua_pcall(lua, 0, 0, 0) : load_result;
+ if (result != 0) {
+ const char *message = lua_tostring(lua, -1);
+
+ logger_error("Lua error in %s: %s", display_path,
+ message == NULL ? "unknown error" : message);
+ lua_pop(lua, 1);
+ }
+
+ free(chunk_name);
+ free(source_path);
+ return result == 0 ? 0 : -1;
+}
+
+static int run_scripts(lua_State *lua, const ScriptList *scripts,
+ const char *input_path) {
+ size_t index;
+
+ for (index = 0; index < scripts->length; ++index) {
+ if (run_script(lua, &scripts->items[index], input_path) != 0) {
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static void register_yapssg(lua_State *lua) {
+ lua_newtable(lua);
+ lua_pushstring(lua, yapssg_version);
+ lua_setfield(lua, -2, "version");
+ lua_setglobal(lua, "yapssg");
+}
+
+static int replace_document_root(lua_State *lua, htmlDocPtr document,
+ const char *input_path) {
+ xmlNodePtr replacement;
+ xmlNodePtr old_root;
+
+ lua_getglobal(lua, "page");
+ if (!lua_istable(lua, -1)) {
+ logger_error("Lua global 'page' must remain a table: %s", input_path);
+ lua_pop(lua, 1);
+ return -1;
+ }
+
+ replacement = dom_page_from_lua(lua, lua_gettop(lua));
+ lua_pop(lua, 1);
+ if (replacement == NULL || replacement->type != XML_ELEMENT_NODE) {
+ logger_error("Lua produced an invalid page root: %s", input_path);
+ xmlFreeNode(replacement);
+ return -1;
+ }
+
+ old_root = xmlDocSetRootElement(document, replacement);
+ xmlFreeNode(old_root);
+ return 0;
+}
+
+int process_html_document(htmlDocPtr document, const char *input_path) {
+ xmlNodePtr root = xmlDocGetRootElement(document);
+ ScriptList scripts = {0};
+ lua_State *lua;
+ int result;
+
+ if (root == NULL) {
+ logger_error("HTML document has no root element: %s", input_path);
+ return -1;
+ }
+ if (collect_scripts(root, &scripts) != 0) {
+ logger_error("unable to collect Lua scripts: %s", input_path);
+ free_scripts(&scripts);
+ return -1;
+ }
+ if (scripts.length == 0) {
+ free_scripts(&scripts);
+ return 0;
+ }
+
+ lua = luaL_newstate();
+ if (lua == NULL) {
+ logger_error("unable to create Lua state");
+ free_scripts(&scripts);
+ return -1;
+ }
+
+ luaL_openlibs(lua);
+ register_yapssg(lua);
+ lua_logger_register(lua);
+ directory_walker_register(lua, input_path);
+ dom_push_page(lua, root);
+ lua_setglobal(lua, "page");
+
+ result = run_scripts(lua, &scripts, input_path);
+ if (result == 0) {
+ result = replace_document_root(lua, document, input_path);
+ }
+
+ lua_close(lua);
+ free_scripts(&scripts);
+ return result;
+}
diff --git a/src/LuaEngine/LuaEngine.h b/src/LuaEngine/LuaEngine.h
new file mode 100644
index 0000000..bd035b9
--- /dev/null
+++ b/src/LuaEngine/LuaEngine.h
@@ -0,0 +1,8 @@
+#ifndef YAPSSG_LUA_ENGINE_H
+#define YAPSSG_LUA_ENGINE_H
+
+#include <libxml/HTMLparser.h>
+
+int process_html_document(htmlDocPtr document, const char *input_path);
+
+#endif
diff --git a/src/Main.c b/src/Main.c
new file mode 100644
index 0000000..b7c77f1
--- /dev/null
+++ b/src/Main.c
@@ -0,0 +1,52 @@
+#include "Cli/Cli.h"
+#include "FileProcessor/FileProcessor.h"
+#include "Logger/Logger.h"
+#include "Path/Path.h"
+
+#include <libxml/parser.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+static int run(int argc, char **argv) {
+ CliOptions options;
+ CliParseResult parse_result;
+ char *automatic_output = NULL;
+ const char *output_path;
+ int result;
+
+ parse_result = cli_parse(argc, argv, &options);
+ if (parse_result != CLI_PARSE_OK) {
+ return parse_result == CLI_PARSE_ERROR ? EXIT_FAILURE : EXIT_SUCCESS;
+ }
+
+ logger_set_level(options.log_level);
+
+ output_path = options.output_path;
+ if (output_path == NULL) {
+ automatic_output = path_make_default_output(options.input_path);
+ if (automatic_output == NULL) {
+ logger_error("unable to allocate output path");
+ return EXIT_FAILURE;
+ }
+ output_path = automatic_output;
+ }
+
+ if (options.input_type == INPUT_TYPE_DIRECTORY) {
+ result = process_directory(options.input_path, output_path,
+ options.overwrite_output);
+ } else {
+ result = process_file(options.input_path, output_path);
+ }
+
+ free(automatic_output);
+ return result == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
+}
+
+int main(int argc, char **argv) {
+ int result;
+
+ xmlInitParser();
+ result = run(argc, argv);
+ xmlCleanupParser();
+ return result;
+}
diff --git a/src/Path/Path.c b/src/Path/Path.c
new file mode 100644
index 0000000..8a0ab2b
--- /dev/null
+++ b/src/Path/Path.c
@@ -0,0 +1,185 @@
+#define _POSIX_C_SOURCE 200809L
+#define _XOPEN_SOURCE 700
+
+#include "Path/Path.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+static const char output_prefix[] = "yapssg_";
+
+static int checked_add(size_t left, size_t right, size_t *sum) {
+ if (left > SIZE_MAX - right) {
+ return -1;
+ }
+ *sum = left + right;
+ return 0;
+}
+
+static char *duplicate_string(const char *value) {
+ size_t length = strlen(value);
+ char *copy = malloc(length + 1);
+
+ if (copy != NULL) {
+ memcpy(copy, value, length + 1);
+ }
+ return copy;
+}
+
+char *path_join(const char *left, const char *right) {
+ size_t left_length = strlen(left);
+ size_t right_length = strlen(right);
+ int needs_separator = left_length > 0 && left[left_length - 1] != '/';
+ size_t path_length;
+ size_t allocation_size;
+ char *path;
+
+ if (checked_add(left_length, (size_t)needs_separator, &path_length) != 0 ||
+ checked_add(path_length, right_length, &path_length) != 0 ||
+ checked_add(path_length, 1, &allocation_size) != 0) {
+ return NULL;
+ }
+
+ path = malloc(allocation_size);
+ if (path == NULL) {
+ return NULL;
+ }
+ memcpy(path, left, left_length);
+ if (needs_separator) {
+ path[left_length++] = '/';
+ }
+ memcpy(path + left_length, right, right_length + 1);
+ return path;
+}
+
+char *path_make_default_output(const char *input_path) {
+ const char *end = input_path + strlen(input_path);
+ const char *base;
+ size_t parent_length;
+ size_t base_length;
+ size_t prefix_length = sizeof(output_prefix) - 1;
+ size_t output_length;
+ size_t allocation_size;
+ char *output_path;
+
+ while (end > input_path + 1 && end[-1] == '/') {
+ --end;
+ }
+ base = end;
+ while (base > input_path && base[-1] != '/') {
+ --base;
+ }
+
+ parent_length = (size_t)(base - input_path);
+ base_length = (size_t)(end - base);
+ if (base_length == 0) {
+ return NULL;
+ }
+
+ if (checked_add(parent_length, prefix_length, &output_length) != 0 ||
+ checked_add(output_length, base_length, &output_length) != 0 ||
+ checked_add(output_length, 1, &allocation_size) != 0) {
+ return NULL;
+ }
+
+ output_path = malloc(allocation_size);
+ if (output_path == NULL) {
+ return NULL;
+ }
+ memcpy(output_path, input_path, parent_length);
+ memcpy(output_path + parent_length, output_prefix, prefix_length);
+ memcpy(output_path + parent_length + prefix_length, base, base_length);
+ output_path[parent_length + prefix_length + base_length] = '\0';
+ return output_path;
+}
+
+char *path_resolve_new(const char *path) {
+ char *path_copy = duplicate_string(path);
+ char *slash;
+ char *parent;
+ char *resolved_parent;
+ char *resolved_path;
+ const char *base;
+ size_t length;
+
+ if (path_copy == NULL) {
+ return NULL;
+ }
+
+ length = strlen(path_copy);
+ while (length > 1 && path_copy[length - 1] == '/') {
+ path_copy[--length] = '\0';
+ }
+
+ slash = strrchr(path_copy, '/');
+ if (slash == NULL) {
+ parent = duplicate_string(".");
+ base = path_copy;
+ } else if (slash == path_copy) {
+ parent = duplicate_string("/");
+ base = slash + 1;
+ } else {
+ *slash = '\0';
+ parent = duplicate_string(path_copy);
+ base = slash + 1;
+ }
+
+ if (parent == NULL || base[0] == '\0') {
+ free(parent);
+ free(path_copy);
+ return NULL;
+ }
+
+ resolved_parent = realpath(parent, NULL);
+ free(parent);
+ if (resolved_parent == NULL) {
+ free(path_copy);
+ return NULL;
+ }
+
+ resolved_path = path_join(resolved_parent, base);
+ free(resolved_parent);
+ free(path_copy);
+ return resolved_path;
+}
+
+char *path_resolve_from_file(const char *file_path, const char *relative_path) {
+ const char *slash;
+ size_t directory_length;
+ size_t relative_length;
+ size_t path_length;
+ size_t allocation_size;
+ char *path;
+
+ if (relative_path[0] == '/') {
+ return duplicate_string(relative_path);
+ }
+
+ slash = strrchr(file_path, '/');
+ directory_length = slash == NULL ? 0 : (size_t)(slash - file_path + 1);
+ relative_length = strlen(relative_path);
+ if (checked_add(directory_length, relative_length, &path_length) != 0 ||
+ checked_add(path_length, 1, &allocation_size) != 0) {
+ return NULL;
+ }
+
+ path = malloc(allocation_size);
+ if (path == NULL) {
+ return NULL;
+ }
+
+ memcpy(path, file_path, directory_length);
+ memcpy(path + directory_length, relative_path, relative_length + 1);
+ return path;
+}
+
+int path_is_within(const char *parent, const char *candidate) {
+ size_t parent_length = strlen(parent);
+
+ if (parent_length == 1 && parent[0] == '/') {
+ return candidate[0] == '/';
+ }
+ return strncmp(parent, candidate, parent_length) == 0 &&
+ (candidate[parent_length] == '\0' || candidate[parent_length] == '/');
+}
diff --git a/src/Path/Path.h b/src/Path/Path.h
new file mode 100644
index 0000000..a4ac530
--- /dev/null
+++ b/src/Path/Path.h
@@ -0,0 +1,10 @@
+#ifndef YAPSSG_PATH_H
+#define YAPSSG_PATH_H
+
+char *path_join(const char *left, const char *right);
+char *path_make_default_output(const char *input_path);
+char *path_resolve_new(const char *path);
+char *path_resolve_from_file(const char *file_path, const char *relative_path);
+int path_is_within(const char *parent, const char *candidate);
+
+#endif
diff --git a/src/Version/Version.c b/src/Version/Version.c
new file mode 100644
index 0000000..4f4a1ea
--- /dev/null
+++ b/src/Version/Version.c
@@ -0,0 +1,3 @@
+#include "Version/Version.h"
+
+const char yapssg_version[] = "0.1.0";
diff --git a/src/Version/Version.h b/src/Version/Version.h
new file mode 100644
index 0000000..f17b243
--- /dev/null
+++ b/src/Version/Version.h
@@ -0,0 +1,6 @@
+#ifndef YAPSSG_VERSION_H
+#define YAPSSG_VERSION_H
+
+extern const char yapssg_version[];
+
+#endif