From: Lukas Hägele Date: Fri, 1 Nov 2024 13:27:34 +0000 (+0100) Subject: add generation of tag an detail pages X-Git-Url: https://git.lhaegele.de/?a=commitdiff_plain;h=2e36f51e131717a40f10757a9b71fee9958e12c5;p=recipes.git add generation of tag an detail pages --- diff --git a/content/spinatcannelloni.md b/content/cannelloni-mit-spinatfuellung.md similarity index 100% rename from content/spinatcannelloni.md rename to content/cannelloni-mit-spinatfuellung.md diff --git a/content/salatdressing-einfach.md b/content/einfaches-salatdressing.md similarity index 100% rename from content/salatdressing-einfach.md rename to content/einfaches-salatdressing.md diff --git a/content/gnocchi-bacon-pfanne.md b/content/gnocchi-bacon-pfanne.md index 0b8576d..314412f 100644 --- a/content/gnocchi-bacon-pfanne.md +++ b/content/gnocchi-bacon-pfanne.md @@ -6,7 +6,7 @@ hauptgericht hellofresh --- -# Frische Gnocchi-Bacon-Pfanne +# Gnocchi-Bacon-Pfanne ## Zutaten diff --git a/content/haehnchenbrust-in-salbei-thymian-marinade.md b/content/haehnchenbrust-in-salbei-thymian-marinade.md index a5e23f3..aeb1387 100644 --- a/content/haehnchenbrust-in-salbei-thymian-marinade.md +++ b/content/haehnchenbrust-in-salbei-thymian-marinade.md @@ -2,7 +2,7 @@ #!ini [tags] -haehnchen +hähnchen hauptgericht hellofresh --- diff --git a/content/harissa-haenchen-mit-zucchini.md b/content/harissa-haenchen-mit-zucchini.md index c25177c..7247c45 100644 --- a/content/harissa-haenchen-mit-zucchini.md +++ b/content/harissa-haenchen-mit-zucchini.md @@ -2,12 +2,12 @@ #!ini [tags] -haehnchen +hähnchen hauptgericht hellofresh --- -# Würziges Harissa-Hähnchen mit Zucchini +# Harissa-Hähnchen mit Zucchini ## Zutaten diff --git a/content/lachssahnesosse.md b/content/lachssahnesosse.md index 9db1ef0..4b946b1 100644 --- a/content/lachssahnesosse.md +++ b/content/lachssahnesosse.md @@ -6,7 +6,7 @@ nudeln soße --- -# Bandnudeln mit Lachssahnesoße +# Lachssahnesoße ## Zutaten diff --git a/content/schweinelachssteaks-mit-schupfnudeln.md b/content/schweinelachssteaks-mit-schupfnudeln.md index 9802115..f28412d 100644 --- a/content/schweinelachssteaks-mit-schupfnudeln.md +++ b/content/schweinelachssteaks-mit-schupfnudeln.md @@ -6,7 +6,7 @@ hauptgericht hellofresh --- -# Würzig gebratene Schweinelachssteaks mit Schupfnudeln +# Schweinelachssteaks mit Schupfnudeln ## Zutaten diff --git a/src/html.c b/src/html.c index 15e7cf4..0fe77f4 100644 --- a/src/html.c +++ b/src/html.c @@ -31,6 +31,7 @@ X(p) \ X(ul) \ X(li) \ + X(a) \ \ X(content) @@ -72,6 +73,7 @@ typedef struct html_tag typedef struct { + str Title; html_tag* Tags; } html_meta; @@ -122,7 +124,7 @@ createMeta(arena* Arena) } static inline void -addAttribute(html_element* Element, str Key, str Child, arena* Arena) +html_addAttribute(html_element* Element, str Key, str Child, arena* Arena) { html_attribute* Attribute = ARENA_PUSH_STRUCT(Arena, html_attribute); { @@ -161,6 +163,53 @@ html_appendContent(html_element* Target, str ContentStr, arena* Arena) html_appendElement(Target->Child, Content); } +static inline void +html_appendArticleSection(html* Html, html_element* Section) +{ + html_element* Article = Html->Article; + html_appendChild(Article, Section); +} + +static inline void +html_appendTagListToArticle(html* Html, str TagsDir, arena* Arena) +{ + html_element* Tags = html_createElement(HTML_ELEMENT_NAME_h2, Arena); + { + html_appendArticleSection(Html, Tags); + html_appendContent(Tags, STR_LITERAL("Tags"), Arena); + + html_element* TagList = html_createElement(HTML_ELEMENT_NAME_ul, Arena); + { + html_appendArticleSection(Html, TagList); + + html_tag* Sentinel = Html->Meta->Tags; + for (html_tag* Tag = Sentinel->Next; Tag != Sentinel; Tag = Tag->Next) + { + html_element* TagElement = html_createElement(HTML_ELEMENT_NAME_li, Arena); + { + html_appendChild(TagList, TagElement); + + html_element* TagLink = html_createElement(HTML_ELEMENT_NAME_a, Arena); + { + str_unbounded uPath = str_startUnbounded(Arena); + { + str_append(&uPath.Str, TagsDir); + str_append(&uPath.Str, Tag->Content); + str_append(&uPath.Str, STR_LITERAL(".html")); + } + str Link = str_endUnbounded(uPath); + + html_addAttribute(TagLink, STR_LITERAL("href"), Link, Arena); + html_appendContent(TagLink, STR_LITERAL("#"), Arena); + html_appendContent(TagLink, Tag->Content, Arena); + html_appendChild(TagElement, TagLink); + } + } + } + } + } +} + static html* html_createDefault(arena* Arena) { @@ -174,7 +223,7 @@ html_createDefault(arena* Arena) html_element* HtmlElement = html_createElement(HTML_ELEMENT_NAME_html, Arena); { - addAttribute(HtmlElement, STR_LITERAL("lang"), STR_LITERAL("de-DE"), Arena); + html_addAttribute(HtmlElement, STR_LITERAL("lang"), STR_LITERAL("de-DE"), Arena); html_appendChild(Root, HtmlElement); html_element* Head = html_createElement(HTML_ELEMENT_NAME_head, Arena); @@ -183,14 +232,14 @@ html_createDefault(arena* Arena) html_element* MetaCharset = html_createElement(HTML_ELEMENT_NAME_meta, Arena); { - addAttribute(MetaCharset, STR_LITERAL("charset"), STR_LITERAL("utf-8"), Arena); + html_addAttribute(MetaCharset, STR_LITERAL("charset"), STR_LITERAL("utf-8"), Arena); html_appendChild(Head, MetaCharset); } 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_addAttribute(MetaViewport, STR_LITERAL("name"), STR_LITERAL("viewport"), Arena); + html_addAttribute(MetaViewport, STR_LITERAL("content"), STR_LITERAL("width=device-width, initial-scale=1"), Arena); html_appendChild(Head, MetaViewport); } @@ -257,8 +306,8 @@ appendTag(html_tag* Sentinel, str Stripped, arena* Arena) Sentinel->Prev = Tag; } -static html_element* -parseHeading(str Line, html_element* Article, arena* Arena) +static void +parseHeading(str Line, html* Html, arena* Arena) { /* actual element name will be set below */ html_element* Heading = html_createElement(HTML_ELEMENT_NAME_invalid, Arena); @@ -295,11 +344,14 @@ parseHeading(str Line, html_element* Article, arena* Arena) } html_appendContent(Heading, HeadingContent, Arena); - } - html_appendChild(Article, Heading); + if (Heading->ElementName == HTML_ELEMENT_NAME_h1) + { + Html->Meta->Title = HeadingContent; + } + } - return Article; + html_appendChild(Html->Article, Heading); } static html_element* @@ -415,9 +467,7 @@ html_parseMarkdown(str Source, arena* Arena) } else if (str_startsWith(Stripped, STR_LITERAL("#"))) { - /* todo: remove return value? */ - PrevElement = parseHeading(Stripped, Html->Article, Arena); - //PreviousLineWasEmpty = 0; + parseHeading(Stripped, Html, Arena); } else if (str_startsWith(Line, STR_LITERAL("-")) || str_startsWith(Line, STR_LITERAL("*"))) @@ -576,11 +626,13 @@ html_setTitle(html* Html, str TitleStr) Content->Content = TitleStr; } -static inline void -html_appendArticleSection(html* Html, html_element* Section) +static str +html_getTitle(html* Html) { - html_element* Article = Html->Article; - html_appendChild(Article, Section); + html_meta* Meta = Html->Meta; + + str Title = Meta->Title; + return Title; } static str @@ -588,8 +640,8 @@ html_toString(html* Html, arena* Arena) { str_unbounded Unbounded = str_startUnbounded(Arena); { - str* Line = &Unbounded.Str; - serializeElement(Line, Html->Root); + str* Target = &Unbounded.Str; + serializeElement(Target, Html->Root); } str Result = str_endUnbounded(Unbounded); return Result; diff --git a/src/main.c b/src/main.c index 6cf95a6..1afec6c 100644 --- a/src/main.c +++ b/src/main.c @@ -38,15 +38,49 @@ #include +enum /* Limits */ +{ + MAX_PATH = 1024 +}; + typedef struct recipe { struct recipe* Next; struct recipe* Prev; - str Name; html* Html; + + str Filename; + str Name; } recipe; +static int +provideDirectory(str Path) +{ + char PathCStr[MAX_PATH] = {0}; + str_toCString(PathCStr, sizeof(PathCStr), Path); + + DIR* Directory = opendir(PathCStr); + if (Directory == NULL) + { + if (mkdir(PathCStr, 0755) == -1) + { + printError("mkdir"); + return -1; + } + } + else + { + if (closedir(Directory) == -1) + { + printError("closedir"); + return -1; + } + } + + return 0; +} + static b32 isValid_recipe(recipe* Recipe) { @@ -134,20 +168,15 @@ getOrCreateTag(tag* Sentinel, str Name, arena* Arena) } static inline void -appendTerminatedDirectory(str* Target, char* Dir) +appendTerminatedDirectory(str* Target, str Dir) { - str_appendCString(Target, Dir); + str_append(Target, Dir); if (!str_endsWith(*Target, STR_LITERAL("/"))) { str_append(Target, STR_LITERAL("/")); } } -enum /* Limits */ -{ - MAX_PATH = 1024 -}; - static u32 generateHtmlFile(str Filename, html* Html, arena* Arena) { @@ -190,13 +219,34 @@ generateHtmlFile(str Filename, html* Html, arena* Arena) return Error; } +static void +appendRecipeLink(html_element* RecipeElement, recipe* Recipe, arena* Arena) +{ + html_element* RecipeLink = html_createElement(HTML_ELEMENT_NAME_a, Arena); + { + str_unbounded uPath = str_startUnbounded(Arena); + { + str_append(&uPath.Str, STR_LITERAL("/recipes/")); + str_append(&uPath.Str, str_stripExtension(Recipe->Filename)); + str_append(&uPath.Str, STR_LITERAL(".html")); + } + str Link = str_endUnbounded(uPath); + + html_addAttribute(RecipeLink, STR_LITERAL("href"), Link, Arena); + html_appendContent(RecipeLink, Recipe->Name, Arena); + html_appendChild(RecipeElement, RecipeLink); + } +} + int main(int ArgumentCount, char** Arguments) { struct { - char* SourceDir; - char* OutputDir; + str SourceDir; + str OutputDir; + str OutputDirTags; + str OutputDirRecipes; recipe* Recipes; tag* Tags; @@ -204,6 +254,8 @@ main(int ArgumentCount, char** Arguments) arena MainArena; } Context = {0}; + Context.MainArena = arena_create(MEGABYTES(16)); + /* handle commandline arguments */ if (ArgumentCount != 3) { @@ -212,12 +264,10 @@ main(int ArgumentCount, char** Arguments) } else { - Context.SourceDir = Arguments[1]; - Context.OutputDir = Arguments[2]; + Context.SourceDir = str_fromCString(&Context.MainArena, Arguments[1]); + Context.OutputDir = str_fromCString(&Context.MainArena, Arguments[2]); } - Context.MainArena = arena_create(MEGABYTES(16)); - /* allocate recipe sentinel */ { recipe* RecipeSentinel = ARENA_PUSH_STRUCT(&Context.MainArena, recipe); @@ -231,7 +281,10 @@ main(int ArgumentCount, char** Arguments) /* enumerate recipe files */ { - DIR* Directory = opendir(Context.SourceDir); + char PathCStr[MAX_PATH] = {0}; + str_toCString(PathCStr, sizeof(PathCStr), Context.SourceDir); + + DIR* Directory = opendir(PathCStr); if (Directory == NULL) { printError("opendir"); @@ -253,13 +306,29 @@ main(int ArgumentCount, char** Arguments) recipe* New = ARENA_PUSH_STRUCT(&Context.MainArena, recipe); { + New->Filename = Name; + recipe* Sentinel = Context.Recipes; + recipe* Next = Sentinel; + if (Sentinel->Next != Sentinel) + { + for (recipe* At = Sentinel->Next; + At != Sentinel; + At = At->Next) + { + if (str_compare(New->Filename, At->Filename) > 0) + { + Next = At; + break; + } + } + } - New->Name = Name; - New->Next = Sentinel; + New->Next = Next; + New->Prev = Next->Prev; - Sentinel->Prev->Next = New; - Sentinel->Prev = New; + Next->Prev->Next = New; + Next->Prev = New; } } @@ -270,28 +339,44 @@ main(int ArgumentCount, char** Arguments) } } - /* ensure existence of output directory */ + /* ensure existence of output directories */ { - DIR* Directory = opendir(Context.OutputDir); - if (Directory == NULL) + if (provideDirectory(Context.OutputDir) == -1) { - if (mkdir(Context.OutputDir, 0755) == -1) + return -1; + } + + /* tags directory */ + { + str_unbounded uPath = str_startUnbounded(&Context.MainArena); + { + appendTerminatedDirectory(&uPath.Str, Context.OutputDir); + appendTerminatedDirectory(&uPath.Str, STR_LITERAL("tags")); + } + Context.OutputDirTags = str_endUnbounded(uPath); + + if (provideDirectory(Context.OutputDirTags) == -1) { - printError("mkdir"); return -1; } } - else + + /* recipe directory */ { - if (closedir(Directory) == -1) + str_unbounded uPath = str_startUnbounded(&Context.MainArena); + { + appendTerminatedDirectory(&uPath.Str, Context.OutputDir); + appendTerminatedDirectory(&uPath.Str, STR_LITERAL("recipes")); + } + Context.OutputDirRecipes = str_endUnbounded(uPath); + + if (provideDirectory(Context.OutputDirRecipes) == -1) { - printError("closedir"); return -1; } } } - /* todo: parse recipe files multithreaded */ for (recipe* Recipe = Context.Recipes->Next; Recipe != Context.Recipes; @@ -306,7 +391,7 @@ main(int ArgumentCount, char** Arguments) str_unbounded uPath = str_startUnbounded(&TempArena); { appendTerminatedDirectory(&uPath.Str, Context.SourceDir); - str_append(&uPath.Str, Recipe->Name); + str_append(&uPath.Str, Recipe->Filename); } str PathStr = str_endUnbounded(uPath); @@ -350,6 +435,7 @@ main(int ArgumentCount, char** Arguments) } Recipe->Html = html_parseMarkdown(FileStr, &Context.MainArena); + Recipe->Name = html_getTitle(Recipe->Html); } /* allocate tag sentinel */ @@ -436,8 +522,25 @@ main(int ArgumentCount, char** Arguments) 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); + { + html_appendChild(TagList, TagElement); + + html_element* TagLink = html_createElement(HTML_ELEMENT_NAME_a, &Context.MainArena); + { + str_unbounded uPath = str_startUnbounded(&Context.MainArena); + { + str_append(&uPath.Str, STR_LITERAL("/tags/")); + str_append(&uPath.Str, Tag->Name); + str_append(&uPath.Str, STR_LITERAL(".html")); + } + str Link = str_endUnbounded(uPath); + + html_addAttribute(TagLink, STR_LITERAL("href"), Link, &Context.MainArena); + html_appendContent(TagLink, STR_LITERAL("#"), &Context.MainArena); + html_appendContent(TagLink, Tag->Name, &Context.MainArena); + html_appendChild(TagElement, TagLink); + } + } } } } @@ -457,8 +560,10 @@ main(int ArgumentCount, char** Arguments) 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); + { + html_appendChild(RecipeList, RecipeElement); + appendRecipeLink(RecipeElement, Recipe, &Context.MainArena); + } } } } @@ -482,33 +587,79 @@ main(int ArgumentCount, char** Arguments) } } -#if 0 - /* detail pages */ - for (recipe* Recipe = Recipes; isValid_recipe(Recipe); Recipe = Recipe->Next) + /* tag pages */ { - /* recipes/... */ - str Filename = ...; - - if (generateHtmlFile(Filename, Recipe->Html) != 0) + tag* Sentinel = Context.Tags; + for (tag* Tag = Sentinel->Next; Tag != Sentinel; Tag = Tag->Next) { - fprintf(stderr, "Error while generating detail page. Stopping.\n"); - return -1; + /* todo: combine createDefault and setTitle in helper function? */ + html* TagPage = html_createDefault(&Context.MainArena); + { + html_setTitle(TagPage, Tag->Name); + + html_element* RecipeList = html_createElement(HTML_ELEMENT_NAME_ul, &Context.MainArena); + { + html_appendArticleSection(TagPage, RecipeList); + + tag_recipe* Sentinel = Tag->Recipes; + for (tag_recipe* TagRecipe = Sentinel->Next; TagRecipe != Sentinel; TagRecipe = TagRecipe->Next) + { + html_element* RecipeElement = html_createElement(HTML_ELEMENT_NAME_li, &Context.MainArena); + { + html_appendChild(RecipeList, RecipeElement); + appendRecipeLink(RecipeElement, TagRecipe->Recipe, &Context.MainArena); + } + } + } + } + + /* serialize html tree */ + { + arena TempArena = Context.MainArena; + str_unbounded uPath = str_startUnbounded(&TempArena); + { + str_append(&uPath.Str, Context.OutputDirTags); + str_append(&uPath.Str, Tag->Name); + str_append(&uPath.Str, STR_LITERAL(".html")); + } + str Filepath = str_endUnbounded(uPath); + + if (generateHtmlFile(Filepath, TagPage, &Context.MainArena) != 0) + { + fprintf(stderr, "Error while generating main page. Stopping.\n"); + return -1; + } + } } } - /* tag page? */ - for (tag* Tag = Tags; isValid_tag(Tag); Tag = Tag->Next) + /* detail pages */ { - /* tags/... */ - str Filename = ...; - - if (generateHtmlFile(Filename, Tag->Html) != 0) + recipe* Sentinel = Context.Recipes; + for (recipe* Recipe = Sentinel->Next; Recipe != Sentinel; Recipe = Recipe->Next) { - fprintf(stderr, "Error while generating tag page. Stopping.\n"); - return -1; + /* append tag list */ + html_appendTagListToArticle(Recipe->Html, STR_LITERAL("/tags/"), &Context.MainArena); + + /* serialize html tree */ + { + arena TempArena = Context.MainArena; + str_unbounded uPath = str_startUnbounded(&TempArena); + { + str_append(&uPath.Str, Context.OutputDirRecipes); + str_append(&uPath.Str, str_stripExtension(Recipe->Filename)); + str_append(&uPath.Str, STR_LITERAL(".html")); + } + str Filepath = str_endUnbounded(uPath); + + if (generateHtmlFile(Filepath, Recipe->Html, &Context.MainArena) != 0) + { + fprintf(stderr, "Error while generating main page. Stopping.\n"); + return -1; + } + } } } -#endif } fprintf(stdout, "Done.\n"); diff --git a/src/util.h b/src/util.h index 3ae15f0..59ce7c1 100644 --- a/src/util.h +++ b/src/util.h @@ -226,6 +226,26 @@ str_stripTail(str Str) return Result; } +static inline str +str_stripExtension(str Str) +{ + memory_size InitialLength = Str.Length; + memory_size OffsetToLast = InitialLength - 1; + + for (memory_size i = 0u; i < InitialLength; i++) + { + u8 Candidate = Str.Base[OffsetToLast - i]; + if (Candidate == '.') + { + Str.Length -= i+1; + break; + } + } + + str Result = Str; + return Result; +} + static inline b32 str_equals(str A, str B) {