From 99e1103b79b50b92a7784d4c538230f22e724f99 Mon Sep 17 00:00:00 2001 From: frosty Date: Tue, 6 Oct 2026 10:11:38 -0400 Subject: feat: add page:emit --- src/FileProcessor/FileProcessor.c | 94 ++++++++++++++++++++++++++++- src/LuaEngine/LuaEngine.c | 124 +++++++++++++++++++++++++++++++++++++- src/LuaEngine/LuaEngine.h | 15 ++++- 3 files changed, 230 insertions(+), 3 deletions(-) diff --git a/src/FileProcessor/FileProcessor.c b/src/FileProcessor/FileProcessor.c index 84efece..8f76216 100644 --- a/src/FileProcessor/FileProcessor.c +++ b/src/FileProcessor/FileProcessor.c @@ -113,6 +113,44 @@ static int copy_regular_file(const char *input_path, const char *output_path, return result; } +static int make_parent_directories(const char *path) { + char *copy; + size_t length = 0; + const char *slash = strrchr(path, '/'); + char *cursor; + + if (slash == NULL) { + return 0; + } + + length = (size_t)(slash - path); + copy = malloc(length + 1); + if (copy == NULL) { + return -1; + } + memcpy(copy, path, length); + copy[length] = '\0'; + + for (cursor = copy + 1; *cursor != '\0'; ++cursor) { + if (*cursor == '/') { + *cursor = '\0'; + if (mkdir(copy, 0777) != 0 && errno != EEXIST) { + free(copy); + return -1; + } + *cursor = '/'; + } + } + + if (mkdir(copy, 0777) != 0 && errno != EEXIST) { + free(copy); + return -1; + } + + free(copy); + return 0; +} + 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; @@ -136,8 +174,11 @@ int process_file(const char *input_path, const char *output_path) { return -1; } - if (process_html_document(document, input_path) != 0) { + EmitList emits = {0}; + + if (process_html_document(document, input_path, &emits) != 0) { xmlFreeDoc(document); + emit_list_free(&emits); return -1; } @@ -145,8 +186,59 @@ int process_file(const char *input_path, const char *output_path) { xmlFreeDoc(document); if (saved < 0) { logger_error("unable to write HTML file: %s", output_path); + emit_list_free(&emits); return -1; } + + for (size_t index = 0; index < emits.length; ++index) { + const char *emitted = emits.items[index].path; + char *emitted_output; + int emitted_saved; + + if (emitted[0] == '/') { + emitted_output = strdup(emitted); + } else { + const char *slash = strrchr(output_path, '/'); + char *directory; + + if (slash == NULL) { + emitted_output = strdup(emitted); + } else { + directory = malloc((size_t)(slash - output_path) + 1); + if (directory != NULL) { + memcpy(directory, output_path, (size_t)(slash - output_path)); + directory[slash - output_path] = '\0'; + emitted_output = path_join(directory, emitted); + free(directory); + } else { + emitted_output = NULL; + } + } + } + + if (emitted_output == NULL || + make_parent_directories(emitted_output) != 0) { + logger_error("unable to prepare output path for %s: %s", + emitted, emitted_output == NULL ? strerror(errno) + : emitted_output); + free(emitted_output); + emit_list_free(&emits); + return -1; + } + + logger_info("emitting %s -> %s", input_path, emitted_output); + emitted_saved = htmlSaveFileFormat(emitted_output, emits.items[index].document, + "UTF-8", 1); + if (emitted_saved < 0) { + logger_error("unable to write HTML file: %s", emitted_output); + free(emitted_output); + emit_list_free(&emits); + return -1; + } + free(emitted_output); + } + + emit_list_free(&emits); return 0; } diff --git a/src/LuaEngine/LuaEngine.c b/src/LuaEngine/LuaEngine.c index c34fd68..af30403 100644 --- a/src/LuaEngine/LuaEngine.c +++ b/src/LuaEngine/LuaEngine.c @@ -143,6 +143,121 @@ static char *make_chunk_name(const char *input_path, unsigned long line) { return chunk_name; } +typedef struct { + htmlDocPtr source; + EmitList *emits; +} EmitContext; + +static int grow_emit_list(EmitList *emits) { + size_t larger_capacity; + EmittedPage *larger_items; + + if (emits->capacity > SIZE_MAX / (2 * sizeof(*emits->items))) { + return -1; + } + + larger_capacity = emits->capacity == 0 ? 4 : emits->capacity * 2; + larger_items = + realloc(emits->items, larger_capacity * sizeof(*emits->items)); + if (larger_items == NULL) { + return -1; + } + + emits->items = larger_items; + emits->capacity = larger_capacity; + return 0; +} + +static int append_emit(EmitList *emits, const char *path, + htmlDocPtr document) { + EmittedPage *emitted; + + if (emits->length == emits->capacity && grow_emit_list(emits) != 0) { + return -1; + } + + emitted = &emits->items[emits->length]; + emitted->path = duplicate_string(path); + if (emitted->path == NULL) { + return -1; + } + + emitted->document = document; + ++emits->length; + return 0; +} + +void emit_list_free(EmitList *emits) { + size_t index; + + for (index = 0; index < emits->length; ++index) { + free(emits->items[index].path); + xmlFreeDoc(emits->items[index].document); + } + free(emits->items); + emits->items = NULL; + emits->length = 0; + emits->capacity = 0; +} + +static htmlDocPtr snapshot_document(htmlDocPtr source, lua_State *lua) { + htmlDocPtr copy; + xmlDtdPtr dtd; + xmlNodePtr root; + + lua_getglobal(lua, "page"); + root = lua_istable(lua, -1) ? dom_page_from_lua(lua, lua_gettop(lua)) : NULL; + lua_pop(lua, 1); + if (root == NULL) { + return NULL; + } + + copy = xmlNewDoc(BAD_CAST "1.0"); + if (copy == NULL) { + xmlFreeNode(root); + return NULL; + } + if (source->encoding != NULL) { + copy->encoding = xmlStrdup(source->encoding); + } + + dtd = xmlGetIntSubset(source); + if (dtd != NULL) { + dtd = xmlCopyDtd(dtd); + if (dtd != NULL) { + xmlAddChild((xmlNodePtr)copy, (xmlNodePtr)dtd); + copy->intSubset = dtd; + } + } + + xmlAddChild((xmlNodePtr)copy, root); + return copy; +} + +static int page_emit(lua_State *lua) { + EmitContext *context; + const char *path; + htmlDocPtr document; + + luaL_checktype(lua, 1, LUA_TTABLE); + path = luaL_checkstring(lua, 2); + if (path[0] == '\0') { + return luaL_error(lua, "emit path cannot be empty"); + } + context = lua_touserdata(lua, lua_upvalueindex(1)); + + document = snapshot_document(context->source, lua); + if (document == NULL) { + return luaL_error(lua, "unable to snapshot the current page"); + } + + if (append_emit(context->emits, path, document) != 0) { + xmlFreeDoc(document); + return luaL_error(lua, "unable to record emitted page"); + } + return 0; +} + static int run_script(lua_State *lua, const LuaScript *script, const char *input_path) { char *source_path = NULL; @@ -228,7 +343,9 @@ static int replace_document_root(lua_State *lua, htmlDocPtr document, return 0; } -int process_html_document(htmlDocPtr document, const char *input_path) { +int process_html_document(htmlDocPtr document, const char *input_path, + EmitList *emits) { + EmitContext context = {document, emits}; xmlNodePtr root = xmlDocGetRootElement(document); ScriptList scripts = {0}; lua_State *lua; @@ -262,6 +379,11 @@ int process_html_document(htmlDocPtr document, const char *input_path) { directory_walker_register(lua, input_path); dom_push_page(lua, root); lua_setglobal(lua, "page"); + lua_getglobal(lua, "page"); + lua_pushlightuserdata(lua, &context); + lua_pushcclosure(lua, page_emit, 1); + lua_setfield(lua, -2, "emit"); + lua_pop(lua, 1); result = run_scripts(lua, &scripts, input_path); if (result == 0) { diff --git a/src/LuaEngine/LuaEngine.h b/src/LuaEngine/LuaEngine.h index bd035b9..2a5eea4 100644 --- a/src/LuaEngine/LuaEngine.h +++ b/src/LuaEngine/LuaEngine.h @@ -3,6 +3,19 @@ #include -int process_html_document(htmlDocPtr document, const char *input_path); +typedef struct { + char *path; + htmlDocPtr document; +} EmittedPage; + +typedef struct { + EmittedPage *items; + size_t length; + size_t capacity; +} EmitList; + +int process_html_document(htmlDocPtr document, const char *input_path, + EmitList *emits); +void emit_list_free(EmitList *emits); #endif -- cgit v1.3