--- /dev/null
+/**
+ * @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;
+}
+
--- /dev/null
+/**
+ * @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;
+}
+
--- /dev/null
+/**
+ * @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;
+}