From: Lukas Hägele Date: Sun, 27 Oct 2024 14:54:15 +0000 (+0100) Subject: add generation of index.html X-Git-Url: https://git.lhaegele.de/?a=commitdiff_plain;h=cbae4941909d0560c8cc89f68a7a52334d822044;p=recipes.git add generation of index.html --- diff --git a/src/html.c b/src/html.c index 5a59ca2..15e7cf4 100644 --- a/src/html.c +++ b/src/html.c @@ -134,7 +134,7 @@ addAttribute(html_element* Element, str Key, str Child, arena* Arena) } static void -appendElement(html_element* Sentinel, html_element* Element) +html_appendElement(html_element* Sentinel, html_element* Element) { /* todo: create sentinel in html_create() (inline and get rid of this function) */ Element->Prev = Sentinel->Prev; @@ -146,7 +146,7 @@ appendElement(html_element* Sentinel, html_element* Element) static inline void html_appendChild(html_element* Target, html_element* Child) { - appendElement(Target->Child, Child); + html_appendElement(Target->Child, Child); } static inline void @@ -158,7 +158,7 @@ html_appendContent(html_element* Target, str ContentStr, arena* Arena) Content->Content = ContentStr; } - appendElement(Target->Child, Content); + html_appendElement(Target->Child, Content); } static html* @@ -174,63 +174,63 @@ html_createDefault(arena* Arena) html_element* HtmlElement = html_createElement(HTML_ELEMENT_NAME_html, Arena); { - addAttribute(HtmlElement, STR_LITERAL("lang"), STR_LITERAL("'de-DE'"), Arena); + addAttribute(HtmlElement, STR_LITERAL("lang"), STR_LITERAL("de-DE"), Arena); html_appendChild(Root, HtmlElement); - } - - html_element* Head = html_createElement(HTML_ELEMENT_NAME_head, Arena); - { - html_appendChild(Root, Head); - html_element* MetaCharset = html_createElement(HTML_ELEMENT_NAME_meta, Arena); + html_element* Head = html_createElement(HTML_ELEMENT_NAME_head, Arena); { - addAttribute(MetaCharset, STR_LITERAL("charset"), STR_LITERAL("'utf-8'"), Arena); - html_appendChild(Head, MetaCharset); - } + html_appendChild(HtmlElement, Head); - html_element* MetaViewport = html_createElement(HTML_ELEMENT_NAME_meta, Arena); - { - addAttribute(MetaViewport, STR_LITERAL("name"), STR_LITERAL("'viewport'"), Arena); - addAttribute(MetaViewport, STR_LITERAL("content"), STR_LITERAL("'width=device-width, initial-scale=1'"), Arena); - html_appendChild(Head, MetaViewport); - } + html_element* MetaCharset = html_createElement(HTML_ELEMENT_NAME_meta, Arena); + { + addAttribute(MetaCharset, STR_LITERAL("charset"), STR_LITERAL("utf-8"), Arena); + html_appendChild(Head, MetaCharset); + } - /* todo: add later? */ - html_element* Title = html_createElement(HTML_ELEMENT_NAME_title, Arena); - { - Html->Title = Title; + html_element* MetaViewport = html_createElement(HTML_ELEMENT_NAME_meta, Arena); + { + addAttribute(MetaViewport, STR_LITERAL("name"), STR_LITERAL("viewport"), Arena); + addAttribute(MetaViewport, STR_LITERAL("content"), STR_LITERAL("width=device-width, initial-scale=1"), Arena); + html_appendChild(Head, MetaViewport); + } - /* todo: use static "nullstring"? */ - html_appendContent(Title, STR_LITERAL(""), Arena); - html_appendChild(Head, Title); - } + /* todo: add later? */ + html_element* Title = html_createElement(HTML_ELEMENT_NAME_title, Arena); + { + static str Untitled = STR_LITERAL("[untitled]"); + Html->Title = Title; - /* todo: add style? */ - } + html_appendContent(Title, Untitled, Arena); + html_appendChild(Head, Title); + } - html_element* Body = html_createElement(HTML_ELEMENT_NAME_body, Arena); - { - html_appendChild(Root, Body); + /* todo: add style? */ + } - html_element* Header = html_createElement(HTML_ELEMENT_NAME_header, Arena); + html_element* Body = html_createElement(HTML_ELEMENT_NAME_body, Arena); { - html_appendChild(Body, Header); + html_appendChild(HtmlElement, Body); - html_element* Heading = html_createElement(HTML_ELEMENT_NAME_h1, Arena); + html_element* Header = html_createElement(HTML_ELEMENT_NAME_header, Arena); { - html_appendContent(Heading, STR_LITERAL("Meine Rezeptsammlung"), Arena); - html_appendChild(Header, Heading); - } - } + html_appendChild(Body, Header); - html_element* Main = html_createElement(HTML_ELEMENT_NAME_main, Arena); - { - html_appendChild(Body, Main); + html_element* Heading = html_createElement(HTML_ELEMENT_NAME_h1, Arena); + { + html_appendContent(Heading, STR_LITERAL("Meine Rezeptsammlung"), Arena); + html_appendChild(Header, Heading); + } + } - html_element* Article = html_createElement(HTML_ELEMENT_NAME_article, Arena); + html_element* Main = html_createElement(HTML_ELEMENT_NAME_main, Arena); { - Html->Article = Article; - html_appendChild(Main, Article); + html_appendChild(Body, Main); + + html_element* Article = html_createElement(HTML_ELEMENT_NAME_article, Arena); + { + Html->Article = Article; + html_appendChild(Main, Article); + } } } } @@ -508,53 +508,79 @@ isValidAttribute(html_attribute* Attribute) } static void -serializeElement(str* Sentinel, html_element* Element) +serializeElement(str* Target, html_element* Parent) { +#if 1 + static str ElementNames[] = + { + #define X(Name_) { .Base = (u8*)#Name_, .Length = (sizeof(#Name_) - 1), .Capacity = 0 }, + ElementNameTable + #undef X + }; +#else static char* ElementNames[] = { - #define X(Name_) "#Name_", + #define X(Name_) #Name_, ElementNameTable #undef X }; +#endif - for (html_element* At = Element; isValidElement(At); At = At->Next) + html_element* Sentinel = Parent->Child; + for (html_element* Child = Sentinel->Next; + Child != Sentinel; + Child = Child->Next) { - if (Element->ElementName == HTML_ELEMENT_NAME_content) + if (Child->ElementName == HTML_ELEMENT_NAME_content) { - str_append(Sentinel, Element->Content); + str_append(Target, Child->Content); } else { - str_append(Sentinel, STR_LITERAL("<")); - str_append(Sentinel, STR_LITERAL(ElementNames[Element->ElementName])); + str_append(Target, STR_LITERAL("<")); + str_append(Target, ElementNames[Child->ElementName]); { - html_attribute* Attribute = Element->Attribute; - if (isValidAttribute(Attribute)) + for (html_attribute* Attribute = Child->Attribute; + isValidAttribute(Attribute); + Attribute = Attribute->Next) { - str_append(Sentinel, Attribute->Key); - str_append(Sentinel, STR_LITERAL("=\"")); - str_append(Sentinel, Attribute->Child); - str_append(Sentinel, STR_LITERAL("\"")); + str_append(Target, STR_LITERAL(" ")); + str_append(Target, Attribute->Key); + str_append(Target, STR_LITERAL("=\"")); + str_append(Target, Attribute->Child); + str_append(Target, STR_LITERAL("\"")); } } - str_append(Sentinel, STR_LITERAL(">")); + str_append(Target, STR_LITERAL(">")); - serializeElement(Sentinel, Element->Child); + if (Child->ElementName != HTML_ELEMENT_NAME_meta) + { + serializeElement(Target, Child); - str_append(Sentinel, STR_LITERAL("<")); - str_append(Sentinel, STR_LITERAL(ElementNames[Element->ElementName])); - str_append(Sentinel, STR_LITERAL("\\>")); + str_append(Target, STR_LITERAL("ElementName]); + str_append(Target, STR_LITERAL(">")); + } } } } static void -html_setTitle(html* Html, str TitleStr, arena* Arena) +html_setTitle(html* Html, str TitleStr) { html_element* Title = Html->Title; - { - html_appendContent(Title, TitleStr, Arena); - } + html_element* ChildSentinel = Title->Child; + html_element* Content = ChildSentinel->Next; + + ASSERT(Content->ElementName = HTML_ELEMENT_NAME_content); + Content->Content = TitleStr; +} + +static inline void +html_appendArticleSection(html* Html, html_element* Section) +{ + html_element* Article = Html->Article; + html_appendChild(Article, Section); } static str diff --git a/src/main.c b/src/main.c index ea7aa3a..6cf95a6 100644 --- a/src/main.c +++ b/src/main.c @@ -38,9 +38,11 @@ #include -typedef struct node +typedef struct recipe { - struct node* Next; + struct recipe* Next; + struct recipe* Prev; + str Name; html* Html; } recipe; @@ -58,6 +60,89 @@ isValid_recipe(recipe* Recipe) return Result; } +typedef struct tag_recipe +{ + struct tag_recipe* Next; + struct tag_recipe* Prev; + recipe* Recipe; +} tag_recipe; + +typedef struct tag +{ + struct tag* Next; + struct tag* Prev; + + tag_recipe* Recipes; + str Name; +} tag; + +static tag_recipe* +createTagRecipeSentinel(arena* Arena) +{ + tag_recipe* Sentinel = ARENA_PUSH_STRUCT(Arena, tag_recipe); + Sentinel->Next = Sentinel; + Sentinel->Prev = Sentinel; + + return Sentinel; +} + +static tag* +getOrCreateTag(tag* Sentinel, str Name, arena* Arena) +{ + tag* Result = 0; + + for (tag* GlobalTag = Sentinel->Next; + GlobalTag != Sentinel; + GlobalTag = GlobalTag->Next) + { + if (str_equals(Name, GlobalTag->Name)) + { + Result = GlobalTag; + break; + } + } + + if (Result == 0) + { + Result = ARENA_PUSH_STRUCT(Arena, tag); + Result->Name = Name; + Result->Recipes = createTagRecipeSentinel(Arena); + + tag* Next = Sentinel; + if (Sentinel->Next != Sentinel) + { + for (tag* At = Sentinel->Next; + At != Sentinel; + At = At->Next) + { + if (str_compare(Result->Name, At->Name) > 0) + { + Next = At; + break; + } + } + } + + Result->Prev = Next->Prev; + Result->Next = Next; + + Next->Prev->Next = Result; + Next->Prev = Result; + } + + return Result; +} + +static inline void +appendTerminatedDirectory(str* Target, char* Dir) +{ + str_appendCString(Target, Dir); + if (!str_endsWith(*Target, STR_LITERAL("/"))) + { + str_append(Target, STR_LITERAL("/")); + } +} + enum /* Limits */ { MAX_PATH = 1024 @@ -71,10 +156,12 @@ generateHtmlFile(str Filename, html* Html, arena* Arena) char Name[MAX_PATH] = {0}; str_toCString(Name, sizeof(Name), Filename); - int FileDescriptor = open(Name, O_WRONLY|O_CREAT); + int Flags = O_WRONLY|O_CREAT; + int Mode = S_IRWXU | (S_IRGRP|S_IXGRP) | (S_IROTH|S_IXOTH); + int FileDescriptor = open(Name, Flags, Mode); if (FileDescriptor == -1) { - perror("open"); + printError("open"); Error = 1; } else @@ -83,13 +170,19 @@ generateHtmlFile(str Filename, html* Html, arena* Arena) if (write(FileDescriptor, HtmlContent.Base, HtmlContent.Length) == -1) { - perror("write"); + printError("write"); + Error = 1; + } + + if (ftruncate(FileDescriptor, HtmlContent.Length) == -1) + { + printError("ftruncate"); Error = 1; } if (close(FileDescriptor) == -1) { - perror("close"); + printError("close"); Error = 1; } } @@ -106,9 +199,8 @@ main(int ArgumentCount, char** Arguments) char* OutputDir; recipe* Recipes; - //tag* Tags; + tag* Tags; - u64 RecipeCount; arena MainArena; } Context = {0}; @@ -126,12 +218,23 @@ main(int ArgumentCount, char** Arguments) Context.MainArena = arena_create(MEGABYTES(16)); + /* allocate recipe sentinel */ + { + recipe* RecipeSentinel = ARENA_PUSH_STRUCT(&Context.MainArena, recipe); + { + RecipeSentinel->Next = RecipeSentinel; + RecipeSentinel->Prev = RecipeSentinel; + + Context.Recipes = RecipeSentinel; + } + } + /* enumerate recipe files */ { DIR* Directory = opendir(Context.SourceDir); if (Directory == NULL) { - perror("opendir"); + printError("opendir"); return -1; } @@ -149,22 +252,50 @@ main(int ArgumentCount, char** Arguments) } recipe* New = ARENA_PUSH_STRUCT(&Context.MainArena, recipe); - New->Name = Name; - New->Next = Context.Recipes; + { + recipe* Sentinel = Context.Recipes; - Context.Recipes = New; - Context.RecipeCount++; + New->Name = Name; + New->Next = Sentinel; + + Sentinel->Prev->Next = New; + Sentinel->Prev = New; + } } if (closedir(Directory) == -1) { - perror("closedir"); + printError("closedir"); return -1; } } + /* ensure existence of output directory */ + { + DIR* Directory = opendir(Context.OutputDir); + if (Directory == NULL) + { + if (mkdir(Context.OutputDir, 0755) == -1) + { + printError("mkdir"); + return -1; + } + } + else + { + if (closedir(Directory) == -1) + { + printError("closedir"); + return -1; + } + } + } + + /* todo: parse recipe files multithreaded */ - for (recipe* Recipe = Context.Recipes; isValid_recipe(Recipe); Recipe = Recipe->Next) + for (recipe* Recipe = Context.Recipes->Next; + Recipe != Context.Recipes; + Recipe = Recipe->Next) { /* copy file */ str FileStr; @@ -174,17 +305,18 @@ main(int ArgumentCount, char** Arguments) arena TempArena = Context.MainArena; str_unbounded uPath = str_startUnbounded(&TempArena); { - str_appendCString(&uPath.Str, Context.SourceDir); + appendTerminatedDirectory(&uPath.Str, Context.SourceDir); str_append(&uPath.Str, Recipe->Name); } str PathStr = str_endUnbounded(uPath); + str_toCString(Path, sizeof(Path), PathStr); } int File = open(Path, O_RDONLY); if (File == -1) { - perror("open"); + printError("open"); return -1; } @@ -194,14 +326,14 @@ main(int ArgumentCount, char** Arguments) if (stat(Path, &FileStat) == -1) { - perror("stat"); + printError("stat"); return -1; } u8* Memory = ARENA_PUSH_ARRAY(&Context.MainArena, u8, FileStat.st_size); if (read(File, Memory, FileStat.st_size) == -1) { - perror("read"); + printError("read"); return -1; } @@ -212,7 +344,7 @@ main(int ArgumentCount, char** Arguments) if (close(File) == -1) { - perror("close"); + printError("close"); return -1; } } @@ -220,82 +352,65 @@ main(int ArgumentCount, char** Arguments) Recipe->Html = html_parseMarkdown(FileStr, &Context.MainArena); } -#if 0 -#if 0 - /* todo: distill unique sorted tags */ - - /* sort recipes */ + /* allocate tag sentinel */ { - u32 WasReordered = 0; - - /* todo: implement faster sorting */ - do + tag* TagSentinel = ARENA_PUSH_STRUCT(&Context.MainArena, tag); { - recipe* Previous = Recipes; - for (recipe* A = Recipes; - isValid(A) && isValid(A->Next); - A = A->Next) - { - recipe* B = A->Next; - - str AStr = A->Name; - str BStr = B->Name; + TagSentinel->Recipes = createTagRecipeSentinel(&Context.MainArena); + TagSentinel->Next = TagSentinel; + TagSentinel->Prev = TagSentinel; - s32 Result = 0; + Context.Tags = TagSentinel; + } - u64 CheckLength = AStr.Length; - if (AStr.Length != BStr.Length) - { - if (AStr.Length < BStr.Length) - { - CheckLength = AStr.Length; - Result = -1; - } - else - { - CheckLength = BStr.Length; - Result = 1; - } - } + tag_recipe* TagRecipeSentinel = ARENA_PUSH_STRUCT(&Context.MainArena, tag_recipe); + { + TagRecipeSentinel->Next = TagRecipeSentinel; + TagRecipeSentinel->Prev = TagRecipeSentinel; - for (u64 StringIndex = 0; StringIndex < CheckLength; StringIndex) - { - u8 ALow = TO_LOWERCASE(AStr.Base[StringIndex]); - u8 BLow = TO_LOWERCASE(BStr.Base[StringIndex]); + TagSentinel->Recipes = TagRecipeSentinel; + } + } - Result = ALow - BLow; - if (Result != 0) - { - break; - } - } + /* todo: distill unique sorted tags */ + for (recipe* Recipe = Context.Recipes->Next; + Recipe != Context.Recipes; + Recipe = Recipe->Next) + { + html* Html = Recipe->Html; + html_meta* Meta = Html->Meta; + html_tag* Sentinel = Meta->Tags; + for (html_tag* Tag = Sentinel->Next; Tag != Sentinel; Tag = Tag->Next) + { + tag* GlobalTag = getOrCreateTag(Context.Tags, Tag->Content, &Context.MainArena); + tag_recipe* GlobalSentinel = GlobalTag->Recipes; + tag_recipe* TagRecipe = ARENA_PUSH_STRUCT(&Context.MainArena, tag_recipe); + { + TagRecipe->Recipe = Recipe; - if (Result != 0) + tag_recipe* Next = GlobalSentinel; + if (GlobalSentinel->Next != GlobalSentinel) { - WasReordered = 1; - - if (Result > 0) + for (tag_recipe* At = GlobalSentinel->Next; + At != GlobalSentinel; + At = At->Next) { - if (Previous == Recipes) - { - Recipes = B; - } - else + if (str_compare(TagRecipe->Recipe->Name, At->Recipe->Name) > 0) { - Previous->Next = B; + Next = At; + break; } - - A->Next = B->Next; - B->Next = A; } } - Previous = A; + TagRecipe->Next = Next; + TagRecipe->Prev = Next->Prev; + + Next->Prev->Next = TagRecipe; + Next->Prev = TagRecipe; } } - while (WasReordered == 1); } -#endif /* generate html output */ { @@ -305,47 +420,69 @@ main(int ArgumentCount, char** Arguments) html* MainPage = html_createDefault(&Context.MainArena); { /* todo: remove title from overview? */ - html_setTitle(MainPage, STR_LITERAL("Überblick"), &Context.MainArena); + html_setTitle(MainPage, STR_LITERAL("Überblick")); /* tags */ + html_element* Tags = html_createElement(HTML_ELEMENT_NAME_h2, &Context.MainArena); { - html_element* Tags = html_createElement(HTML_ELEMENT_NAME_h2, &Context.MainArena); html_appendArticleSection(MainPage, Tags); + html_appendContent(Tags, STR_LITERAL("Tags"), &Context.MainArena); html_element* TagList = html_createElement(HTML_ELEMENT_NAME_ul, &Context.MainArena); - html_appendValue(Tags, TagList); - - for (tag* Tag = Context.Tags; isValid_tag(Tag); Tag = Tag->Next) { - html_element* TagElement = html_createElement(HTML_ELEMENT_NAME_li, &Context.MainArena); - html_appendContent(TagElement, Tag->Name, &Context.MainArena); + html_appendArticleSection(MainPage, TagList); + + tag* Sentinel = Context.Tags; + for (tag* Tag = Sentinel->Next; Tag != Sentinel; Tag = Tag->Next) + { + html_element* TagElement = html_createElement(HTML_ELEMENT_NAME_li, &Context.MainArena); + html_appendContent(TagElement, Tag->Name, &Context.MainArena); + html_appendChild(TagList, TagElement); + } } } /* recipes */ + html_element* Recipes = html_createElement(HTML_ELEMENT_NAME_h2, &Context.MainArena); { - html_element* Recipes = html_createElement(HTML_ELEMENT_NAME_h2, &Context.MainArena); html_appendArticleSection(MainPage, Recipes); + html_appendContent(Recipes, STR_LITERAL("Rezepte"), &Context.MainArena); html_element* RecipeList = html_createElement(HTML_ELEMENT_NAME_ul, &Context.MainArena); - html_appendValue(Recipes, RecipeList); - - for (recipe* Recipe = Context.Recipes; isValid_recipe(Recipe); Recipe = Recipe->Next) { - html_element* RecipeElement = html_createElement(HTML_ELEMENT_NAME_li, &Context.MainArena); - html_appendContent(RecipeElement, Recipe->Name, &Context.MainArena); + html_appendArticleSection(MainPage, RecipeList); + + for (recipe* Recipe = Context.Recipes->Next; + Recipe != Context.Recipes; + Recipe = Recipe->Next) + { + html_element* RecipeElement = html_createElement(HTML_ELEMENT_NAME_li, &Context.MainArena); + html_appendContent(RecipeElement, Recipe->Name, &Context.MainArena); + html_appendChild(RecipeList, RecipeElement); + } } } - } - str Filename = STR_LITERAL("index.html"); - if (generateHtmlFile(Filename, MainPage, &Context.MainArena) != 0) - { - fprintf(stderr, "Error while generating main page. Stopping.\n"); - return -1; + /* serialize html tree */ + { + arena TempArena = Context.MainArena; + str_unbounded uPath = str_startUnbounded(&TempArena); + { + appendTerminatedDirectory(&uPath.Str, Context.OutputDir); + str_append(&uPath.Str, STR_LITERAL("index.html")); + } + str Filepath = str_endUnbounded(uPath); + + if (generateHtmlFile(Filepath, MainPage, &Context.MainArena) != 0) + { + fprintf(stderr, "Error while generating main page. Stopping.\n"); + return -1; + } + } } } +#if 0 /* detail pages */ for (recipe* Recipe = Recipes; isValid_recipe(Recipe); Recipe = Recipe->Next) { @@ -371,8 +508,8 @@ main(int ArgumentCount, char** Arguments) return -1; } } - } #endif + } fprintf(stdout, "Done.\n"); diff --git a/src/util.h b/src/util.h index ba64c36..3ae15f0 100644 --- a/src/util.h +++ b/src/util.h @@ -80,6 +80,14 @@ arena_push(arena* Arena, memory_size Size, b32 Zero) #define ARENA_PUSH_ARRAY(Arena_, Struct_, Count_) (Struct_*)arena_push(Arena_, (sizeof(Struct_) * Count_), 1) +/* error handling */ +static inline void +printError(char* Message) +{ + perror(Message); +} + + /* string handling */ #define TO_LOWERCASE(C_) ( ((C_ >= 'A') && (C_ <= 'Z')) ? (C_ | 0x20) : C_ ) @@ -243,19 +251,19 @@ str_equals(str A, str B) } static inline b32 -str_startsWith(str A, str B) +str_startsWith(str A, str Start) { b32 Result = 1; - if (A.Length < B.Length) + if (A.Length < Start.Length) { Result = 0; } else { - for (memory_size i = 0u; i < B.Length; i++) + for (memory_size i = 0u; i < Start.Length; i++) { - if (A.Base[i] != B.Base[i]) + if (A.Base[i] != Start.Base[i]) { Result = 0; break; @@ -266,6 +274,31 @@ str_startsWith(str A, str B) return Result; } +static inline b32 +str_endsWith(str A, str End) +{ + b32 Result = 1; + + if (A.Length < End.Length) + { + Result = 0; + } + else + { + for (memory_size i = 0u; i < End.Length; i++) + { + u8* Candidate = A.Base + (A.Length-1) - i; + u8* Reference = End.Base + (End.Length-1) - i; + if (*Candidate != *Reference) + { + Result = 0; + } + } + } + + return Result; +} + static inline b32 str_isWhitespaceOnly(str Str) {