From: Lukas Hägele Date: Sun, 9 Mar 2025 14:57:45 +0000 (+0100) Subject: add "recently added" list X-Git-Url: https://git.lhaegele.de/?a=commitdiff_plain;h=7d92fdf2d93fbbc9434d67474589993c934879ce;p=recipes.git add "recently added" list --- diff --git a/content/.template.md.tmp b/content/.template.md.tmp index 30a5f8c..584008d 100644 --- a/content/.template.md.tmp +++ b/content/.template.md.tmp @@ -1,6 +1,10 @@ --- #!ini +[details] +date = 20.01.2025 18:53 +portions = 4 + [tags] --- diff --git a/content/schoko-nusskuchen-mit-kirschen.md b/content/schoko-nusskuchen-mit-kirschen.md index 6589619..62bc253 100644 --- a/content/schoko-nusskuchen-mit-kirschen.md +++ b/content/schoko-nusskuchen-mit-kirschen.md @@ -1,6 +1,9 @@ --- #!ini +[details] +date = 02.03.2025 13:55 + [tags] kuchen --- diff --git a/html/index.html b/html/index.html index 9e5e346..2525ce4 100755 --- a/html/index.html +++ b/html/index.html @@ -1 +1 @@ -Meine Rezeptsammlung | lhaegele.de

Meine Rezeptsammlung

Rezepte

Tags

\ No newline at end of file +Meine Rezeptsammlung | lhaegele.de

Meine Rezeptsammlung

Zuletzt hinzugefügt

Rezepte

Tags

\ No newline at end of file diff --git a/html/style.css b/html/style.css index e4d5af0..d661af2 100644 --- a/html/style.css +++ b/html/style.css @@ -84,7 +84,11 @@ ul { ul.recipes { column-count: 3; } - + ul.recent { + display: flex; + flex-direction: column; + justify-content: center; + } ul.tags { column-count: 4; } diff --git a/src/html.c b/src/html.c index 681cb11..fd53e5c 100644 --- a/src/html.c +++ b/src/html.c @@ -73,9 +73,25 @@ typedef struct html_tag str Content; } html_tag; +typedef struct +{ + u16 Year; + u8 Month; + u8 Day; + u8 Hour; + u8 Minute; +} html_datetime; + +typedef struct +{ + str Str; + html_datetime Datetime; +} html_date; + typedef struct { html_tag* Tags; + html_date Date; } html_meta; typedef struct @@ -512,6 +528,37 @@ parseParagraph(str Line, html_element* PrevElement, arena* Arena) #endif } +static inline u16 +u16AtOffset(str Str, memory_size Offset, memory_size Length) +{ + str Substr = str_view(Str, Offset, Length); + u16 Result = str_toU16(Substr); + return Result; +} + +static inline html_datetime +calcEpoch(str Str) +{ + html_datetime Datetime = {0}; + + /* DD.MM.YYYY HH:MM */ + Datetime.Day = u16AtOffset(Str, 0, 2); + Datetime.Month = u16AtOffset(Str, 3, 2); + Datetime.Year = u16AtOffset(Str, 6, 4); + Datetime.Hour = u16AtOffset(Str, 11, 2); + Datetime.Minute = u16AtOffset(Str, 14, 2); + + return Datetime; +} + +typedef enum +{ + SECTION_TYPE_none, + + SECTION_TYPE_details, + SECTION_TYPE_tags +} SECTION_TYPE; + static html* html_parseMarkdown(str Source, arena* Arena) { @@ -522,8 +569,7 @@ html_parseMarkdown(str Source, arena* Arena) b32 ExpectShebang = 0; b32 SkipIniSection = 0; - // todo: remove - //b32 PreviousLineWasEmpty = 0; + SECTION_TYPE SectionType = SECTION_TYPE_none; html_element* PrevElement = Html->Article; for (str Line = str_getLine(Source); @@ -541,7 +587,6 @@ html_parseMarkdown(str Source, arena* Arena) { Frontmatter = 1; ExpectShebang = 1; - //PreviousLineWasEmpty = 0; } else if (str_startsWith(Stripped, STR_LITERAL("#"))) { @@ -551,19 +596,14 @@ html_parseMarkdown(str Source, arena* Arena) str_startsWith(Line, STR_LITERAL("*"))) { PrevElement = parseListItem(Stripped, PrevElement, Arena); - //PreviousLineWasEmpty = 0; } else if (Stripped.Length > 0) { - /* todo: check if `PreviousLineWasEmpty` is really necessary */ PrevElement = parseParagraph(Stripped, PrevElement, Arena); - //PrevElement = parseParagraph(Stripped, PreviousLineWasEmpty, PrevElement, Arena); - //PreviousLineWasEmpty = 0; } } else { - //PreviousLineWasEmpty = 1; PrevElement = Html->Article; } } @@ -591,9 +631,13 @@ html_parseMarkdown(str Source, arena* Arena) if (0); else if (str_startsWith(Stripped, STR_LITERAL("["))) { - if (str_equals(Stripped, STR_LITERAL("[tags]"))) + if (str_equals(Stripped, STR_LITERAL("[details]"))) { - /* skip section header */ + SectionType = SECTION_TYPE_details; + } + else if (str_equals(Stripped, STR_LITERAL("[tags]"))) + { + SectionType = SECTION_TYPE_tags; } else { @@ -607,8 +651,32 @@ html_parseMarkdown(str Source, arena* Arena) } else if (Stripped.Length > 0) { - html_tag* Sentinel = Html->Meta->Tags; - appendTag(Sentinel, Stripped, Arena); + switch (SectionType) + { + case SECTION_TYPE_details: + { + if (str_startsWith(Stripped, STR_LITERAL("date"))) + { + html_date* Date = &Html->Meta->Date; + Stripped = str_removeStart(Stripped, STR_LITERAL("date")); + Stripped = str_removeStart(Stripped, STR_LITERAL("=")); + Date->Str = Stripped; + Date->Datetime = calcEpoch(Date->Str); + } + else if (str_startsWith(Stripped, STR_LITERAL("portions"))) + { + /* todo: implement to process "portions" info */ + } + } break; + + case SECTION_TYPE_tags: + { + html_tag* Sentinel = Html->Meta->Tags; + appendTag(Sentinel, Stripped, Arena); + } break; + + INVALID_DEFAULT; + } } else { diff --git a/src/main.c b/src/main.c index 084958f..3b00a8d 100644 --- a/src/main.c +++ b/src/main.c @@ -53,6 +53,14 @@ typedef struct recipe str Name; } recipe; +typedef struct date +{ + struct date* Next; + struct date* Prev; + + recipe* Recipe; +} date; + typedef struct image { struct image* Next; @@ -194,6 +202,47 @@ searchImage(recipe* Recipe, image* Images) return Image; } +static inline b32 isValid_datetime(html_datetime Datetime) +{ + b32 Result = 0; + + if (Datetime.Year && Datetime.Month && Datetime.Day && + Datetime.Hour && Datetime.Minute) + { + Result = 1; + } + + return Result; +} + +static inline b32 isNewerThan(html_datetime D, html_datetime Ref) +{ + b32 Result = 0; + + if (D.Year > Ref.Year) + { + Result = 1; + } + else if (D.Month > Ref.Month) + { + Result = 1; + } + else if (D.Day > Ref.Day) + { + Result = 1; + } + else if (D.Hour > Ref.Hour) + { + Result = 1; + } + else if (D.Minute > Ref.Minute) + { + Result = 1; + } + + return Result; +} + static u32 generateHtmlFile(str Filename, html* Html, arena* Arena) { @@ -237,22 +286,55 @@ generateHtmlFile(str Filename, html* Html, arena* Arena) } static void -appendRecipeLink(html_element* RecipeElement, recipe* Recipe, arena* Arena) +appendLink(html_element* Parent, str Link, str Content, arena* Arena) { html_element* RecipeLink = html_createElement(HTML_ELEMENT_NAME_a, Arena); + html_appendContent(RecipeLink, Content, Arena); + html_prependAttribute(RecipeLink, STR_LITERAL("href"), Link, Arena); + html_appendChild(Parent, RecipeLink); +} + +static str +generateRecipePath(recipe* Recipe, arena* Arena) +{ + str_unbounded uPath = str_startUnbounded(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); + str_append(&uPath.Str, STR_LITERAL("/recipes/")); + str_append(&uPath.Str, str_stripExtension(Recipe->Filename)); + str_append(&uPath.Str, STR_LITERAL(".html")); + } + + str Path = str_endUnbounded(uPath); + return Path; +} - html_prependAttribute(RecipeLink, STR_LITERAL("href"), Link, Arena); - html_appendContent(RecipeLink, Recipe->Name, Arena); - html_appendChild(RecipeElement, RecipeLink); +static void +appendRecipeLink(html_element* RecipeElement, recipe* Recipe, arena* Arena) +{ + str Path = generateRecipePath(Recipe, Arena); + appendLink(RecipeElement, Path, Recipe->Name, Arena); +} + +static void +appendRecipeLinkWithDatetime(html_element* RecipeElement, date* Date, arena* Arena) +{ + recipe* Recipe = Date->Recipe; + + str Path = generateRecipePath(Recipe, Arena); + + str_unbounded uContent = str_startUnbounded(Arena); + { + str_append(&uContent.Str, Recipe->Name); + str_append(&uContent.Str, STR_LITERAL(" (")); + html_date Date = Recipe->Html->Meta->Date; + /* cut off the time */ + str DateStr = str_view(Date.Str, 0u, 10u); + str_append(&uContent.Str, DateStr); + str_append(&uContent.Str, STR_LITERAL(")")); } + str Content = str_endUnbounded(uContent); + + appendLink(RecipeElement, Path, Content, Arena); } int @@ -268,6 +350,7 @@ main(int ArgumentCount, char** Arguments) recipe* Recipes; tag* Tags; image* Images; + date* Dates; arena MainArena; } Context = {0}; @@ -578,6 +661,52 @@ main(int ArgumentCount, char** Arguments) } } + /* allocate date sentinel */ + { + date* DateSentinel = ARENA_PUSH_STRUCT(&Context.MainArena, date); + { + DateSentinel->Next = DateSentinel; + DateSentinel->Prev = DateSentinel; + + Context.Dates = DateSentinel; + } + } + + /* distill sorted date list */ + { + recipe* RecipeSentinel = Context.Recipes; + for (recipe* At = RecipeSentinel->Next; At != RecipeSentinel; At = At->Next) + { + html_datetime Datetime = At->Html->Meta->Date.Datetime; + if (isValid_datetime(Datetime)) + { + date* Date = ARENA_PUSH_STRUCT(&Context.MainArena, date); + Date->Recipe = At; + + date* DateSentinel = Context.Dates; + date* Next = DateSentinel; + if (DateSentinel->Next != DateSentinel) + { + for (date* Comp = DateSentinel->Next; Comp != DateSentinel; Comp = Comp->Next) + { + html_datetime CompDatetime = Comp->Recipe->Html->Meta->Date.Datetime; + if (isNewerThan(Datetime, CompDatetime)) + { + Next = Comp; + break; + } + } + } + + Date->Next = Next; + Date->Prev = Next->Prev; + + Next->Prev->Next = Date; + Next->Prev = Date; + } + } + } + /* generate html output */ { /* main page */ @@ -586,6 +715,33 @@ main(int ArgumentCount, char** Arguments) html* MainPage = html_createDefault(&Context.MainArena); html_setArticleClass(MainPage, STR_LITERAL("overview"), &Context.MainArena); + /* recently added */ + { + html_element* Recent = html_createElement(HTML_ELEMENT_NAME_h2, &Context.MainArena); + html_appendArticleSection(MainPage, Recent); + html_appendContent(Recent, STR_LITERAL("Zuletzt hinzugefügt"), &Context.MainArena); + + html_element* RecentList = html_createElement(HTML_ELEMENT_NAME_ul, &Context.MainArena); + html_appendArticleSection(MainPage, RecentList); + html_prependAttribute(RecentList, STR_LITERAL("class"), STR_LITERAL("recent"), &Context.MainArena); + + u8 RecentCount = 0u; + date* Sentinel = Context.Dates; + for (date* At = Sentinel->Next; At != Sentinel; At = At->Next) + { + html_element* RecentElement = html_createElement(HTML_ELEMENT_NAME_li, &Context.MainArena); + { + html_appendChild(RecentList, RecentElement); + appendRecipeLinkWithDatetime(RecentElement, At, &Context.MainArena); + } + + if (++RecentCount > 3) + { + break; + } + } + } + /* recipes */ { html_element* Recipes = html_createElement(HTML_ELEMENT_NAME_h2, &Context.MainArena); @@ -608,60 +764,61 @@ main(int ArgumentCount, char** Arguments) } } } + } - /* tags */ + /* tags */ + { + 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_element* Tags = html_createElement(HTML_ELEMENT_NAME_h2, &Context.MainArena); - html_appendArticleSection(MainPage, Tags); - html_appendContent(Tags, STR_LITERAL("Tags"), &Context.MainArena); + html_appendArticleSection(MainPage, TagList); + html_prependAttribute(TagList, STR_LITERAL("class"), STR_LITERAL("tags"), &Context.MainArena); - html_element* TagList = html_createElement(HTML_ELEMENT_NAME_ul, &Context.MainArena); + tag* Sentinel = Context.Tags; + for (tag* Tag = Sentinel->Next; Tag != Sentinel; Tag = Tag->Next) { - html_appendArticleSection(MainPage, TagList); - html_prependAttribute(TagList, STR_LITERAL("class"), STR_LITERAL("tags"), &Context.MainArena); - - 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_element* TagElement = html_createElement(HTML_ELEMENT_NAME_li, &Context.MainArena); - { - html_appendChild(TagList, TagElement); + html_appendChild(TagList, TagElement); - html_element* TagLink = html_createElement(HTML_ELEMENT_NAME_a, &Context.MainArena); + html_element* TagLink = html_createElement(HTML_ELEMENT_NAME_a, &Context.MainArena); + { + str_unbounded uPath = str_startUnbounded(&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_prependAttribute(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); + 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_prependAttribute(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); } } } } + } - /* serialize html tree */ + + /* serialize html tree */ + { + arena TempArena = Context.MainArena; + str_unbounded uPath = str_startUnbounded(&TempArena); { - 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); + 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 (generateHtmlFile(Filepath, MainPage, &Context.MainArena) != 0) + { + fprintf(stderr, "Error while generating main page. Stopping.\n"); + return -1; } } } diff --git a/src/util.h b/src/util.h index f50288c..d31ff21 100644 --- a/src/util.h +++ b/src/util.h @@ -10,6 +10,8 @@ #define ASSERT(Cond_) if ((Cond_) == 0) { *(volatile int*)0 = 0; } #define INVALID_CODE_PATH ASSERT(!"invalid code path") +#define INVALID_DEFAULT default: { INVALID_CODE_PATH; break; } +#define NOT_YET_IMPLEMENTED ASSERT(!"not yet implemented") #define MIN(A_, B_) (((A_) < (B_)) ? (A_) : (B_)) @@ -17,6 +19,7 @@ #include typedef uint8_t u8; +typedef uint16_t u16; typedef uint32_t u32; typedef uint64_t u64; typedef int32_t s32; @@ -297,6 +300,40 @@ str_startsWith(str A, str Start) return Result; } +static inline str +str_view(str Str, memory_size Offset, memory_size Length) +{ + str Result = {0}; + + Result.Base = &Str.Base[Offset]; + Result.Length = Length; + Result.Capacity = (Str.Capacity) ? (Str.Capacity - Offset) : 0u; + + return Result; +} + +static inline str +str_viewAtOffset(str Str, memory_size Offset) +{ + memory_size Length = Str.Length - Offset; + str Result = str_view(Str, Offset, Length); + return Result; +} + +static inline str +str_removeStart(str Str, str Start) +{ + str Result = Str; + + if (str_startsWith(Str, Start)) + { + Result = str_viewAtOffset(Str, Start.Length); + Result = str_stripHead(Result); + } + + return Result; +} + static inline b32 str_endsWith(str A, str End) { @@ -438,3 +475,27 @@ str_fromCString(arena* Arena, char* CStr) return Result; } +static inline u16 +str_toU16(str Str) +{ + u16 Result = 0u; + + for (memory_size i = 0u; i < Str.Length; i++) + { + memory_size Index = Str.Length - i - 1u; + u8 DigitLetter = Str.Base[Index]; + u8 Digit = DigitLetter - '0'; + + u16 DecadeFactor = 1; + memory_size Decade = i; + while (Decade--) + { + DecadeFactor *= 10; + } + + Result += Digit * DecadeFactor; + } + + return Result; +} +