work in progress to create a new markdown to html converter
authorLukas Hägele <lukas.haegele93@web.de>
Fri, 4 Oct 2024 09:29:49 +0000 (11:29 +0200)
committerLukas Hägele <lukas.haegele93@web.de>
Fri, 4 Oct 2024 09:29:49 +0000 (11:29 +0200)
75 files changed:
content/.template.md.tmp [moved from .template.md.tmp with 100% similarity]
content/bechamelsosse.md [moved from bechamelsosse.md with 100% similarity]
content/bohnenmus.md [moved from bohnenmus.md with 100% similarity]
content/briegelschmiere.md [moved from briegelschmiere.md with 100% similarity]
content/burger.md [moved from burger.md with 100% similarity]
content/chilaquiles-rancheros.md [moved from chilaquiles-rancheros.md with 100% similarity]
content/chili-con-carne_karin.md [moved from chili-con-carne_karin.md with 100% similarity]
content/donauwelle.md [moved from donauwelle.md with 100% similarity]
content/feta-oliven-dip.md [moved from feta-oliven-dip.md with 100% similarity]
content/fettuccine-mit-trueffeloel.md [moved from fettuccine-mit-trueffeloel.md with 100% similarity]
content/gebratene-schweinemedaillons.md [moved from gebratene-schweinemedaillons.md with 100% similarity]
content/gebratene-spaetzle-mit-bacon.md [moved from gebratene-spaetzle-mit-bacon.md with 100% similarity]
content/gnocchi-bacon-pfanne.md [moved from gnocchi-bacon-pfanne.md with 100% similarity]
content/gnocchi-hackfleisch-auflauf.md [moved from gnocchi-hackfleisch-auflauf.md with 100% similarity]
content/gnocchi-mit-speck-zucchini.md [moved from gnocchi-mit-speck-zucchini.md with 100% similarity]
content/griesssuppe.md [moved from griesssuppe.md with 100% similarity]
content/gulaschsuppe.md [moved from gulaschsuppe.md with 100% similarity]
content/hackbaellchen-gefuellt-mit-mozzarella.md [moved from hackbaellchen-gefuellt-mit-mozzarella.md with 100% similarity]
content/hackbaellchen-in-senfsosse.md [moved from hackbaellchen-in-senfsosse.md with 100% similarity]
content/hackbaellchenpfanne-mit-paprikagemuese.md [moved from hackbaellchenpfanne-mit-paprikagemuese.md with 100% similarity]
content/hackfleischsosse.md [moved from hackfleischsosse.md with 100% similarity]
content/hackfleischsosse_lasagne.md [moved from hackfleischsosse_lasagne.md with 100% similarity]
content/haehnchenbrust-in-salbei-thymian-marinade.md [moved from haehnchenbrust-in-salbei-thymian-marinade.md with 100% similarity]
content/haehnchengeschnetzeltes-mit-spaetzle.md [moved from haehnchengeschnetzeltes-mit-spaetzle.md with 100% similarity]
content/haferbrei.md [moved from haferbrei.md with 100% similarity]
content/harissa-haenchen-mit-zucchini.md [moved from harissa-haenchen-mit-zucchini.md with 100% similarity]
content/hazans-basilikum-pesto.md [moved from hazans-basilikum-pesto.md with 100% similarity]
content/hefeteig.md [moved from hefeteig.md with 100% similarity]
content/joghurtdressing.md [moved from joghurtdressing.md with 100% similarity]
content/knoblauchbaguette.md [moved from knoblauchbaguette.md with 100% similarity]
content/lachssahnesosse.md [moved from lachssahnesosse.md with 100% similarity]
content/lasagne.md [moved from lasagne.md with 100% similarity]
content/lauchfrischkaese.md [moved from lauchfrischkaese.md with 100% similarity]
content/lauchgemuese.md [moved from lauchgemuese.md with 100% similarity]
content/linsenbolognese.md [moved from linsenbolognese.md with 100% similarity]
content/linseneintopf.md [moved from linseneintopf.md with 100% similarity]
content/marmorkuchen.md [moved from marmorkuchen.md with 100% similarity]
content/mediterranes-gemuese.md [moved from mediterranes-gemuese.md with 100% similarity]
content/nusszopf.md [moved from nusszopf.md with 100% similarity]
content/orzo-nudel-risotto-mit-rauchigen-pilzen.md [moved from orzo-nudel-risotto-mit-rauchigen-pilzen.md with 100% similarity]
content/pfannkuchen-auflauf.md [moved from pfannkuchen-auflauf.md with 100% similarity]
content/pfannkuchenteig.md [moved from pfannkuchenteig.md with 100% similarity]
content/power-muesli.md [moved from power-muesli.md with 100% similarity]
content/putengeschnetzeltes.md [moved from putengeschnetzeltes.md with 100% similarity]
content/quarkoelteig.md [moved from quarkoelteig.md with 100% similarity]
content/radieschenfrischkaese.md [moved from radieschenfrischkaese.md with 100% similarity]
content/rauchiges-schweinefilet.md [moved from rauchiges-schweinefilet.md with 100% similarity]
content/reiseintopf-mit-hackfleisch.md [moved from reiseintopf-mit-hackfleisch.md with 100% similarity]
content/reiseintopf-mit-haenchenbrust.md [moved from reiseintopf-mit-haenchenbrust.md with 100% similarity]
content/salatdressing-einfach.md [moved from salatdressing-einfach.md with 100% similarity]
content/salsa-dip.md [moved from salsa-dip.md with 100% similarity]
content/schinkennudeln.md [moved from schinkennudeln.md with 100% similarity]
content/schwedische-frikadellen-mit-kartoffelstampf.md [moved from schwedische-frikadellen-mit-kartoffelstampf.md with 100% similarity]
content/schweinelachssteaks-mit-schupfnudeln.md [moved from schweinelachssteaks-mit-schupfnudeln.md with 100% similarity]
content/selbstgebackenes-knoblauchbrot.md [moved from selbstgebackenes-knoblauchbrot.md with 100% similarity]
content/spaetzle.md [moved from spaetzle.md with 100% similarity]
content/spinatcannelloni.md [moved from spinatcannelloni.md with 100% similarity]
content/spinatquiche.md [moved from spinatquiche.md with 100% similarity]
content/thailaendisches-kokos-limetten-curry.md [moved from thailaendisches-kokos-limetten-curry.md with 100% similarity]
content/thunfischsalat.md [moved from thunfischsalat.md with 100% similarity]
content/thunfischsosse.md [moved from thunfischsosse.md with 100% similarity]
content/tomatensosse.md [moved from tomatensosse.md with 100% similarity]
content/tortilla.md [moved from tortilla.md with 100% similarity]
content/toskanischer-filettopf.md [moved from toskanischer-filettopf.md with 100% similarity]
content/vietnamesisches-rindfleisch.md [moved from vietnamesisches-rindfleisch.md with 100% similarity]
content/weihnachtliche-entenbrust-an-wintergemuese.md [moved from weihnachtliche-entenbrust-an-wintergemuese.md with 100% similarity]
content/wurstsalat.md [moved from wurstsalat.md with 100% similarity]
content/zitronen-haenchenkeule-mit-tabbouleh.md [moved from zitronen-haenchenkeule-mit-tabbouleh.md with 100% similarity]
content/zitronenkuchen.md [moved from zitronenkuchen.md with 100% similarity]
content/zucchini-lasagne.md [moved from zucchini-lasagne.md with 100% similarity]
content/zucchinicremesuppe.md [moved from zucchinicremesuppe.md with 100% similarity]
content/zuckerglasur.md [moved from zuckerglasur.md with 100% similarity]
src/html.c [new file with mode: 0644]
src/main.c [new file with mode: 0644]
src/util.h [new file with mode: 0644]

similarity index 100%
rename from .template.md.tmp
rename to content/.template.md.tmp
similarity index 100%
rename from bechamelsosse.md
rename to content/bechamelsosse.md
similarity index 100%
rename from bohnenmus.md
rename to content/bohnenmus.md
similarity index 100%
rename from burger.md
rename to content/burger.md
similarity index 100%
rename from donauwelle.md
rename to content/donauwelle.md
similarity index 100%
rename from griesssuppe.md
rename to content/griesssuppe.md
similarity index 100%
rename from gulaschsuppe.md
rename to content/gulaschsuppe.md
similarity index 100%
rename from haferbrei.md
rename to content/haferbrei.md
similarity index 100%
rename from hefeteig.md
rename to content/hefeteig.md
similarity index 100%
rename from lasagne.md
rename to content/lasagne.md
similarity index 100%
rename from lauchgemuese.md
rename to content/lauchgemuese.md
similarity index 100%
rename from linseneintopf.md
rename to content/linseneintopf.md
similarity index 100%
rename from marmorkuchen.md
rename to content/marmorkuchen.md
similarity index 100%
rename from nusszopf.md
rename to content/nusszopf.md
similarity index 100%
rename from power-muesli.md
rename to content/power-muesli.md
similarity index 100%
rename from quarkoelteig.md
rename to content/quarkoelteig.md
similarity index 100%
rename from salsa-dip.md
rename to content/salsa-dip.md
similarity index 100%
rename from schinkennudeln.md
rename to content/schinkennudeln.md
similarity index 100%
rename from spaetzle.md
rename to content/spaetzle.md
similarity index 100%
rename from spinatquiche.md
rename to content/spinatquiche.md
similarity index 100%
rename from thunfischsalat.md
rename to content/thunfischsalat.md
similarity index 100%
rename from thunfischsosse.md
rename to content/thunfischsosse.md
similarity index 100%
rename from tomatensosse.md
rename to content/tomatensosse.md
similarity index 100%
rename from tortilla.md
rename to content/tortilla.md
similarity index 100%
rename from wurstsalat.md
rename to content/wurstsalat.md
similarity index 100%
rename from zitronenkuchen.md
rename to content/zitronenkuchen.md
similarity index 100%
rename from zuckerglasur.md
rename to content/zuckerglasur.md
diff --git a/src/html.c b/src/html.c
new file mode 100644 (file)
index 0000000..801f0f4
--- /dev/null
@@ -0,0 +1,325 @@
+/**
+ * @author
+ *  Lukas Hägele
+ *
+ * @brief
+ *  html handling
+ */
+
+#include "util.h"
+
+#define ElementNameTable \
+    X(invalid) \
+               \
+    X(html)    \
+    X(head)    \
+    X(meta)    \
+    X(title)   \
+    X(style)   \
+    X(body)    \
+               \
+    X(h1)      \
+    X(h2)      \
+    X(h3)      \
+    X(h4)      \
+    X(h5)      \
+    X(h6)      \
+    X(p)       \
+    X(ul)      \
+    X(li)      \
+               \
+    X(content)
+
+typedef enum
+{
+#define X(Name_) HTML_ELEMENT_NAME_##Name_,
+    ElementNameTable
+#undef X
+} html_element_name;
+
+typedef struct html_attribute
+{
+    struct html_attribute* Next;
+
+    str Key;
+    str Value;
+} html_attribute;
+
+typedef struct html_element
+{
+    struct html_element* Prev;
+    struct html_element* Next;
+    struct html_element* Value;
+
+    u8 ElementName;
+    union {
+        html_attribute* Attribute;
+        str Content;
+    };
+} html_element;
+
+typedef html_element html;
+
+typedef struct
+{
+    html_element Html;
+} html_markdownContext;
+
+static void
+appendElement(html_element* Sentinel, html_element* Element)
+{
+    /* todo: create sentinel in html_create() */
+    Element->Prev = Sentinel->Prev;
+    Sentinel->Prev->Next = Element;
+    Element->Next = Sentinel;
+}
+
+static inline html_element*
+createContent(str ContentStr, arena* Arena)
+{
+    ContentStr = str_stripHead(ContentStr);
+
+    html_element* Content = ARENA_PUSH_STRUCT(Arena, html_element);
+    {
+        Content->ElementName = HTML_ELEMENT_NAME_content;
+        Content->Content     = ContentStr;
+    }
+
+    return Content;
+}
+
+static void
+parseHeading(str Line, html_element* Html, arena* Arena)
+{
+    html_element* Heading = ARENA_PUSH_STRUCT(Arena, html_element);
+
+    /* identify element name (depth level) */
+    u8 Depth = 0u;
+    {
+        for (memory_size i = 0u; i < Line.Length; i++)
+        {
+            if (Line.Base[i] == '#')
+            {
+                Depth++;
+            }
+            else
+            {
+                break;
+            }
+        }
+
+        u8 Offset = Depth - 1;
+        u8 Base   = HTML_ELEMENT_NAME_h1;
+        Heading->ElementName = Base + Offset;
+    }
+
+    /* process content of heading */
+    {
+        str HeadingContent = Line;
+        {
+            HeadingContent.Base     += Depth;
+            HeadingContent.Length   -= Depth;
+            HeadingContent.Capacity -= Depth;
+        }
+
+        Heading->Value = createContent(HeadingContent, Arena);
+    }
+
+    /* todo: check if this is actually the sentinel! */
+    html_element* Sentinel = Html;
+    appendElement(Sentinel, Heading);
+}
+
+static void
+parseListItem(str Line, html_element* Html, arena* Arena)
+{
+    html_element* ListItem = ARENA_PUSH_STRUCT(Arena, html_element);
+    {
+        ListItem->ElementName = HTML_ELEMENT_NAME_li;
+    }
+
+    /* process content */
+    {
+        str ListItemContent = Line;
+        {
+            ListItemContent.Base     += 1u;
+            ListItemContent.Length   -= 1u;
+            ListItemContent.Capacity -= 1u;
+        }
+
+        ListItem->Value = createContent(ListItemContent, Arena);
+    }
+
+    /* append to unordered list */
+    {
+        html_element* List        = 0;
+        html_element* PrevElement = Html->Prev;
+        if (PrevElement->ElementName == HTML_ELEMENT_NAME_ul)
+        {
+            List = PrevElement;
+        }
+        else
+        {
+            /* todo: allocate sentinel? */
+            List = ARENA_PUSH_STRUCT(Arena, html_element);
+            List->ElementName = HTML_ELEMENT_NAME_ul;
+        }
+
+        html_element* ListItemSentinel = List->Value;
+        appendElement(ListItemSentinel, ListItem);
+    }
+}
+
+static void
+parseParagraph(str Line, html_element* Html, arena* Arena)
+{
+    html_element* ParagraphItem = ARENA_PUSH_STRUCT(Arena, html_element);
+    {
+        ParagraphItem->ElementName = HTML_ELEMENT_NAME_content;
+        ParagraphItem->Value       = createContent(Line, Arena);
+    }
+
+    /* append to paragraph */
+    {
+        html_element* Paragraph   = 0;
+        html_element* PrevElement = Html->Prev;
+
+        if ((str_isWhitespaceOnly(Line)) ||
+            (PrevElement->ElementName != HTML_ELEMENT_NAME_p))
+        {
+            Paragraph = ARENA_PUSH_STRUCT(Arena, html_element);
+            Paragraph->ElementName = HTML_ELEMENT_NAME_p;
+        }
+        else
+        {
+            Paragraph = PrevElement;
+        }
+
+        html_element* ParagraphSentinel = Paragraph->Value;
+        appendElement(ParagraphSentinel, ParagraphItem);
+    }
+}
+
+static void
+parseLine(str Line, html_element* Html, arena* Arena)
+{
+    str Stripped = str_stripTail(Line);
+
+    switch (Stripped.Base[0])
+    {
+        case '#':
+        {
+            parseHeading(Stripped, Html, Arena);
+        } break;
+
+        case '-':
+        case '*':
+        {
+            parseListItem(Stripped, Html, Arena);
+        } break;
+
+        default:
+        {
+            parseParagraph(Stripped, Html, Arena);
+        } break;
+    }
+}
+
+static html
+html_parseMarkdown(str Source, arena* Arena)
+{
+    html_element Html = html_create();
+
+    for (str Line = str_getLine(Source);
+         str_isValid(Line);
+         Line = str_getLine(Source))
+    {
+        parseLine(Line, &Html, Arena);
+        str_advance(Source, Line.Length);
+    }
+
+    return Html;
+}
+
+static inline b32
+isValidElement(html_element* Element)
+{
+    b32 Result = 0;
+
+    if (Element != 0)
+    {
+        if (Element->ElementName != HTML_ELEMENT_NAME_invalid)
+        {
+            Result = 1;
+        }
+    }
+
+    return Result;
+}
+
+static inline b32
+isValidAttribute(html_attribute* Attribute)
+{
+    b32 Result = 0;
+
+    if (Attribute != 0)
+    {
+        Result = 1;
+    }
+
+    return Result;
+}
+
+static void
+serializeElement(str* Sentinel, html_element* Element)
+{
+    static char* ElementNames[] =
+    {
+    #define X(Name_) "#Name_",
+        ElementNameTable
+    #undef X
+    };
+
+    for (html_element* At = Element; isValidElement(At); At = At->Next)
+    {
+        if (Element->ElementName == HTML_ELEMENT_NAME_content)
+        {
+            str_append(Sentinel, Element->Content);
+        }
+        else
+        {
+            str_append(Sentinel, STR_LITERAL("<"));
+            str_append(Sentinel, STR_LITERAL(ElementNames[Element->ElementName]));
+            {
+                html_attribute* Attribute = Element->Attribute;
+                if (isValidAttribute(Attribute))
+                {
+                    str_append(Sentinel, Attribute->Key);
+                    str_append(Sentinel, STR_LITERAL("=\""));
+                    str_append(Sentinel, Attribute->Value);
+                    str_append(Sentinel, STR_LITERAL("\""));
+                }
+            }
+            str_append(Sentinel, STR_LITERAL(">"));
+
+            serializeElement(Sentinel, Element->Value);
+
+            str_append(Sentinel, STR_LITERAL("<"));
+            str_append(Sentinel, STR_LITERAL(ElementNames[Element->ElementName]));
+            str_append(Sentinel, STR_LITERAL("\\>"));
+        }
+    }
+}
+
+static str
+html_toString(arena* Arena, html* Html)
+{
+    str_unbounded Unbounded = str_startUnbounded(Arena);
+    {
+        str* Line = &Unbounded.Str;
+        serializeElement(Line, Html);
+    }
+    str Result = str_endUnbounded(Unbounded);
+    return Result;
+}
+
diff --git a/src/main.c b/src/main.c
new file mode 100644 (file)
index 0000000..b632ffb
--- /dev/null
@@ -0,0 +1,280 @@
+/**
+ * @author
+ *  Lukas Hägele
+ *
+ * @brief
+ *  static site generator for my recipe collection
+ *
+ * @todo
+ *  - (german) unicode support
+ *
+ *  - markdown parser
+ *    - multithreaded (one thread per recipe)
+ *    - output status from main thread?
+ *    - front matter
+ *      - encoding (file extension)
+ *      - tags
+ *      - date created
+ *      - date changed (optional)
+ *
+ *  - images (optional)
+ *
+ *  - output to html directory
+ *    - sortable table? (sort by most recent, by name, ...)
+ *    - navigation (return to main page from recipe)
+ *
+ *  - combine connected foods (lasagne)
+ */
+
+#define _POSIX_C_SOURCE 200112L
+
+#include "util.h"
+#include "html.c"
+
+#include <dirent.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+
+static u32
+generateHtmlFile(str Filename, html* Html)
+{
+    u32 Error = 0;
+
+    enum { MAX_PATH = 1024 };
+    char Path[MAX_PATH] = {0};
+
+    /* todo: create path as c string */
+    str_toCString(Path, sizeof(Path), Filename);
+
+    int FileDescriptor = open(Path, O_WRONLY|O_CREAT);
+    if (FileDescriptor == -1)
+    {
+        perror("open");
+        Error = 1;
+    }
+    else
+    {
+        str HtmlContent = html_toString(Html);
+
+        if (write(FileDescriptor, HtmlContent.Buffer, HtmlContent.Length) == -1)
+        {
+            perror("write");
+            Error = 1;
+        }
+
+        if (close(FileDescriptor) == -1)
+        {
+            perror("close");
+            Error = 1;
+        }
+    }
+
+    return Error;
+}
+
+typedef struct node
+{
+    struct node* Next;
+    str Name;
+} recipe, tag;
+
+int
+main(int ArgumentCount, char** Arguments)
+{
+    struct
+    {
+        char* SourceDir;
+        char* OutputDir;
+
+        recipe* Recipes;
+        tag*    Tag;
+
+        u64 RecipeCount;
+        arena MainArena;
+    } Context;
+
+    /* handle commandline arguments */
+    if (ArgumentCount != 3)
+    {
+        fprintf(stderr, "usage: converter <source_dir> <output_dir>\n");
+        return -1;
+    }
+    else
+    {
+        Context.SourceDir = Arguments[1];
+        Context.OutputDir = Arguments[2];
+    }
+
+    Context.MainArena = arena_create(MEGABYTES(16));
+
+    /* enumerate recipe files */
+    {
+        DIR* Directory = opendir(Context.SourceDir);
+        if (Directory == NULL)
+        {
+            perror("opendir");
+            return -1;
+        }
+
+        struct dirent* Entry;
+        while ((Entry = readdir(Directory)) != NULL)
+        {
+            recipe* New = ARENA_PUSH_STRUCT(&Context.MainArena, recipe);
+            New->Name = str_fromCString(&Context.MainArena, Entry->d_name);
+            New->Next = Context.Recipes;
+
+            Context.Recipes = New;
+            Context.RecipeCount++;
+        }
+
+        if (closedir(Directory) == -1)
+        {
+            perror("closedir");
+            return -1;
+        }
+    }
+
+    /* todo: parse recipe files (multithreaded) */
+    Recipe->Html = html_parseMarkdown();
+
+#if 0
+    /* todo: distill unique sorted tags */
+
+    /* sort recipes */
+    {
+        u32 WasReordered = 0;
+
+        /* todo: implement faster sorting */
+        do
+        {
+            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;
+
+                s32 Result = 0;
+
+                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;
+                    }
+                }
+
+                for (u64 StringIndex = 0; StringIndex < CheckLength; StringIndex)
+                {
+                    u8 ALow = TO_LOWERCASE(AStr.Base[StringIndex]);
+                    u8 BLow = TO_LOWERCASE(BStr.Base[StringIndex]);
+
+                    Result = ALow - BLow;
+                    if (Result != 0)
+                    {
+                        break;
+                    }
+                }
+
+                if (Result != 0)
+                {
+                    WasReordered = 1;
+
+                    if (Result > 0)
+                    {
+                        if (Previous == Recipes)
+                        {
+                            Recipes = B;
+                        }
+                        else
+                        {
+                            Previous->Next = B;
+                        }
+
+                        A->Next = B->Next;
+                        B->Next = A;
+                    }
+                }
+
+                Previous = A;
+            }
+        }
+        while (WasReordered == 1);
+    }
+#endif
+
+    /* generate html output */
+    {
+        /* main page */
+        {
+            /* todo: assemble html */
+            html MainPage = html_create();
+            {
+                html_append(MainPage, HTML_H2, "Tags");
+                html_start(MainPage, HTML_UL);
+                for (tag* Tag = Tags; isValid(Tag); Tag = Tag->Next)
+                {
+                    html_append(MainPage, HTML_LI, Tag->Name);
+                }
+                html_end(MainPage, HTML_UL);
+
+                html_append(MainPage, HTML_H2, "Rezepte");
+                html_start(MainPage, HTML_UL);
+                for (recipe* Recipe = Recipes; isValid(Recipe); Recipe = Recipe->Next)
+                {
+                    html_append(MainPage, HTML_LI, Recipe->Name);
+                }
+                html_end(MainPage, HTML_UL);
+            }
+
+            str Filename = STR_LITERAL("index.html");
+            if (generateHtmlFile(Filename, MainPage) != 0)
+            {
+                fprintf(stderr, "Error while generating main page. Stopping.\n");
+                return -1;
+            }
+        }
+
+        /* detail pages */
+        for (recipe* Recipe = Recipes; isValid(Recipe); Recipe = Recipe->Next)
+        {
+            /* recipes/... */
+            str Filename = ...;
+
+            if (generateHtmlFile(Filename, Recipe->Html) != 0)
+            {
+                fprintf(stderr, "Error while generating detail page. Stopping.\n");
+                return -1;
+            }
+        }
+
+        /* tag page? */
+        for (tag* Tag = Tags; isValid(Tag); Tag = Tag->Next)
+        {
+            /* tags/... */
+            str Filename = ...;
+
+            if (generateHtmlFile(Filename, Tag->Html) != 0)
+            {
+                fprintf(stderr, "Error while generating tag page. Stopping.\n");
+                return -1;
+            }
+        }
+    }
+
+    fprintf(stdout, "Done.\n");
+
+    return 0;
+}
+
diff --git a/src/util.h b/src/util.h
new file mode 100644 (file)
index 0000000..86c6a9c
--- /dev/null
@@ -0,0 +1,286 @@
+/**
+ * @author
+ *  Lukas Hägele
+ *
+ * @brief
+ *  utility stuff that is used in all modules
+ */
+
+#pragma once
+
+#define ASSERT(Cond_)       if ((Cond_) == 0) { *(volatile int*)0 = 0; }
+#define INVALID_CODE_PATH   ASSERT(!"invalid code path")
+
+#define MIN(A_, B_) (((A_) < (B_)) ? (A_) : (B_))
+
+#define MEGABYTES(S_) ((S_) * 1024 * 1024)
+
+#include <stdint.h>
+typedef uint8_t  u8;
+typedef uint32_t u32;
+typedef uint64_t u64;
+typedef int32_t  s32;
+
+typedef u32 b32;
+typedef u64 memory_size;
+
+typedef struct
+{
+    u8*         Base;
+    memory_size Length;
+    memory_size Capacity;
+} buffer;
+
+
+/* arena handling */
+
+#include <malloc.h>
+
+typedef buffer arena;
+
+static inline arena
+arena_create(memory_size Size)
+{
+    arena Arena = {0};
+
+    void* Memory = malloc(Size);
+    if (Memory != NULL)
+    {
+        Arena.Base = (u8*)Memory;
+        Arena.Capacity = Size;
+    }
+    else
+    {
+        perror("malloc");
+    }
+
+    return Arena;
+}
+
+static inline void*
+arena_push(arena* Arena, memory_size Size, b32 Zero)
+{
+    ASSERT( (Arena->Length + Size) < Arena->Capacity);
+
+    u8* Memory = Arena->Base + Arena->Length;
+    if (Zero != 0)
+    {
+        for (memory_size i = 0; i < Size; i++)
+        {
+            Memory[i] = 0;
+        }
+    }
+
+    Arena->Length += Size;
+
+    return Memory;
+}
+
+#define ARENA_PUSH_STRUCT(Arena_, Struct_)        (Struct_*)arena_push(Arena_,  sizeof(Struct_)          , 1)
+#define ARENA_PUSH_ARRAY(Arena_, Struct_, Count_) (Struct_*)arena_push(Arena_, (sizeof(Struct_) * Count_), 1)
+
+
+/* string handling */
+
+#define TO_LOWERCASE(C_) ( ((C_ >= 'A') && (C_ <= 'Z')) ? (C_ | 0x20) : C_ )
+
+typedef buffer str;
+
+#define STR_LOCAL(Buffer_)    (str){ .Base = Buffer_,       .Length = 0,                    .Capacity = sizeof(Buffer_) }
+#define STR_LITERAL(Literal_) (str){ .Base = (u8*)Literal_, .Length = (sizeof(Literal_)-1), .Capacity = 0               }
+
+static inline void
+str_append(str* Target, str Source)
+{
+    ASSERT( (Target->Length + Source.Length) < Target->Capacity);
+
+    u8* Memory = Target->Base + Target->Length;
+    for (u64 i = 0u; i < Source.Length; i++)
+    {
+        Memory[i] = Source.Base[i];
+    }
+
+    Target->Length += Source.Length;
+}
+
+static inline str
+str_getLine(str Str)
+{
+    str Line = {0};
+
+    if (Str.Length > 0u)
+    {
+        Line.Base = Str.Base;
+
+        memory_size Index = 0u;
+        for (memory_size i = 0u; i < Str.Length; i++)
+        {
+            if (Str.Base[i] == '\n')
+            {
+                Line.Length = i;
+                break;
+            }
+        }
+    }
+
+    return Line;
+}
+
+static inline void
+str_advance(str Str, memory_size Increment)
+{
+    ASSERT( (Str.Length + Increment) < Str.Capacity );
+
+    Str.Length += Increment;
+}
+
+static inline b32
+isWhitespace(char Char)
+{
+    b32 Result = 0u;
+
+    if ((Char == ' ')  ||
+        (Char == '\t') ||
+        (Char == '\n'))
+    {
+        Result = 1u;
+    }
+
+    return Result;
+}
+
+static inline str
+str_stripHead(str Str)
+{
+    memory_size InitialLength = Str.Length;
+
+    for (memory_size i = 0u; i < InitialLength; i++)
+    {
+        u8 Candidate = Str.Base[i];
+        if (!isWhitespace(Candidate))
+        {
+            Str.Base     += i;
+            Str.Length   -= i;
+            Str.Capacity -= i;
+            break;
+        }
+    }
+
+    str Result = Str;
+    return Result;
+}
+
+static inline str
+str_stripTail(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 (isWhitespace(Candidate))
+        {
+            Str.Length--;
+        }
+        else
+        {
+            break;
+        }
+    }
+
+    str Result = Str;
+    return Result;
+}
+
+static inline b32
+str_isWhitespaceOnly(str Str)
+{
+    b32 Result = 1u;
+
+    for (memory_size i = 0u; i < Str.Length; i++)
+    {
+        if (!isWhitespace(Str.Base[i]))
+        {
+            Result = 0u;
+            break;
+        }
+    }
+
+    return Result;
+}
+
+static inline b32
+str_isValid(str Str)
+{
+    b32 Result = 0;
+
+    if (Str.Base != 0)
+    {
+        Result = 1;
+    }
+
+    return Result;
+}
+
+
+typedef struct
+{
+    arena* Arena;
+    str Str;
+} str_unbounded;
+
+static inline str_unbounded
+str_startUnbounded(arena* Arena)
+{
+    str_unbounded Unbounded =
+    {
+        .Arena = Arena,
+        .Str =
+        {
+            .Base     = Arena->Base     + Arena->Length,
+            .Capacity = Arena->Capacity - Arena->Length
+        }
+    };
+
+    return Unbounded;
+}
+
+static inline str
+str_endUnbounded(str_unbounded Unbounded)
+{
+    arena_push(Unbounded.Arena, Unbounded.Str.Length, 0);
+    Unbounded.Str.Capacity = Unbounded.Str.Length;
+    return Unbounded.Str;
+}
+
+static inline void
+str_toCString(char* Destination, memory_size DestinationSize, str Source)
+{
+    ASSERT(DestinationSize > Source.Length);
+
+    for (u64 i = 0; i < Source.Length; i++)
+    {
+        Destination[i] = Source.Base[i];
+    }
+
+    Destination[Source.Length] = '\0';
+}
+
+static inline str
+str_fromCString(arena* Arena, char* CStr)
+{
+    char* Start = CStr;
+
+    while (*CStr++ != '0');
+    u64 Length = CStr - Start;
+    u8* Target = ARENA_PUSH_ARRAY(Arena, u8, Length);
+
+    for (u64 i = 0u; i < Length; i++)
+    {
+        Target[i] = Start[i];
+    }
+
+    str Result = { .Base = Target, .Length = Length, .Capacity = Length };
+    return Result;
+}