add generation of tag an detail pages
authorLukas Hägele <lukas.haegele93@web.de>
Fri, 1 Nov 2024 13:27:34 +0000 (14:27 +0100)
committerLukas Hägele <lukas.haegele93@web.de>
Fri, 1 Nov 2024 13:27:34 +0000 (14:27 +0100)
content/cannelloni-mit-spinatfuellung.md [moved from content/spinatcannelloni.md with 100% similarity]
content/einfaches-salatdressing.md [moved from content/salatdressing-einfach.md with 100% similarity]
content/gnocchi-bacon-pfanne.md
content/haehnchenbrust-in-salbei-thymian-marinade.md
content/harissa-haenchen-mit-zucchini.md
content/lachssahnesosse.md
content/schweinelachssteaks-mit-schupfnudeln.md
src/html.c
src/main.c
src/util.h

index 0b8576db9372b7413897bc04b2c24832a48aa6cd..314412f52dc0d541d4b6b63d7cb642e109513c5f 100644 (file)
@@ -6,7 +6,7 @@ hauptgericht
 hellofresh
 ---
 
-# Frische Gnocchi-Bacon-Pfanne
+# Gnocchi-Bacon-Pfanne
 
 ## Zutaten
 
index a5e23f30588964e6c39791a49c0176229e2d77de..aeb1387c6e83418a43d3b46950ba1a2145936c49 100644 (file)
@@ -2,7 +2,7 @@
 #!ini
 
 [tags]
-haehnchen
+hähnchen
 hauptgericht
 hellofresh
 ---
index c25177c80a9c3ad09d2f09a062a3aa8661957756..7247c45c069fdef778c0646c7c233003cef38c80 100644 (file)
@@ -2,12 +2,12 @@
 #!ini
 
 [tags]
-haehnchen
+hähnchen
 hauptgericht
 hellofresh
 ---
 
-# Würziges Harissa-Hähnchen mit Zucchini
+# Harissa-Hähnchen mit Zucchini
 
 ## Zutaten
 
index 9db1ef09fb56d44bddd075483e653ca76cb47046..4b946b15c65d4081e158cfe9a523c73f95b2de3d 100644 (file)
@@ -6,7 +6,7 @@ nudeln
 soße
 ---
 
-# Bandnudeln mit Lachssahnesoße
+# Lachssahnesoße
 
 ## Zutaten
 
index 9802115b00e24982ddac2b1cd25b52d495caafa5..f28412dc5fb4dda8bb00135a0db366b8a2231aaa 100644 (file)
@@ -6,7 +6,7 @@ hauptgericht
 hellofresh
 ---
 
-# Würzig gebratene Schweinelachssteaks mit Schupfnudeln
+# Schweinelachssteaks mit Schupfnudeln
 
 ## Zutaten
 
index 15e7cf4bf7ea877330a19c12624e07ba16b8e6a5..0fe77f43e663c15293b82624585c053dae1ea72e 100644 (file)
@@ -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;
index 6cf95a6c39ab278b246264baba0258a604db8f90..1afec6c1fa579b1fa2837608d5c83b299a8bf3e7 100644 (file)
 
 #include <sys/stat.h>
 
+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");
index 3ae15f09006ff6cd3e796a842b2af416f8a76e72..59ce7c16448441d93971065e32bf8c48d15b887d 100644 (file)
@@ -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)
 {