#if 0 ctime -begin ${0%.*}.ctm gcc -g -fsanitize=address -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl ctime -end ${0%.*}.ctm exit #endif #define DEBUG 0 typedef unsigned int bool; #define TRUE 1 #define FALSE 0 #include // NOTE(matt): varargs #include // NOTE(matt): printf, sprintf, vsprintf, fprintf, perror #include // NOTE(matt): calloc, malloc, free #include "hmmlib.h" #include // NOTE(matt): getopts //#include "config.h" // TODO(matt): Implement config.h #include #include #include #include #include #include // NOTE(matt): strerror #include //NOTE(matt): errno #include // NOTE(matt): inotify #include // NOTE(matt): sleep() #define Kilobytes(Bytes) Bytes << 10 #define Megabytes(Bytes) Bytes << 20 enum { EDITION_SINGLE, EDITION_PROJECT, EDITION_NETWORK } editions; enum { // NOTE(matt): https://tools.ietf.org/html/rfc5424#section-6.2.1 LOG_EMERGENCY, LOG_ALERT, LOG_CRITICAL, LOG_ERROR, LOG_WARNING, LOG_NOTICE, LOG_INFORMATIONAL, LOG_DEBUG } log_levels; enum { MODE_BARE, MODE_INTEGRATE } modes; enum { RC_ARENA_FULL, RC_ERROR_DIRECTORY, RC_ERROR_FATAL, RC_ERROR_FILE, RC_ERROR_HMML, RC_ERROR_MAX_REFS, RC_ERROR_MEMORY, RC_ERROR_QUOTE, RC_ERROR_TEMPLATE, RC_FAILURE, RC_FOUND, RC_UNFOUND, RC_INVALID_TEMPLATE, RC_INVALID_REFERENCE, RC_NOOP, RC_REFRESHED, RC_RIP, RC_SUCCESS } returns; typedef struct { char *BaseDir; char CacheDir[255]; char *CSSDir; int Edition; char *ImagesDir; char *JSDir; int LogLevel; char *DefaultMedium; int Mode; char *OutLocation; char *OutIntegratedLocation; bool ForceIntegration; char *ProjectDir; char *TemplateIndexLocation; char *TemplatePlayerLocation; } config; typedef struct { void *Location; void *Ptr; char *ID; int Size; } arena; typedef struct { char *Location; char *Ptr; char *ID; int Size; } buffer; enum { TAG_INDEX, TAG_INCLUDES, TAG_MENUS, TAG_PLAYER, TAG_SCRIPT, TAG_TITLE } template_tags; typedef struct { int Code; // template_tags char *Tag; } tag; tag Tags[] = { { TAG_INDEX, "__CINERA_INDEX__" }, { TAG_INCLUDES, "__CINERA_INCLUDES__" }, { TAG_MENUS, "__CINERA_MENUS__" }, { TAG_PLAYER, "__CINERA_PLAYER__" }, { TAG_SCRIPT, "__CINERA_SCRIPT__" }, { TAG_TITLE, "__CINERA_TITLE__" }, }; typedef struct { int Offset; int TagCode; } tag_offset; typedef struct { char Filename[120]; tag_offset Tag[16]; int Validity; // NOTE(matt): Bitmask describing which page the template is valid for, i.e. contents and / or player page int TagCount; } template; typedef struct { buffer IncludesIndex; buffer Index; buffer IncludesPlayer; buffer Menus; buffer Player; buffer Script; char Title[255]; char Project[32]; } buffers; // TODO(matt): Consider putting the ref_info and quote_info into linked lists on the heap, just to avoid all the hardcoded sizes typedef struct { char Date[32]; char Text[512]; } quote_info; typedef struct { char Timecode[8]; int Identifier; } identifier; #define REF_MAX_IDENTIFIER 64 typedef struct { char RefTitle[620]; char ID[512]; char URL[512]; char Source[256]; identifier Identifier[REF_MAX_IDENTIFIER]; int IdentifierCount; } ref_info; typedef struct { char Marker[32]; char WrittenText[32]; } category_info; typedef struct { category_info Category[64]; int Count; } categories; // TODO(matt): Parse this stuff out of a config file typedef struct { char *Username; char *CreditedName; char *HomepageURL; char *SupportIcon; char *SupportURL; } credential_info; credential_info Credentials[] = { { "Miblo", "Matt Mascarenhas", "http://miblodelcarpio.co.uk", "cinera_icon_patreon.png", "https://patreon.com/miblo"}, { "miotatsu", "Mio Iwakura", "http://riscy.tv/", "cinera_icon_patreon.png", "https://patreon.com/miotatsu"}, { "nothings", "Sean Barrett", "https://nothings.org/", "", ""}, { "cmuratori", "Casey Muratori", "https://handmadehero.org", "cinera_icon_patreon.png", "https://patreon.com/cmuratori"}, { "fierydrake", "Mike Tunnicliffe", "", "", ""}, { "abnercoimbre", "Abner Coimbre", "https://handmade.network/m/abnercoimbre", "cinera_icon_patreon.png", "https://patreon.com/handmade_dev"}, { "/y_lee", "Yunsup Lee", "https://www.linkedin.com/in/yunsup-lee-385b692b/", "", ""}, { "/a_waterman", "Andrew Waterman", "https://www.linkedin.com/in/andrew-waterman-76805788", "", ""}, { "debiatan", "Miguel Lechón", "http://blog.debiatan.net/", "", ""}, }; typedef struct { char *Medium; char *Icon; char *WrittenName; } category_medium; category_medium CategoryMedium[] = { // medium icon written name { "afk", "…" , "Away from Keyboard"}, // TODO(matt): Filter this out by default { "authored", "🗪", "Chat Comment"}, // TODO(matt): Conditionally handle Chat vs Guest Comments { "blackboard", "🖌", "Blackboard"}, { "experience", "🍷", "Experience"}, { "owl", "🦉", "Owl of Shame"}, { "programming", "🖮", "Programming"}, // TODO(matt): Potentially make this configurable per project { "rant", "💢", "Rant"}, { "research", "📖", "Research"}, { "run", "🏃", "In-Game"}, // TODO(matt): Potentially make this configurable per project { "trivia", "🎲", "Trivia"}, }; #define ArrayCount(A) sizeof(A)/sizeof(*(A)) __attribute__ ((format (printf, 2, 3))) void CopyString(char Dest[], char *Format, ...) { va_list Args; va_start(Args, Format); vsprintf(Dest, Format, Args); va_end(Args); } int StringLength(char *String) { int i = 0; while(String[i]) { ++i; } return i; } void CopyBuffer(buffer *Dest, buffer *Src) { Src->Ptr = Src->Location; while(*Src->Ptr) { // TODO(matt) { if(Dest->Ptr - Dest->Location >= Dest->Size) { fprintf(stderr, "CopyBuffer: %s cannot accommodate %s\n", Dest->ID, Src->ID); __asm__("int3"); } } *Dest->Ptr++ = *Src->Ptr++; } *Dest->Ptr = '\0'; } int CopyStringNoFormat(char *Dest, char *String) { int Length = 0; while(*String) { *Dest++ = *String++; ++Length; } *Dest = '\0'; return Length; } // TODO(matt): Maybe do a version of this that takes a string as a Terminator int CopyStringNoFormatT(char *Dest, char *String, char Terminator) { int Length = 0; while(*String != Terminator) { *Dest++ = *String++; ++Length; } *Dest = '\0'; return Length; } __attribute__ ((format (printf, 2, 3))) void CopyStringToBuffer(buffer *Dest, char *Format, ...) { va_list Args; va_start(Args, Format); int Length = vsnprintf(Dest->Ptr, Dest->Size - (Dest->Ptr - Dest->Location), Format, Args); va_end(Args); // TODO(matt): { if(Length + (Dest->Ptr - Dest->Location) >= Dest->Size) { fprintf(stderr, "CopyStringToBuffer: %s cannot accommodate %d-character string:\n" "\n" "%s\n", Dest->ID, Length, Format); __asm__("int3"); } } Dest->Ptr += Length; } void CopyStringToBufferHTMLSafe(buffer *Dest, char *String) { while(*String) { if(Dest->Ptr - Dest->Location >= Dest->Size) { fprintf(stderr, "CopyStringToBufferHTMLSafe: %s cannot accommodate %d-character string\n", Dest->ID, StringLength(String)); __asm__("int3"); } switch(*String) { case '<': CopyStringToBuffer(Dest, "<"); String++; break; case '>': CopyStringToBuffer(Dest, ">"); String++; break; case '&': CopyStringToBuffer(Dest, "&"); String++; break; case '\"': CopyStringToBuffer(Dest, """); String++; break; case '\'': CopyStringToBuffer(Dest, "'"); String++; break; default: *Dest->Ptr++ = *String++; break; } } } int StringsDiffer(char *A, char *B) // NOTE(matt): Two null-terminated strings { while(*A && *B && *A == *B) { ++A, ++B; } return *A - *B; } bool StringsDifferT(char *A, // NOTE(matt): Null-terminated string char *B, // NOTE(matt): Not null-terminated string (e.g. one mid-buffer) char Terminator // NOTE(matt): Caller definable terminator. Pass 0 to only match on the extent of A ) { int ALength = StringLength(A); int i = 0; while(i < ALength && A[i] && A[i] == B[i]) { ++i; } if((!Terminator && !A[i] && ALength == i) || (!A[i] && ALength == i && (B[i] == Terminator))) { return FALSE; } else { return TRUE; } } int MakeDir(char *Path) { // TODO(matt): Correctly check for permissions int i = StringLength(Path); int Ancestors = 0; while(mkdir(Path, 00755) == -1) { if(errno == EACCES) { return RC_ERROR_DIRECTORY; } while(Path[i] != '/' && i > 0) { --i; } ++Ancestors; Path[i] = '\0'; if(i == 0) { return RC_ERROR_DIRECTORY; } } while(Ancestors > 0) { while(Path[i] != '\0') { ++i; } Path[i] = '/'; --Ancestors; if((mkdir(Path, 00755)) == -1) { return RC_ERROR_DIRECTORY; } } return RC_SUCCESS; } void LogUsage(buffer Buffer, char *CacheDir) { #if DEBUG char LogPath[255]; CopyString(LogPath, "%s/%s", CacheDir, "buffers.log"); FILE *LogFile; if(!(LogFile = fopen(LogPath, "a+"))) { MakeDir(CacheDir); if(!(LogFile = fopen(LogPath, "a+"))) { perror("LogUsage"); return; } } fprintf(LogFile, "%s,%ld,%d\n", Buffer.ID, Buffer.Ptr - Buffer.Location, Buffer.Size); fclose(LogFile); #endif } __attribute__ ((format (printf, 3, 4))) void LogError(config Config, int LogLevel, char *Format, ...) { if(Config.LogLevel >= LogLevel) { char LogPath[255]; CopyString(LogPath, "%s/%s", Config.CacheDir, "errors.log"); FILE *LogFile; if(!(LogFile = fopen(LogPath, "a+"))) { MakeDir(Config.CacheDir); if(!(LogFile = fopen(LogPath, "a+"))) { perror("LogUsage"); return; } } va_list Args; va_start(Args, Format); vfprintf(LogFile, Format, Args); va_end(Args); // TODO(matt): Include the LogLevel "string" and the current wall time fprintf(LogFile, "\n"); fclose(LogFile); } } void FreeBuffer(buffer *Buffer) { free(Buffer->Location); } #if 0 #define ClaimBuffer(MemoryArena, Buffer, ID, Size) if(__ClaimBuffer(MemoryArena, Buffer, ID, Size))\ {\ fprintf(stderr, "%s:%d: MemoryArena cannot contain %s of size %d\n", __FILE__, __LINE__, ID, Size);\ hmml_free(&HMML);\ FreeBuffer(MemoryArena);\ return 1;\ }; #endif int ClaimBuffer(arena *MemoryArena, buffer *Buffer, char *ID, int Size) { if(MemoryArena->Ptr - MemoryArena->Location + Size > MemoryArena->Size) { return RC_ARENA_FULL; } Buffer->Location = (char *)MemoryArena->Ptr; Buffer->Size = Size; Buffer->ID = ID; MemoryArena->Ptr += Buffer->Size; *Buffer->Location = '\0'; Buffer->Ptr = Buffer->Location; #if DEBUG printf(" Claimed: %s: %d\n" " Total ClaimedMemory: %ld\n\n", Buffer->ID, Buffer->Size, MemoryArena->Ptr - MemoryArena->Location); #endif return RC_SUCCESS; } #define DeclaimBuffer(MemoryArena, Buffer) __DeclaimBuffer(MemoryArena, Buffer, Config) void __DeclaimBuffer(arena *MemoryArena, buffer *Buffer, config Config) { *Buffer->Location = '\0'; MemoryArena->Ptr -= Buffer->Size; float PercentageUsed = (float)(Buffer->Ptr - Buffer->Location) / Buffer->Size * 100; #if DEBUG printf("Declaimed: %s\n" " Used: %ld / %d (%.2f%%)\n" "\n" " Total ClaimedMemory: %ld\n\n", Buffer->ID, Buffer->Ptr - Buffer->Location, Buffer->Size, PercentageUsed, MemoryArena->Ptr - MemoryArena->Location); #endif LogUsage(*Buffer, Config.CacheDir); if(PercentageUsed >= 80.0f) { // TODO(matt): Implement either dynamically growing buffers, or phoning home to matt@handmadedev.org LogError(Config, LOG_ERROR, "%s used %.2f%% of its allotted memory\n", Buffer->ID, PercentageUsed); fprintf(stderr, "Warning: %s used %.2f%% of its allotted memory\n", Buffer->ID, PercentageUsed); } } int ClaimTemplate(arena *MemoryArena, template **Template, char *ID) { if(MemoryArena->Ptr - MemoryArena->Location + sizeof(template) > MemoryArena->Size) { return RC_ARENA_FULL; } *Template = (template *)MemoryArena->Ptr; CopyString((*Template)->Filename, ID); MemoryArena->Ptr += sizeof(template); #if DEBUG printf(" Claimed: %s metadata: %ld\n" " Total ClaimedMemory: %ld\n\n", (*Template)->Filename, sizeof(template), MemoryArena->Ptr - MemoryArena->Location); #endif return RC_SUCCESS; } int DeclaimTemplate(arena *MemoryArena, template **Template) { MemoryArena->Ptr -= sizeof(template); #if DEBUG printf("Declaimed: %s metadata\n" " Total ClaimedMemory: %ld\n\n", (*Template)->Filename, MemoryArena->Ptr - MemoryArena->Location); #endif return RC_SUCCESS; } int TimecodeToSeconds(char *Timecode) { int HMS[3] = { 0, 0, 0 }; // 0 == Seconds; 1 == Minutes; 2 == Hours int Colons = 0; while(*Timecode) { //if((*Timecode < '0' || *Timecode > '9') && *Timecode != ':') { return FALSE; } if(*Timecode == ':') { ++Colons; //if(Colons > 2) { return FALSE; } for(int i = 0; i < Colons; ++i) { HMS[Colons - i] = HMS[Colons - (i + 1)]; } HMS[0] = 0; } else { HMS[0] = HMS[0] * 10 + *Timecode - '0'; } ++Timecode; } //if(HMS[0] > 59 || HMS[1] > 59 || Timecode[-1] == ':') { return FALSE; } return HMS[2] * 60 * 60 + HMS[1] * 60 + HMS[0]; } typedef struct { unsigned int Hue:16; unsigned int Saturation:8; unsigned int Lightness:8; } hsl_colour; hsl_colour CharToColour(char Char) { hsl_colour Colour; if(Char >= 'a' && Char <= 'z') { Colour.Hue = (((float)Char - 'a') / ('z' - 'a') * 360); Colour.Saturation = (((float)Char - 'a') / ('z' - 'a') * 26 + 74); } else if(Char >= 'A' && Char <= 'Z') { Colour.Hue = (((float)Char - 'A') / ('Z' - 'A') * 360); Colour.Saturation = (((float)Char - 'A') / ('Z' - 'A') * 26 + 74); } else if(Char >= '0' && Char <= '9') { Colour.Hue = (((float)Char - '0') / ('9' - '0') * 360); Colour.Saturation = (((float)Char - '0') / ('9' - '0') * 26 + 74); } else { Colour.Hue = 180; Colour.Saturation = 50; } return Colour; } hsl_colour * StringToColourHash(hsl_colour *Colour, char *String) { Colour->Hue = 0; Colour->Saturation = 0; Colour->Lightness = 26; int i; for(i = 0; String[i]; ++i) { Colour->Hue += CharToColour(String[i]).Hue; Colour->Saturation += CharToColour(String[i]).Saturation; } Colour->Hue = Colour->Hue % 360; Colour->Saturation = Colour->Saturation % 26 + 74; return(Colour); } char * SanitisePunctuation(char *String) { char *Ptr = String; while(*Ptr) { if(*Ptr == ' ') { *Ptr = '_'; } if((*Ptr < '0' || *Ptr > '9') && (*Ptr < 'a' || *Ptr > 'z') && (*Ptr < 'A' || *Ptr > 'Z')) { *Ptr = '-'; } ++Ptr; } return String; } enum { CreditsError_NoHost, CreditsError_NoAnnotator, CreditsError_NoCredentials }; int SearchCredentials(config Config, buffer *CreditsMenu, bool *HasCreditsMenu, char *ImagesDir, char *Person, char* Role) { bool Found = FALSE; for(int CredentialIndex = 0; CredentialIndex < ArrayCount(Credentials); ++CredentialIndex) { if(!StringsDiffer(Person, Credentials[CredentialIndex].Username)) { Found = TRUE; if(*HasCreditsMenu == FALSE) { CopyStringToBuffer(CreditsMenu, "
\n" "
\n" " Credits\n" "
\n"); *HasCreditsMenu = TRUE; } CopyStringToBuffer(CreditsMenu, " \n"); if(*Credentials[CredentialIndex].HomepageURL) { CopyStringToBuffer(CreditsMenu, " \n" "
%s
\n" "
%s
\n" "
\n", Credentials[CredentialIndex].HomepageURL, Role, Credentials[CredentialIndex].CreditedName); } else { CopyStringToBuffer(CreditsMenu, "
\n" "
%s
\n" "
%s
\n" "
\n", Role, Credentials[CredentialIndex].CreditedName); } if(*Credentials[CredentialIndex].SupportIcon && *Credentials[CredentialIndex].SupportURL) { if(Config.Edition == EDITION_PROJECT) { CopyStringToBuffer(CreditsMenu, " \n", Credentials[CredentialIndex].SupportURL, ImagesDir, Credentials[CredentialIndex].SupportIcon); } else { CopyStringToBuffer(CreditsMenu, " \n", Credentials[CredentialIndex].SupportURL, ImagesDir, Credentials[CredentialIndex].SupportIcon); } } CopyStringToBuffer(CreditsMenu, "
\n"); } } return Found ? 0 : CreditsError_NoCredentials; } int BuildCredits(config Config, buffer *CreditsMenu, bool *HasCreditsMenu, char *ImagesDir, HMML_VideoMetaData Metadata) // TODO(matt): Make this take the Credentials, once we are parsing them from a config { if(Metadata.member) { if(SearchCredentials(Config, CreditsMenu, HasCreditsMenu, ImagesDir, Metadata.member, "Host")) { printf("No credentials for %s. Please contact matt@handmadedev.org with their:\n" " Full name\n" " Homepage URL (optional)\n" " Financial support info, e.g. Patreon URL (optional)\n", Metadata.member); } } else { if(*HasCreditsMenu == TRUE) { CopyStringToBuffer(CreditsMenu, "
\n" "
\n"); } return CreditsError_NoHost; } if(Metadata.co_host_count > 0) { for(int i = 0; i < Metadata.co_host_count; ++i) { if(SearchCredentials(Config, CreditsMenu, HasCreditsMenu, ImagesDir, Metadata.co_hosts[i], "Co-host")) { printf("No credentials for %s. Please contact matt@handmadedev.org with their:\n" " Full name\n" " Homepage URL (optional)\n" " Financial support info, e.g. Patreon URL (optional)\n", Metadata.co_hosts[i]); } } } if(Metadata.guest_count > 0) { for(int i = 0; i < Metadata.guest_count; ++i) { if(SearchCredentials(Config, CreditsMenu, HasCreditsMenu, ImagesDir, Metadata.guests[i], "Guest")) { printf("No credentials for %s. Please contact matt@handmadedev.org with their:\n" " Full name\n" " Homepage URL (optional)\n" " Financial support info, e.g. Patreon URL (optional)\n", Metadata.guests[i]); } } } if(Metadata.annotator_count > 0) { for(int i = 0; i < Metadata.annotator_count; ++i) { if(SearchCredentials(Config, CreditsMenu, HasCreditsMenu, ImagesDir, Metadata.annotators[i], "Annotator")) { printf("No credentials for %s. Please contact matt@handmadedev.org with their:\n" " Full name\n" " Homepage URL (optional)\n" " Financial support info, e.g. Patreon URL (optional)\n", Metadata.annotators[i]); } } } else { if(*HasCreditsMenu == TRUE) { CopyStringToBuffer(CreditsMenu, " \n" " \n"); } return CreditsError_NoAnnotator; } if(*HasCreditsMenu == TRUE) { CopyStringToBuffer(CreditsMenu, " \n" " \n"); } return 0; } int BuildReference(ref_info *ReferencesArray, int RefIdentifier, int UniqueRefs, HMML_Reference Ref, HMML_Annotation Anno) { #define REF_SITE (1 << 0) #define REF_PAGE (1 << 1) #define REF_URL (1 << 2) #define REF_TITLE (1 << 3) #define REF_ARTICLE (1 << 4) #define REF_AUTHOR (1 << 5) #define REF_EDITOR (1 << 6) #define REF_PUBLISHER (1 << 7) #define REF_ISBN (1 << 8) int Mask = 0; if(Ref.site) { Mask |= REF_SITE; } if(Ref.page) { Mask |= REF_PAGE; } if(Ref.url) { Mask |= REF_URL; } if(Ref.title) { Mask |= REF_TITLE; } if(Ref.article) { Mask |= REF_ARTICLE; } if(Ref.author) { Mask |= REF_AUTHOR; } if(Ref.editor) { Mask |= REF_EDITOR; } if(Ref.publisher) { Mask |= REF_PUBLISHER; } if(Ref.isbn) { Mask |= REF_ISBN; } if((REF_URL | REF_TITLE | REF_AUTHOR | REF_PUBLISHER | REF_ISBN) == Mask) { CopyString(ReferencesArray[UniqueRefs].ID, Ref.isbn); CopyString(ReferencesArray[UniqueRefs].Source, "%s (%s)", Ref.author, Ref.publisher); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref.title); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref.url); } else if((REF_AUTHOR | REF_SITE | REF_PAGE | REF_URL) == Mask) { CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref.url); CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, Ref.site); CopyString(ReferencesArray[UniqueRefs].RefTitle, "%s: \"%s\"", Ref.author, Ref.page); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref.url); } else if((REF_PAGE | REF_URL | REF_TITLE) == Mask) { CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref.url); CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, Ref.title); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref.page); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref.url); } else if((REF_SITE | REF_PAGE | REF_URL) == Mask) { CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref.url); CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, Ref.site); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref.page); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref.url); } else if((REF_SITE | REF_URL | REF_TITLE) == Mask) { CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref.url); CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, Ref.site); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref.title); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref.url); } else if((REF_TITLE | REF_AUTHOR | REF_ISBN) == Mask) { CopyString(ReferencesArray[UniqueRefs].ID, Ref.isbn); CopyString(ReferencesArray[UniqueRefs].Source, Ref.author); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref.title); CopyString(ReferencesArray[UniqueRefs].URL, "http://www.openisbn.com/isbn/%s", Ref.isbn); } else if((REF_URL | REF_ARTICLE | REF_AUTHOR) == Mask) { CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref.url); CopyString(ReferencesArray[UniqueRefs].Source, Ref.author); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref.article); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref.url); } else if((REF_URL | REF_TITLE | REF_AUTHOR) == Mask) { CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref.url); CopyString(ReferencesArray[UniqueRefs].Source, Ref.author); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref.title); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref.url); } else if((REF_URL | REF_TITLE) == Mask) { CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref.url); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref.title); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref.url); } else if((REF_SITE | REF_URL) == Mask) { CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref.url); CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref.site); CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref.url); } else { return 1; } CopyString(ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Timecode, Anno.time); ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Identifier = RefIdentifier; return 0; } void InsertCategory(categories *Topics, categories *Media, char *Marker) { bool IsMedium = FALSE; int CategoryMediumIndex; for(CategoryMediumIndex = 0; CategoryMediumIndex < ArrayCount(CategoryMedium); ++CategoryMediumIndex) { if(!StringsDiffer(CategoryMedium[CategoryMediumIndex].Medium, Marker)) { IsMedium = TRUE; break; } } int CategoryIndex; if(IsMedium) { for(CategoryIndex = 0; CategoryIndex < Media->Count; ++CategoryIndex) { if(!StringsDiffer(CategoryMedium[CategoryMediumIndex].Medium, Media->Category[CategoryIndex].Marker)) { return; } if((StringsDiffer(CategoryMedium[CategoryMediumIndex].WrittenName, Media->Category[CategoryIndex].WrittenText)) < 0) { int CategoryCount; for(CategoryCount = Media->Count; CategoryCount > CategoryIndex; --CategoryCount) { CopyString(Media->Category[CategoryCount].Marker, Media->Category[CategoryCount-1].Marker); CopyString(Media->Category[CategoryCount].WrittenText, Media->Category[CategoryCount-1].WrittenText); } CopyString(Media->Category[CategoryCount].Marker, CategoryMedium[CategoryMediumIndex].Medium); CopyString(Media->Category[CategoryCount].WrittenText, CategoryMedium[CategoryMediumIndex].WrittenName); break; } } if(CategoryIndex == Media->Count) { CopyString(Media->Category[CategoryIndex].Marker, CategoryMedium[CategoryMediumIndex].Medium); CopyString(Media->Category[CategoryIndex].WrittenText, CategoryMedium[CategoryMediumIndex].WrittenName); } ++Media->Count; } else { for(CategoryIndex = 0; CategoryIndex < Topics->Count; ++CategoryIndex) { if(!StringsDiffer(Marker, Topics->Category[CategoryIndex].Marker)) { return; } if((StringsDiffer(Marker, Topics->Category[CategoryIndex].Marker)) < 0) { int CategoryCount; for(CategoryCount = Topics->Count; CategoryCount > CategoryIndex; --CategoryCount) { CopyString(Topics->Category[CategoryCount].Marker, Topics->Category[CategoryCount-1].Marker); } CopyString(Topics->Category[CategoryCount].Marker, Marker); break; } } if(CategoryIndex == Topics->Count) { CopyString(Topics->Category[CategoryIndex].Marker, Marker); } ++Topics->Count; } return; } void BuildCategories(buffer *AnnotationClass, buffer *TopicDots, categories LocalTopics, categories LocalMedia, int *MarkerIndex) { if(LocalTopics.Count > 0) { CopyStringToBuffer(TopicDots, ""); for(int i = 0; i < LocalTopics.Count; ++i) { CopyStringToBuffer(TopicDots, "
", SanitisePunctuation(LocalTopics.Category[i].Marker), SanitisePunctuation(LocalTopics.Category[i].Marker)); CopyStringToBuffer(AnnotationClass, " cat_%s", SanitisePunctuation(LocalTopics.Category[i].Marker)); } CopyStringToBuffer(TopicDots, "
"); } for(int i = 0; i < LocalMedia.Count; ++i) { CopyStringToBuffer(AnnotationClass, " %s", SanitisePunctuation(LocalMedia.Category[i].Marker)); } CopyStringToBuffer(AnnotationClass, "\""); } int StringToInt(char *String) { int Result = 0; while(*String) { Result = Result * 10 + (*String - '0'); ++String; } return Result; } size_t CurlIntoBuffer(char *InPtr, size_t CharLength, size_t Chars, char **OutputPtr) { int Length = CharLength * Chars; int i; for(i = 0; InPtr[i] && i < Length; ++i) { *((*OutputPtr)++) = InPtr[i]; } **OutputPtr = '\0'; return Length; }; void CurlQuotes(buffer *QuoteStaging, char *QuotesURL) { CURL *curl = curl_easy_init(); if(curl) { CURLcode res; curl_easy_setopt(curl, CURLOPT_WRITEDATA, &QuoteStaging->Ptr); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlIntoBuffer); curl_easy_setopt(curl, CURLOPT_URL, QuotesURL); if((res = curl_easy_perform(curl))) { fprintf(stderr, "%s", curl_easy_strerror(res)); } curl_easy_cleanup(curl); } } int SearchQuotes(buffer QuoteStaging, int CacheSize, quote_info *Info, int ID) { QuoteStaging.Ptr = QuoteStaging.Location; while(QuoteStaging.Ptr - QuoteStaging.Location < CacheSize) { char InID[4] = { 0 }; char InTime[16] = { 0 }; char *OutPtr = InID; QuoteStaging.Ptr += CopyStringNoFormatT(OutPtr, QuoteStaging.Ptr, ','); if(StringToInt(InID) == ID) { QuoteStaging.Ptr += 1; OutPtr = InTime; QuoteStaging.Ptr += CopyStringNoFormatT(OutPtr, QuoteStaging.Ptr, ','); long int Time = StringToInt(InTime); char DayString[3]; strftime(DayString, 3, "%d", gmtime(&Time)); int Day = StringToInt(DayString); char DaySuffix[3]; if(DayString[1] == '1' && Day != 11) { CopyString(DaySuffix, "st"); } else if(DayString[1] == '2' && Day != 12) { CopyString(DaySuffix, "nd"); } else if(DayString[1] == '3' && Day != 13) { CopyString(DaySuffix, "rd"); } else { CopyString(DaySuffix, "th"); } char MonthYear[32]; strftime(MonthYear, 32, "%B, %Y", gmtime(&Time)); CopyString(Info->Date, "%d%s %s", Day, DaySuffix, MonthYear); QuoteStaging.Ptr += 1; OutPtr = Info->Text; QuoteStaging.Ptr += CopyStringNoFormatT(OutPtr, QuoteStaging.Ptr, '\n'); FreeBuffer(&QuoteStaging); return RC_FOUND; } else { while(*QuoteStaging.Ptr != '\n') { ++QuoteStaging.Ptr; } ++QuoteStaging.Ptr; } } return RC_UNFOUND; } int BuildQuote(quote_info *Info, char *Speaker, int ID, char *CacheDir) { // TODO(matt): Rebuild cache option char QuoteCacheDir[255]; CopyString(QuoteCacheDir, "%s/quotes", CacheDir); char QuoteCachePath[255]; CopyString(QuoteCachePath, "%s/%s", QuoteCacheDir, Speaker); FILE *QuoteCache; char QuotesURL[256]; // TODO(matt): Make the URL configurable CopyString(QuotesURL, "https://dev.abaines.me.uk/quotes/%s.raw", Speaker); bool CacheAvailable = FALSE; if(!(QuoteCache = fopen(QuoteCachePath, "a+"))) { if(MakeDir(QuoteCacheDir) == RC_SUCCESS) { CacheAvailable = TRUE; }; if(!(QuoteCache = fopen(QuoteCachePath, "a+"))) { fprintf(stderr, "Unable to open quote cache %s: %s\n", QuoteCachePath, strerror(errno)); } else { CacheAvailable = TRUE; } } else { CacheAvailable = TRUE; } buffer QuoteStaging; QuoteStaging.ID = "QuoteStaging"; QuoteStaging.Size = Kilobytes(256); if(!(QuoteStaging.Location = malloc(QuoteStaging.Size))) { fclose(QuoteCache); return RC_ERROR_MEMORY; } QuoteStaging.Ptr = QuoteStaging.Location; if(CacheAvailable) { fseek(QuoteCache, 0, SEEK_END); int FileSize = ftell(QuoteCache); fseek(QuoteCache, 0, SEEK_SET); fread(QuoteStaging.Location, FileSize, 1, QuoteCache); fclose(QuoteCache); if(SearchQuotes(QuoteStaging, FileSize, Info, ID) == RC_UNFOUND) { CurlQuotes(&QuoteStaging, QuotesURL); if(!(QuoteCache = fopen(QuoteCachePath, "w"))) { perror(QuoteCachePath); } fwrite(QuoteStaging.Location, QuoteStaging.Ptr - QuoteStaging.Location, 1, QuoteCache); fclose(QuoteCache); int CacheSize = QuoteStaging.Ptr - QuoteStaging.Location; QuoteStaging.Ptr = QuoteStaging.Location; if(SearchQuotes(QuoteStaging, CacheSize, Info, ID) == 1) { FreeBuffer(&QuoteStaging); return 1; } } } else { CurlQuotes(&QuoteStaging, QuotesURL); int CacheSize = QuoteStaging.Ptr - QuoteStaging.Location; QuoteStaging.Ptr = QuoteStaging.Location; if(SearchQuotes(QuoteStaging, CacheSize, Info, ID) == 1) { FreeBuffer(&QuoteStaging); return 1; } } return 0; } int GenerateTopicColours(char *Topic, char *TopicsDir) { for(int i = 0; i < ArrayCount(CategoryMedium); ++i) { if(!StringsDiffer(Topic, CategoryMedium[i].Medium)) { return RC_NOOP; } } FILE *TopicsFile; char *TopicsBuffer; // TODO(matt): Consider (optionally) pulling this path from the config char TopicsPath[255]; CopyString(TopicsPath, "%s/cinera_topics.css", TopicsDir); if((TopicsFile = fopen(TopicsPath, "a+"))) { fseek(TopicsFile, 0, SEEK_END); int TopicsLength = ftell(TopicsFile); fseek(TopicsFile, 0, SEEK_SET); if(!(TopicsBuffer = malloc(TopicsLength))) { return RC_ERROR_MEMORY; } fread(TopicsBuffer, TopicsLength, 1, TopicsFile); char *TopicsPtr = TopicsBuffer; while(TopicsPtr - TopicsBuffer < TopicsLength) { TopicsPtr += StringLength(".category."); if(!StringsDifferT(SanitisePunctuation(Topic), TopicsPtr, ' ')) { free(TopicsBuffer); fclose(TopicsFile); return RC_NOOP; } while(TopicsPtr - TopicsBuffer < TopicsLength && *TopicsPtr != '\n') { ++TopicsPtr; } ++TopicsPtr; } hsl_colour Colour; StringToColourHash(&Colour, Topic); fprintf(TopicsFile, ".category.%s { border: 1px solid hsl(%d, %d%%, %d%%); background: hsl(%d, %d%%, %d%%); }\n", SanitisePunctuation(Topic), Colour.Hue, Colour.Saturation, Colour.Lightness, Colour.Hue, Colour.Saturation, Colour.Lightness); fclose(TopicsFile); free(TopicsBuffer); return RC_SUCCESS; } else { return RC_ERROR_FILE; } } void PrintUsage(char *BinaryLocation, config DefaultConfig) { fprintf(stderr, "Usage: %s [option(s)] filename(s)\n" "\n" "Options:\n" " -b \n" " Override default base output directory (\"%s\")\n" " -c \n" " Override default CSS directory (\"%s\")\n" " -f\n" " Force integration with an incomplete template\n" " -i \n" " Override default images directory (\"%s\")\n" " -j \n" " Override default JS directory (\"%s\")\n" " -l \n" " Override default log level (%d), where n is from 0 (terse) to 7 (verbose)\n" " -m \n" " Override default default medium (\"%s\")\n" " -o \n" " Override default output location (\"%s\")\n" " -p \n" " Override default project directory (\"%s\")\n" " -t \n" " Override default player template location (\"%s\")\n" " and automatically enable integration\n" " -x \n" " Override default index template location (\"%s\")\n" " and automatically enable integration\n" //" -c config location\n" " -h\n" " display this help\n" "\n" "Environment Variables:\n" " CINERA_MODE\n" " =INTEGRATE\n" " Enable integration\n" "\n" "Template:\n" " A complete template shall contain exactly one each of the following tags:\n" " \n" " \n" " \n" " (must come after )\n" " Other available tags include:\n" " \n" "\n" "HMML Specification:\n" " https://git.handmade.network/Annotation-Pushers/Annotation-System/wikis/hmmlspec\n", BinaryLocation, DefaultConfig.BaseDir, DefaultConfig.CSSDir, DefaultConfig.ImagesDir, DefaultConfig.JSDir, DefaultConfig.LogLevel, DefaultConfig.DefaultMedium, DefaultConfig.OutLocation, DefaultConfig.ProjectDir, DefaultConfig.TemplatePlayerLocation, DefaultConfig.TemplateIndexLocation); } void DepartComment(buffer *Template) { while(Template->Ptr - Template->Location < Template->Size) { if(!StringsDifferT("-->", Template->Ptr, 0)) { Template->Ptr += StringLength("-->"); break; } ++Template->Ptr; } } enum { PAGE_PLAYER = 1 << 0, PAGE_INDEX = 1 << 1 } pages; int ValidateTemplate(buffer *Errors, template *TemplateMetadata, config Config, int PageType) { Errors->Ptr = Errors->Location; bool HaveErrors = FALSE; FILE *TemplateFile; if(!(TemplateFile = fopen(TemplateMetadata->Filename, "r"))) { LogError(Config, LOG_ERROR, "Unable to open template %s: %s", TemplateMetadata->Filename, strerror(errno)); fprintf(stderr, "Unable to open template %s: %s\n", TemplateMetadata->Filename, strerror(errno)); return RC_ERROR_FILE; } buffer Template; Template.ID = TemplateMetadata->Filename; fseek(TemplateFile, 0, SEEK_END); Template.Size = ftell(TemplateFile); fseek(TemplateFile, 0, SEEK_SET); if(!(Template.Location = malloc(Template.Size))) { LogError(Config, LOG_ERROR, "ValidateTemplate(): %s", strerror(errno)); return RC_ERROR_MEMORY; } Template.Ptr = Template.Location; fread(Template.Location, Template.Size, 1, TemplateFile); fclose(TemplateFile); bool FoundIncludes = FALSE; bool FoundMenus = FALSE; bool FoundPlayer = FALSE; bool FoundScript = FALSE; bool FoundIndex = FALSE; char *Previous = Template.Location; while(Template.Ptr - Template.Location < Template.Size) { Here: if(*Template.Ptr == '!' && (Template.Ptr > Template.Location && !StringsDifferT("", Template.Ptr, 0)) { for(int i = 0; i < ArrayCount(Tags); ++i) { if(!(StringsDifferT(Tags[i].Tag, Template.Ptr, 0))) { // TODO(matt): Pack up this data for BuffersToHTML() to use /* * Potential ways to compress these cases * * bool Found[TAG_COUNT] * -Asaf * * int* flags[] = { [TAG_INCLUDES] = &FoundIncludes, [TAG_MENUS] = &FoundMenus } flags[Tags[i].Code] = true; * -insofaras * */ //printf("Switching on the tags\n"); switch(Tags[i].Code) { case TAG_INDEX: TemplateMetadata->Tag[TemplateMetadata->TagCount].Offset = CommentStart - Previous; TemplateMetadata->Tag[TemplateMetadata->TagCount].TagCode = TAG_INDEX; TemplateMetadata->TagCount++; DepartComment(&Template); Previous = Template.Ptr; FoundIndex = TRUE; goto Here; case TAG_INCLUDES: if(!Config.ForceIntegration && FoundIncludes == TRUE) { CopyStringToBuffer(Errors, "Template contains more than one tag\n", Tags[i].Tag); HaveErrors = TRUE; } TemplateMetadata->Tag[TemplateMetadata->TagCount].Offset = CommentStart - Previous; TemplateMetadata->Tag[TemplateMetadata->TagCount].TagCode = TAG_INCLUDES; TemplateMetadata->TagCount++; DepartComment(&Template); Previous = Template.Ptr; FoundIncludes = TRUE; goto Here; case TAG_MENUS: if(!Config.ForceIntegration && FoundMenus == TRUE) { CopyStringToBuffer(Errors, "Template contains more than one tag\n", Tags[i].Tag); HaveErrors = TRUE; } TemplateMetadata->Tag[TemplateMetadata->TagCount].Offset = CommentStart - Previous; TemplateMetadata->Tag[TemplateMetadata->TagCount].TagCode = TAG_MENUS; TemplateMetadata->TagCount++; DepartComment(&Template); Previous = Template.Ptr; FoundMenus = TRUE; goto Here; case TAG_PLAYER: if(!Config.ForceIntegration && FoundPlayer == TRUE) { CopyStringToBuffer(Errors, "Template contains more than one tag\n", Tags[i].Tag); HaveErrors = TRUE; } TemplateMetadata->Tag[TemplateMetadata->TagCount].Offset = CommentStart - Previous; TemplateMetadata->Tag[TemplateMetadata->TagCount].TagCode = TAG_PLAYER; TemplateMetadata->TagCount++; DepartComment(&Template); Previous = Template.Ptr; FoundPlayer = TRUE; goto Here; case TAG_SCRIPT: if(!Config.ForceIntegration && FoundPlayer == FALSE) { CopyStringToBuffer(Errors, " must come after \n", Tags[i].Tag); HaveErrors = TRUE; } if(!Config.ForceIntegration && FoundScript == TRUE) { CopyStringToBuffer(Errors, "Template contains more than one tag\n", Tags[i].Tag); HaveErrors = TRUE; } TemplateMetadata->Tag[TemplateMetadata->TagCount].Offset = CommentStart - Previous; TemplateMetadata->Tag[TemplateMetadata->TagCount].TagCode = TAG_SCRIPT; TemplateMetadata->TagCount++; DepartComment(&Template); Previous = Template.Ptr; FoundScript = TRUE; goto Here; case TAG_TITLE: TemplateMetadata->Tag[TemplateMetadata->TagCount].Offset = CommentStart - Previous; TemplateMetadata->Tag[TemplateMetadata->TagCount].TagCode = TAG_TITLE; TemplateMetadata->TagCount++; DepartComment(&Template); Previous = Template.Ptr; goto Here; }; } } ++Template.Ptr; } } else { ++Template.Ptr; } } FreeBuffer(&Template); if(FoundIndex) { TemplateMetadata->Validity |= PAGE_INDEX; } if(!HaveErrors && FoundIncludes && FoundMenus && FoundPlayer && FoundScript) { TemplateMetadata->Validity |= PAGE_PLAYER; } if(!Config.ForceIntegration) { if(PageType == PAGE_INDEX && !(TemplateMetadata->Validity & PAGE_INDEX)) { CopyStringToBuffer(Errors, "Index template %s must include one tag\n", TemplateMetadata->Filename); fprintf(stderr, "%s", Errors->Location); return RC_INVALID_TEMPLATE; } else if(PageType == PAGE_PLAYER && !(TemplateMetadata->Validity & PAGE_PLAYER)) { if(!FoundIncludes){ CopyStringToBuffer(Errors, "Player template %s must include one tag\n", TemplateMetadata->Filename); }; if(!FoundMenus){ CopyStringToBuffer(Errors, "Player template %s must include one tag\n", TemplateMetadata->Filename); }; if(!FoundPlayer){ CopyStringToBuffer(Errors, "Player template %s must include one tag\n", TemplateMetadata->Filename); }; if(!FoundScript){ CopyStringToBuffer(Errors, "Player template %s must include one tag\n", TemplateMetadata->Filename); }; fprintf(stderr, "%s", Errors->Location); return RC_INVALID_TEMPLATE; } } return RC_SUCCESS; } void RewindBuffer(buffer *Buffer) { #if DEBUG float PercentageUsed = (float)(Buffer->Ptr - Buffer->Location) / Buffer->Size * 100; printf("Rewinding %s\n" " Used: %ld / %d (%.2f%%)\n\n", Buffer->ID, Buffer->Ptr - Buffer->Location, Buffer->Size, PercentageUsed); #endif Buffer->Ptr = Buffer->Location; } int HMMLToBuffers(arena *MemoryArena, buffers *CollationBuffers, config Config, char *Filename) { RewindBuffer(&CollationBuffers->IncludesPlayer); RewindBuffer(&CollationBuffers->Menus); RewindBuffer(&CollationBuffers->Player); RewindBuffer(&CollationBuffers->Script); RewindBuffer(&CollationBuffers->IncludesIndex); char Filepath[255]; if(Config.Edition == EDITION_PROJECT) { CopyString(Filepath, "%s/%s", Config.ProjectDir, Filename); } else { CopyString(Filepath, "%s", Filename); } FILE *InFile; if(!(InFile = fopen(Filepath, "r"))) { LogError(Config, LOG_ERROR, "Unable to open (annotations file) %s: %s", Filename, strerror(errno)); fprintf(stderr, "Unable to open (annotations file) %s: %s\n", Filename, strerror(errno)); return RC_ERROR_FILE; } HMML_Output HMML = hmml_parse_file(InFile); fclose(InFile); if(HMML.well_formed) { CopyString(CollationBuffers->Title, HMML.metadata.title); CopyString(CollationBuffers->Project, HMML.metadata.project); #if DEBUG printf( "================================================================================\n" "%s\n" "================================================================================\n", Filename); #endif // NOTE(matt): Tree structure of "global" buffer dependencies // Master // IncludesPlayer // Menus // QuoteMenu // ReferenceMenu // FilterMenu // FilterTopics // FilterMedia // CreditsMenu // Player // Script // FilterState buffer QuoteMenu; buffer ReferenceMenu; buffer FilterMenu; buffer FilterTopics; buffer FilterMedia; buffer CreditsMenu; buffer Annotation; buffer AnnotationHeader; buffer AnnotationClass; buffer AnnotationData; buffer Text; buffer TopicDots; buffer FilterState; if(ClaimBuffer(MemoryArena, &QuoteMenu, "QuoteMenu", Kilobytes(16)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; }; if(ClaimBuffer(MemoryArena, &ReferenceMenu, "ReferenceMenu", Kilobytes(16)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; }; if(ClaimBuffer(MemoryArena, &FilterMenu, "FilterMenu", Kilobytes(16)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; }; if(ClaimBuffer(MemoryArena, &FilterTopics, "FilterTopics", Kilobytes(8)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; }; if(ClaimBuffer(MemoryArena, &FilterMedia, "FilterMedia", Kilobytes(8)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; }; if(ClaimBuffer(MemoryArena, &CreditsMenu, "CreditsMenu", Kilobytes(8)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; }; if(ClaimBuffer(MemoryArena, &FilterState, "FilterState", Kilobytes(4)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; }; ref_info ReferencesArray[200] = { 0 }; categories Topics = { 0 }; categories Media = { 0 }; bool HasQuoteMenu = FALSE; bool HasReferenceMenu = FALSE; bool HasFilterMenu = FALSE; bool HasCreditsMenu = FALSE; int QuoteIdentifier = 0x3b1; int RefIdentifier = 1; int UniqueRefs = 0; CopyStringToBuffer(&CollationBuffers->Menus, "
\n" " ", HMML.metadata.project); CopyStringToBufferHTMLSafe(&CollationBuffers->Menus, HMML.metadata.title); CopyStringToBuffer(&CollationBuffers->Menus, "\n" " ⚠ Click here to regain focus ⚠\n"); CopyStringToBuffer(&CollationBuffers->Player, "
\n" "
\n" "
\n", HMML.metadata.id, HMML.metadata.project); int CreditsErrorCode = BuildCredits(Config, &CreditsMenu, &HasCreditsMenu, Config.ImagesDir, HMML.metadata); if(CreditsErrorCode) { switch(CreditsErrorCode) { case CreditsError_NoHost: fprintf(stderr, "%s: Missing \"member\" in the [video] node. Skipping...\n", Filename); goto Cleanup; break; case CreditsError_NoAnnotator: fprintf(stderr, "%s: Missing \"annotator\" in the [video] node. Skipping...\n", Filename); goto Cleanup; break; default: break; } } #if DEBUG printf("\n\n --- Entering Annotations Loop ---\n\n\n\n"); #endif for(int AnnotationIndex = 0; AnnotationIndex < HMML.annotation_count; ++AnnotationIndex) { #if DEBUG printf("%d\n", AnnotationIndex); #endif HMML_Annotation *Anno = HMML.annotations + AnnotationIndex; categories LocalTopics = { 0 }; categories LocalMedia = { 0 }; bool HasQuote = FALSE; bool HasReference = FALSE; quote_info QuoteInfo = { 0 }; // NOTE(matt): Tree structure of "annotation local" buffer dependencies // Annotation // AnnotationHeader // AnnotationClass // AnnotationData // Text // TopicDots if(ClaimBuffer(MemoryArena, &Annotation, "Annotation", Kilobytes(8)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; }; if(ClaimBuffer(MemoryArena, &AnnotationHeader, "AnnotationHeader", 512) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; }; if(ClaimBuffer(MemoryArena, &AnnotationClass, "AnnotationClass", 256) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; }; if(ClaimBuffer(MemoryArena, &AnnotationData, "AnnotationData", 512) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; }; if(ClaimBuffer(MemoryArena, &Text, "Text", Kilobytes(4)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; }; if(ClaimBuffer(MemoryArena, &TopicDots, "TopicDots", 512) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; }; CopyStringToBuffer(&AnnotationHeader, "
time)); CopyStringToBuffer(&AnnotationClass, " class=\"marker"); if(Anno->author) { if(!HasFilterMenu) { HasFilterMenu = TRUE; } InsertCategory(&Topics, &Media, "authored"); CopyStringToBuffer(&AnnotationClass, " authored"); hsl_colour AuthorColour; StringToColourHash(&AuthorColour, Anno->author); if(Config.Edition == EDITION_NETWORK) { fprintf(stderr, "%s:%d - TODO(matt): Implement author hoverbox\n", __FILE__, __LINE__); // NOTE(matt): We should get instructions on how to get this info in the config CopyStringToBuffer(&Text, "%s ", Anno->author, AuthorColour.Hue, AuthorColour.Saturation, AuthorColour.Lightness, AuthorColour.Hue, AuthorColour.Saturation, Anno->author); } else { CopyStringToBuffer(&Text, "%s ", AuthorColour.Hue, AuthorColour.Saturation, AuthorColour.Lightness, AuthorColour.Hue, AuthorColour.Saturation, Anno->author); } } char *InPtr = Anno->text; int MarkerIndex = 0, RefIndex = 0; while(*InPtr || RefIndex < Anno->reference_count) { if(MarkerIndex < Anno->marker_count && InPtr - Anno->text == Anno->markers[MarkerIndex].offset) { char *Readable = Anno->markers[MarkerIndex].parameter ? Anno->markers[MarkerIndex].parameter : Anno->markers[MarkerIndex].marker; if(Anno->markers[MarkerIndex].type == HMML_MEMBER) { hsl_colour MemberColour; StringToColourHash(&MemberColour, Anno->markers[MarkerIndex].marker); if(Config.Edition == EDITION_NETWORK) { fprintf(stderr, "%s:%d - TODO(matt): Implement member hoverbox\n", __FILE__, __LINE__); // NOTE(matt): We should get instructions on how to get this info in the config CopyStringToBuffer(&Text, "%.*s", Anno->markers[MarkerIndex].marker, MemberColour.Hue, MemberColour.Saturation, MemberColour.Lightness, MemberColour.Hue, MemberColour.Saturation, StringLength(Readable), InPtr); } else { CopyStringToBuffer(&Text, "%.*s", MemberColour.Hue, MemberColour.Saturation, MemberColour.Lightness, MemberColour.Hue, MemberColour.Saturation, StringLength(Readable), InPtr); } InPtr += StringLength(Readable); ++MarkerIndex; } else if(Anno->markers[MarkerIndex].type == HMML_PROJECT) { hsl_colour ProjectColour; StringToColourHash(&ProjectColour, Anno->markers[MarkerIndex].marker); if(Config.Edition == EDITION_NETWORK) { fprintf(stderr, "%s:%d - TODO(matt): Implement project hoverbox\n", __FILE__, __LINE__); // NOTE(matt): We should get instructions on how to get this info in the config CopyStringToBuffer(&Text, "%s", Anno->markers[MarkerIndex].marker, ProjectColour.Hue, ProjectColour.Saturation, ProjectColour.Lightness, ProjectColour.Hue, ProjectColour.Saturation, Readable); } else { CopyStringToBuffer(&Text, "%s", ProjectColour.Hue, ProjectColour.Saturation, ProjectColour.Lightness, ProjectColour.Hue, ProjectColour.Saturation, Readable); } InPtr += StringLength(Readable); ++MarkerIndex; } else if(Anno->markers[MarkerIndex].type == HMML_CATEGORY) { switch(GenerateTopicColours(Anno->markers[MarkerIndex].marker, Config.CSSDir)) { case RC_SUCCESS: case RC_NOOP: break; case RC_ERROR_FILE: case RC_ERROR_MEMORY: return RC_ERROR_FATAL; }; if(!HasFilterMenu) { HasFilterMenu = TRUE; } InsertCategory(&Topics, &Media, Anno->markers[MarkerIndex].marker); // Global InsertCategory(&LocalTopics, &LocalMedia, Anno->markers[MarkerIndex].marker); // Local } } while(RefIndex < Anno->reference_count && InPtr - Anno->text == Anno->references[RefIndex].offset) { HMML_Reference *CurrentRef = Anno->references + RefIndex; if(!HasReferenceMenu) { CopyStringToBuffer(&ReferenceMenu, "
\n" " References ▼\n" "
\n" "
\n"); if(BuildReference(ReferencesArray, RefIdentifier, UniqueRefs, *CurrentRef, *Anno) == 1) { LogError(Config, LOG_ERROR, "Reference combination processing failed: %s:%d", Filename, Anno->line); fprintf(stderr, "%s:%d: Cannot process new combination of reference info\n" "\n" "Either tweak your annotation, or contact matt@handmadedev.org\n" "mentioning the ref node you want to write and how you want it to\n" "appear in the references menu\n", Filename, Anno->line); hmml_free(&HMML); return RC_INVALID_REFERENCE; } ++ReferencesArray[RefIdentifier - 1].IdentifierCount; ++UniqueRefs; HasReferenceMenu = TRUE; } else { for(int i = 0; i < UniqueRefs; ++i) { if(ReferencesArray[i].IdentifierCount == REF_MAX_IDENTIFIER) { LogError(Config, LOG_EMERGENCY, "REF_MAX_IDENTIFIER (%d) reached. Contact matt@handmadedev.org", REF_MAX_IDENTIFIER); fprintf(stderr, "%s:%d: Too many timecodes associated with one reference (increase REF_MAX_IDENTIFIER)\n", Filename, Anno->line); hmml_free(&HMML); return RC_ERROR_MAX_REFS; } if(CurrentRef->isbn) { if(!StringsDiffer(CurrentRef->isbn, ReferencesArray[i].ID)) { CopyString(ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Timecode, Anno->time); ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Identifier = RefIdentifier; ++ReferencesArray[i].IdentifierCount; goto AppendedIdentifier; } } else if(CurrentRef->url) { if(!StringsDiffer(CurrentRef->url, ReferencesArray[i].ID)) { CopyString(ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Timecode, Anno->time); ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Identifier = RefIdentifier; ++ReferencesArray[i].IdentifierCount; goto AppendedIdentifier; } } else { LogError(Config, LOG_ERROR, "Reference missing ISBN or URL: %s:%d", Filename, Anno->line); fprintf(stderr, "%s:%d: Reference must have an ISBN or URL\n", Filename, Anno->line); hmml_free(&HMML); return RC_INVALID_REFERENCE; } } if(BuildReference(ReferencesArray, RefIdentifier, UniqueRefs, *CurrentRef, *Anno) == 1) { LogError(Config, LOG_ERROR, "Reference combination processing failed: %s:%d", Filename, Anno->line); fprintf(stderr, "%s:%d: Cannot process new combination of reference info\n" "\n" "Either tweak your annotation, or contact matt@handmadedev.org\n" "mentioning the ref node you want to write and how you want it to\n" "appear in the references menu\n", Filename, Anno->line); hmml_free(&HMML); return RC_INVALID_REFERENCE; } ++ReferencesArray[UniqueRefs].IdentifierCount; ++UniqueRefs; } AppendedIdentifier: if(!HasReference) { if(CurrentRef->isbn) { CopyStringToBuffer(&AnnotationData, " data-ref=\"%s", CurrentRef->isbn); } else if(CurrentRef->url) { CopyStringToBuffer(&AnnotationData, " data-ref=\"%s", CurrentRef->url); } else { LogError(Config, LOG_ERROR, "Reference missing ISBN or URL: %s:%d", Filename, Anno->line); fprintf(stderr, "%s:%d: Reference must have an ISBN or URL\n", Filename, Anno->line); hmml_free(&HMML); return RC_INVALID_REFERENCE; } HasReference = TRUE; } else { if(CurrentRef->isbn) { CopyStringToBuffer(&AnnotationData, ",%s", CurrentRef->isbn); } else if(CurrentRef->url) { CopyStringToBuffer(&AnnotationData, ",%s", CurrentRef->url); } else { LogError(Config, LOG_ERROR, "Reference missing ISBN or URL: %s:%d", Filename, Anno->line); fprintf(stderr, "%s:%d: Reference must have an ISBN or URL", Filename, Anno->line); hmml_free(&HMML); return RC_INVALID_REFERENCE; } } if(Anno->references[RefIndex].offset == Anno->references[RefIndex-1].offset) { CopyStringToBuffer(&Text, ",%d", RefIdentifier); } else { CopyStringToBuffer(&Text, "%d", RefIdentifier); } ++RefIndex; ++RefIdentifier; } if(*InPtr) { switch(*InPtr) { case '<': CopyStringToBuffer(&Text, "<"); InPtr++; break; case '>': CopyStringToBuffer(&Text, ">"); InPtr++; break; case '&': CopyStringToBuffer(&Text, "&"); InPtr++; break; case '\"': CopyStringToBuffer(&Text, """); InPtr++; break; case '\'': CopyStringToBuffer(&Text, "'"); InPtr++; break; default: *Text.Ptr++ = *InPtr++; *Text.Ptr = '\0'; break; } } } if(Anno->is_quote) { if(!HasQuoteMenu) { CopyStringToBuffer(&QuoteMenu, "
\n" " Quotes ▼\n" "
\n" "
\n"); HasQuoteMenu = TRUE; } if(!HasReference) { CopyStringToBuffer(&AnnotationData, " data-ref=\"&#%d;", QuoteIdentifier); } else { CopyStringToBuffer(&AnnotationData, ",&#%d;", QuoteIdentifier); } HasQuote = TRUE; char *Speaker = Anno->quote.author ? Anno->quote.author : HMML.metadata.stream_username ? HMML.metadata.stream_username : HMML.metadata.member; if(BuildQuote(&QuoteInfo, Speaker, Anno->quote.id, Config.CacheDir) == 1) { LogError(Config, LOG_ERROR, "Quote #%s %d not found: %s:%d", Speaker, Anno->quote.id, Filename, Anno->line); fprintf(stderr, "%s:%d: Quote #%s %d not found. Skipping this file...\n", Filename, Anno->line, Speaker, Anno->quote.id); hmml_free(&HMML); return RC_ERROR_QUOTE; } CopyStringToBuffer(&QuoteMenu, " \n" " \n" " \n" "
Quote %d
\n" "
", Speaker, Anno->quote.id, QuoteIdentifier, Anno->quote.id); CopyStringToBufferHTMLSafe(&QuoteMenu, QuoteInfo.Text); CopyStringToBuffer(&QuoteMenu, "
\n" " \n" "
\n" "
\n" " [&#%d;]%s\n" "
\n" "
\n" "
\n", Speaker, QuoteInfo.Date, TimecodeToSeconds(Anno->time), QuoteIdentifier, Anno->time); if(!Anno->text[0]) { CopyStringToBuffer(&Text, "“"); CopyStringToBufferHTMLSafe(&Text, QuoteInfo.Text); CopyStringToBuffer(&Text, "”"); } CopyStringToBuffer(&Text, "&#%d;", QuoteIdentifier); ++QuoteIdentifier; } while(MarkerIndex < Anno->marker_count) { switch(GenerateTopicColours(Anno->markers[MarkerIndex].marker, Config.CSSDir)) { case RC_SUCCESS: case RC_NOOP: break; case RC_ERROR_FILE: case RC_ERROR_MEMORY: return RC_ERROR_FATAL; }; if(!HasFilterMenu) { HasFilterMenu = TRUE; } if(Anno->markers[MarkerIndex].marker) { InsertCategory(&Topics, &Media, Anno->markers[MarkerIndex].marker); } InsertCategory(&LocalTopics, &LocalMedia, Anno->markers[MarkerIndex].marker); ++MarkerIndex; } if(LocalMedia.Count == 0) { InsertCategory(&Topics, &Media, Config.DefaultMedium); InsertCategory(&LocalTopics, &LocalMedia, Config.DefaultMedium); } BuildCategories(&AnnotationClass, &TopicDots, LocalTopics, LocalMedia, &MarkerIndex); CopyBuffer(&AnnotationHeader, &AnnotationClass); if(HasQuote || HasReference) { CopyStringToBuffer(&AnnotationData, "\""); CopyBuffer(&AnnotationHeader, &AnnotationData); } CopyStringToBuffer(&AnnotationHeader, ">\n"); CopyBuffer(&Annotation, &AnnotationHeader); CopyStringToBuffer(&Annotation, "
%s", Anno->time); CopyBuffer(&Annotation, &Text); if(LocalTopics.Count > 0) { CopyBuffer(&Annotation, &TopicDots); } CopyStringToBuffer(&Annotation, "
\n" "
\n" "
%s", Anno->time); CopyBuffer(&Annotation, &Text); CopyStringToBuffer(&Annotation, "
\n" "
\n" "
\n" "
%s", Anno->time); CopyBuffer(&Annotation, &Text); CopyStringToBuffer(&Annotation, "
\n" "
\n" "
\n"); CopyBuffer(&CollationBuffers->Player, &Annotation); // NOTE(matt): Tree structure of "annotation local" buffer dependencies // TopicDots // Text // AnnotationData // AnnotationClass // AnnotationHeader // Annotation DeclaimBuffer(MemoryArena, &TopicDots); DeclaimBuffer(MemoryArena, &Text); DeclaimBuffer(MemoryArena, &AnnotationData); DeclaimBuffer(MemoryArena, &AnnotationClass); DeclaimBuffer(MemoryArena, &AnnotationHeader); DeclaimBuffer(MemoryArena, &Annotation); } #if DEBUG printf("\n\n --- End of Annotations Loop ---\n\n\n\n"); #endif if(HasQuoteMenu) { CopyStringToBuffer(&QuoteMenu, "
\n" "
\n"); CopyBuffer(&CollationBuffers->Menus, &QuoteMenu); } if(HasReferenceMenu) { for(int i = 0; i < UniqueRefs; ++i) { CopyStringToBuffer(&ReferenceMenu, " \n" " \n", ReferencesArray[i].ID, ReferencesArray[i].URL); if(*ReferencesArray[i].Source) { CopyStringToBuffer(&ReferenceMenu, "
"); CopyStringToBufferHTMLSafe(&ReferenceMenu, ReferencesArray[i].Source); CopyStringToBuffer(&ReferenceMenu, "
\n" "
"); CopyStringToBufferHTMLSafe(&ReferenceMenu, ReferencesArray[i].RefTitle); CopyStringToBuffer(&ReferenceMenu, "
\n"); } else { CopyStringToBuffer(&ReferenceMenu, "
"); CopyStringToBufferHTMLSafe(&ReferenceMenu, ReferencesArray[i].RefTitle); CopyStringToBuffer(&ReferenceMenu, "
\n"); } CopyStringToBuffer(&ReferenceMenu, "
\n"); for(int j = 0; j < ReferencesArray[i].IdentifierCount;) { CopyStringToBuffer(&ReferenceMenu, "
\n "); for(int k = 0; k < 3 && j < ReferencesArray[i].IdentifierCount; ++k, ++j) { CopyStringToBuffer(&ReferenceMenu, "[%d]%s", TimecodeToSeconds(ReferencesArray[i].Identifier[j].Timecode), ReferencesArray[i].Identifier[j].Identifier, ReferencesArray[i].Identifier[j].Timecode); } CopyStringToBuffer(&ReferenceMenu, "\n" "
\n"); } CopyStringToBuffer(&ReferenceMenu, "
\n"); } CopyStringToBuffer(&ReferenceMenu, "
\n" "
\n"); CopyBuffer(&CollationBuffers->Menus, &ReferenceMenu); } if(HasFilterMenu) { CopyStringToBuffer(&FilterState, " var filterState = {\n"); for(int i = 0; i < Topics.Count; ++i) { CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": false },\n", Topics.Category[i].Marker, "topic"); } for(int i = 0; i < Media.Count; ++i) { CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": false },\n", Media.Category[i].Marker, "medium"); } CopyStringToBuffer(&FilterState, " };\n"); if(Config.Edition == EDITION_PROJECT) { CopyStringToBuffer(&FilterMenu, "
\n" " \n" "
\n" "
Filter mode:
\n" "
\n", Config.ImagesDir); } else { CopyStringToBuffer(&FilterMenu, "
\n" " \n" "
\n" "
Filter mode:
\n" "
\n", Config.ImagesDir); } if(Topics.Count > 0) { CopyStringToBuffer(&FilterMenu, "
\n" "
Topics
\n"); for(int i = 0; i < Topics.Count; ++i) { CopyStringToBuffer(&FilterTopics, "
\n" " %s\n" "
\n", Topics.Category[i].Marker, Topics.Category[i].Marker, Topics.Category[i].Marker); } CopyStringToBuffer(&FilterTopics, "
\n"); CopyBuffer(&FilterMenu, &FilterTopics); } if(Media.Count > 0) { CopyStringToBuffer(&FilterMedia, "
\n" "
Media
\n"); for(int i = 0; i < Media.Count; ++i) { int j; for(j = 0; j < ArrayCount(CategoryMedium); ++j) { if(!StringsDiffer(Media.Category[i].Marker, CategoryMedium[j].Medium)) { break; } } CopyStringToBuffer(&FilterMedia, "
\n" " %s%s\n" "
\n", Media.Category[i].Marker, CategoryMedium[j].Icon, CategoryMedium[j].WrittenName ); } CopyStringToBuffer(&FilterMedia, "
\n"); CopyBuffer(&FilterMenu, &FilterMedia); } CopyStringToBuffer(&FilterMenu, "
\n" "
\n" "
\n"); CopyBuffer(&CollationBuffers->Menus, &FilterMenu); } if(HasCreditsMenu) { CopyBuffer(&CollationBuffers->Menus, &CreditsMenu); } CopyStringToBuffer(&CollationBuffers->Menus, "
\n" " ?\n" "
\n" " ?

Keyboard Navigation

\n" "\n" "

Global Keys

\n" " W, A, P / S, D, N Jump to previous / next marker
\n"); if(HasFilterMenu) { CopyStringToBuffer(&CollationBuffers->Menus, " z Toggle filter mode V Revert filter to original state\n"); } else { CopyStringToBuffer(&CollationBuffers->Menus, " z Toggle filter mode V Revert filter to original state\n"); } CopyStringToBuffer(&CollationBuffers->Menus, "\n" "

Menu toggling

\n"); if(HasQuoteMenu) { CopyStringToBuffer(&CollationBuffers->Menus, " q Quotes\n"); } else { CopyStringToBuffer(&CollationBuffers->Menus, " q Quotes\n"); } if(HasReferenceMenu) { CopyStringToBuffer(&CollationBuffers->Menus, " r References\n"); } else { CopyStringToBuffer(&CollationBuffers->Menus, " r References\n"); } if(HasFilterMenu) { CopyStringToBuffer(&CollationBuffers->Menus, " f Filter\n"); } else { CopyStringToBuffer(&CollationBuffers->Menus, " f Filter\n"); } if(HasCreditsMenu) { CopyStringToBuffer(&CollationBuffers->Menus, " c Credits\n"); } else { CopyStringToBuffer(&CollationBuffers->Menus, " c Credits\n"); } CopyStringToBuffer(&CollationBuffers->Menus, "\n" "

Movement

\n" "
\n" "
\n" "
\n" " a\n" "
\n" "
\n" " w
\n" " s\n" "
\n" "
\n" " d\n" "
\n" "
\n" "
\n" " h\n" " j\n" " k\n" " l\n" "
\n" "
\n" "
\n" " \n" "
\n" "
\n" "
\n" " \n" "
\n" "
\n" " \n" "
\n" "
\n" "
\n" "
\n"); if(HasQuoteMenu) { CopyStringToBuffer(&CollationBuffers->Menus, "

Quotes "); if(HasReferenceMenu) { CopyStringToBuffer(&CollationBuffers->Menus, "and References Menus

\n"); } else { CopyStringToBuffer(&CollationBuffers->Menus, "and References Menus\n"); } } else { CopyStringToBuffer(&CollationBuffers->Menus, "

Quotes"); if(HasReferenceMenu) { CopyStringToBuffer(&CollationBuffers->Menus, " and References Menus

\n"); } else { CopyStringToBuffer(&CollationBuffers->Menus, " and References Menus\n"); } } if(HasQuoteMenu || HasReferenceMenu) { CopyStringToBuffer(&CollationBuffers->Menus, " Enter Jump to timecode
\n"); } else { CopyStringToBuffer(&CollationBuffers->Menus, " Enter Jump to timecode
\n"); } CopyStringToBuffer(&CollationBuffers->Menus, "\n"); if(HasQuoteMenu) { CopyStringToBuffer(&CollationBuffers->Menus, "

Quotes"); if(HasReferenceMenu) { CopyStringToBuffer(&CollationBuffers->Menus, ", References "); if(HasCreditsMenu) { CopyStringToBuffer(&CollationBuffers->Menus, "and Credits Menus

"); } else { CopyStringToBuffer(&CollationBuffers->Menus, "and Credits Menus"); } } else { CopyStringToBuffer(&CollationBuffers->Menus, ", References "); if(HasCreditsMenu) { CopyStringToBuffer(&CollationBuffers->Menus, "and Credits Menus"); } else { CopyStringToBuffer(&CollationBuffers->Menus, "and Credits Menus"); } } } else { CopyStringToBuffer(&CollationBuffers->Menus, "

Quotes"); if(HasReferenceMenu) { CopyStringToBuffer(&CollationBuffers->Menus, ", References "); if(HasCreditsMenu) { CopyStringToBuffer(&CollationBuffers->Menus, "and Credits Menus

"); } else { CopyStringToBuffer(&CollationBuffers->Menus, "and Credits Menus"); } } else { CopyStringToBuffer(&CollationBuffers->Menus, ", References "); if(HasCreditsMenu) { CopyStringToBuffer(&CollationBuffers->Menus, "and Credits Menus"); } else { CopyStringToBuffer(&CollationBuffers->Menus, "and Credits Menus"); } } } CopyStringToBuffer(&CollationBuffers->Menus, "\n"); if(HasQuoteMenu || HasReferenceMenu || HasCreditsMenu) { CopyStringToBuffer(&CollationBuffers->Menus, " o Open URL (in new tab)\n"); } else { CopyStringToBuffer(&CollationBuffers->Menus, " o Open URL (in new tab)\n"); } CopyStringToBuffer(&CollationBuffers->Menus, "\n"); if(HasFilterMenu) { CopyStringToBuffer(&CollationBuffers->Menus, "

Filter Menu

\n" " x, Space Toggle category and focus next
\n" " X, ShiftSpace Toggle category and focus previous
\n" " v Invert topics / media as per focus\n"); } else { CopyStringToBuffer(&CollationBuffers->Menus, "

Filter Menu

\n" " x, Space Toggle category and focus next
\n" " X, ShiftSpace Toggle category and focus previous
\n" " v Invert topics / media as per focus\n"); } CopyStringToBuffer(&CollationBuffers->Menus, "\n"); if(HasCreditsMenu) { CopyStringToBuffer(&CollationBuffers->Menus, "

Credits Menu

\n" " Enter Open URL (in new tab)
\n"); } else { CopyStringToBuffer(&CollationBuffers->Menus, "

Credits Menu

\n" " Enter Open URL (in new tab)
\n"); } CopyStringToBuffer(&CollationBuffers->Menus, "
\n" "
\n" "
"); CopyStringToBuffer(&CollationBuffers->Player, "
\n" "
"); // TODO(matt): Maybe do something about indentation levels // TODO(matt): We may need to do some actual logic here to figure out // where the style paths are in relation to us, rather than assuming them // to be one directory up the tree if(Config.Edition == EDITION_PROJECT) { CopyStringToBuffer(&CollationBuffers->IncludesIndex, "\n" " \n" " \n" "\n" " \n" " \n", Config.CSSDir, Config.CSSDir, HMML.metadata.project, Config.CSSDir); CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "\n" " \n" " \n", Config.CSSDir, Config.CSSDir, HMML.metadata.project, Config.CSSDir); } else { CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "\n" " \n" " \n", Config.CSSDir, Config.CSSDir, HMML.metadata.project, Config.CSSDir); } CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "\n" " \n" " \n"); if(Topics.Count || Media.Count) { CopyStringToBuffer(&CollationBuffers->IncludesPlayer, " 0) { for(int i = 0; i < Topics.Count; ++i) { CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "%s, ", Topics.Category[i].Marker); } } if(Media.Count > 0) { for(int i = 0; i < Media.Count; ++i) { CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "%s, ", Media.Category[i].WrittenText); } } CollationBuffers->IncludesPlayer.Ptr -= 2; CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "\">\n\n"); } if(Config.Edition == EDITION_PROJECT) { CopyStringToBuffer(&CollationBuffers->IncludesPlayer, " \n", Config.JSDir); } else { CopyStringToBuffer(&CollationBuffers->IncludesPlayer, " \n", Config.JSDir); } CopyStringToBuffer(&CollationBuffers->Script, " "); // NOTE(matt): Tree structure of "global" buffer dependencies // FilterState // CreditsMenu // FilterMedia // FilterTopics // FilterMenu // ReferenceMenu // QuoteMenu Cleanup: DeclaimBuffer(MemoryArena, &FilterState); DeclaimBuffer(MemoryArena, &CreditsMenu); DeclaimBuffer(MemoryArena, &FilterMedia); DeclaimBuffer(MemoryArena, &FilterTopics); DeclaimBuffer(MemoryArena, &FilterMenu); DeclaimBuffer(MemoryArena, &ReferenceMenu); DeclaimBuffer(MemoryArena, &QuoteMenu); } else { LogError(Config, LOG_ERROR, "%s:%d: %s", Filename, HMML.error.line, HMML.error.message); fprintf(stderr, "%s:%d: %s\n", Filename, HMML.error.line, HMML.error.message); hmml_free(&HMML); return RC_ERROR_HMML; } hmml_free(&HMML); return RC_SUCCESS; } int BuffersToHTML(arena *MemoryArena, buffers CollationBuffers, template *TemplateMetadata, config Config, char *OutputPath, int PageType) { #if DEBUG printf("\n\n --- Buffer Collation ---\n" " %s\n\n\n", OutputPath); #endif if(Config.Mode == MODE_INTEGRATE) { if(TemplateMetadata->Validity & PageType) { // TODO(matt): Maybe enable the Template(s) to stick around for the duration of RefreshProject() buffer Template; Template.ID = TemplateMetadata->Filename; FILE *TemplateFile; if(!(TemplateFile = fopen(TemplateMetadata->Filename, "r"))) { LogError(Config, LOG_ERROR, "Unable to open template %s: %s", TemplateMetadata->Filename, strerror(errno)); return RC_ERROR_FILE; } fseek(TemplateFile, 0, SEEK_END); Template.Size = ftell(TemplateFile); fseek(TemplateFile, 0, SEEK_SET); if(!(Template.Location = malloc(Template.Size))) { LogError(Config, LOG_ERROR, "BuffersToHTML(): %s", strerror(errno)); fclose(TemplateFile); return RC_ERROR_MEMORY; } Template.Ptr = Template.Location; fread(Template.Location, Template.Size, 1, TemplateFile); fclose(TemplateFile); buffer Output; Output.Size = Template.Size + (Kilobytes(512)); Output.ID = "Output"; if(!(Output.Location = malloc(Output.Size))) { LogError(Config, LOG_ERROR, "BuffersToHTML(): %s", strerror(errno)); FreeBuffer(&Template); return RC_ERROR_MEMORY; } Output.Ptr = Output.Location; Template.Ptr = Template.Location; for(int i = 0; i < TemplateMetadata->TagCount; ++i) { int j = 0; while(TemplateMetadata->Tag[i].Offset > j) { *Output.Ptr++ = *Template.Ptr++; ++j; } switch(TemplateMetadata->Tag[i].TagCode) { case TAG_TITLE: CopyStringToBuffer(&Output, CollationBuffers.Title); break; case TAG_INDEX: CopyBuffer(&Output, &CollationBuffers.Index); break; case TAG_INCLUDES: if(PageType == PAGE_PLAYER) { CopyBuffer(&Output, &CollationBuffers.IncludesPlayer); } else { CopyBuffer(&Output, &CollationBuffers.IncludesIndex); } break; case TAG_MENUS: CopyBuffer(&Output, &CollationBuffers.Menus); break; case TAG_PLAYER: CopyBuffer(&Output, &CollationBuffers.Player); break; case TAG_SCRIPT: CopyBuffer(&Output, &CollationBuffers.Script); break; } DepartComment(&Template); } while(Template.Ptr - Template.Location < Template.Size) { *Output.Ptr++ = *Template.Ptr++; } FreeBuffer(&Template); FILE *OutFile; if(!(OutFile = fopen(Config.Edition == EDITION_PROJECT ? OutputPath : Config.OutIntegratedLocation, "w"))) { LogError(Config, LOG_ERROR, "Unable to open output file %s: %s", Config.Edition == EDITION_PROJECT ? OutputPath : Config.OutIntegratedLocation, strerror(errno)); free(Output.Location); return RC_ERROR_FILE; } fwrite(Output.Location, Output.Ptr - Output.Location, 1, OutFile); fclose(OutFile); free(Output.Location); return RC_SUCCESS; } else { return RC_INVALID_TEMPLATE; } } else { buffer Master; if(ClaimBuffer(MemoryArena, &Master, "Master", Kilobytes(512)) == RC_ARENA_FULL) { return RC_ARENA_FULL; }; CopyStringToBuffer(&Master, "\n" " \n"); CopyBuffer(&Master, PageType == PAGE_PLAYER ? &CollationBuffers.IncludesPlayer : &CollationBuffers.IncludesIndex); CopyStringToBuffer(&Master, "\n"); CopyStringToBuffer(&Master, " \n" " \n"); if(PageType == PAGE_PLAYER) { CopyBuffer(&Master, &CollationBuffers.Menus); CopyStringToBuffer(&Master, "\n"); CopyBuffer(&Master, &CollationBuffers.Player); CopyStringToBuffer(&Master, "\n"); CopyBuffer(&Master, &CollationBuffers.Script); CopyStringToBuffer(&Master, "\n"); } else { CopyBuffer(&Master, &CollationBuffers.Index); } CopyStringToBuffer(&Master, " \n" "\n"); FILE *OutFile; if(!(OutFile = fopen(Config.Edition == EDITION_PROJECT ? OutputPath : Config.OutLocation, "w"))) { LogError(Config, LOG_ERROR, "Unable to open output file %s: %s", Config.Edition == EDITION_PROJECT ? OutputPath : Config.OutLocation, strerror(errno)); DeclaimBuffer(MemoryArena, &Master); return RC_ERROR_FILE; } fwrite(Master.Location, Master.Ptr - Master.Location, 1, OutFile); fclose(OutFile); DeclaimBuffer(MemoryArena, &Master); return RC_SUCCESS; } } int RefreshIndex(arena *MemoryArena, buffers *CollationBuffers, config Config, char *BaseFilename) { char IndexPath[255]; CopyString(IndexPath, "%s/%s.index", Config.CacheDir, CollationBuffers->Project); FILE *IndexFile; if(!(IndexFile = fopen(IndexPath, "a+"))) { LogError(Config, LOG_ERROR, "Unable to open index file %s: %s", IndexPath, strerror(errno)); return RC_ERROR_FILE; } buffer Index; Index.ID = "Index"; fseek(IndexFile, 0, SEEK_END); int IndexFileSize = ftell(IndexFile); Index.Size = IndexFileSize + StringLength(BaseFilename) + StringLength(CollationBuffers->Title) + 3; fseek(IndexFile, 0, SEEK_SET); if(!(Index.Location = malloc(Index.Size))) { LogError(Config, LOG_ERROR, "RefreshIndex(): %s", strerror(errno)); return RC_ERROR_MEMORY; } Index.Ptr = Index.Location; fread(Index.Location, IndexFileSize, 1, IndexFile); bool Found = FALSE; bool Inserted = FALSE; int EntryCount = 0; while(Index.Ptr - Index.Location < IndexFileSize) { char IndexedFile[32]; char *Ptr = IndexedFile; Index.Ptr += CopyStringNoFormatT(Ptr, Index.Ptr, ','); if(!StringsDiffer(IndexedFile, BaseFilename)) { Found = TRUE; break; } else if(StringsDiffer(IndexedFile, BaseFilename) > 0) { while(Index.Ptr > Index.Location && *Index.Ptr != '\n') { --Index.Ptr; } if(Index.Ptr > Index.Location) { ++Index.Ptr; } int Head = Index.Ptr - Index.Location; buffer Scratch; Scratch.Size = IndexFileSize; if(!(Scratch.Location = malloc(Scratch.Size + 1))) { LogError(Config, LOG_ERROR, "RefreshIndex(): %s", strerror(errno)); free(Index.Location); fclose(IndexFile); return RC_ERROR_MEMORY; } Scratch.Ptr = Scratch.Location; while(Index.Ptr - Index.Location < IndexFileSize) { *Scratch.Ptr++ = *Index.Ptr++; } *Scratch.Ptr = '\0'; Index.Ptr = Index.Location + Head; CopyStringToBuffer(&Index, "%s,%s\n" "%s", BaseFilename, CollationBuffers->Title, Scratch.Location); free(Scratch.Location); Index.Size = Index.Ptr - Index.Location; fclose(IndexFile); IndexFile = fopen(IndexPath, "w"); fwrite(Index.Location, Index.Size, 1, IndexFile); Inserted = TRUE; } else { while(Index.Ptr - Index.Location < IndexFileSize && *Index.Ptr != '\n') { ++Index.Ptr; } ++Index.Ptr; } ++EntryCount; } if(!Found) { ++EntryCount; // TODO(matt): Write the EntryCount into the index if(!Inserted) { CopyStringToBuffer(&Index, "%s,%s\n", BaseFilename, CollationBuffers->Title); fprintf(IndexFile, "%s,%s\n", BaseFilename, CollationBuffers->Title); } RewindBuffer(&CollationBuffers->Index); Index.Size = Index.Ptr - Index.Location; RewindBuffer(&Index); CopyStringToBuffer(&CollationBuffers->Index, "
\n"); while(Index.Ptr - Index.Location < Index.Size) { char IndexedFile[32]; char *Ptr = IndexedFile; Index.Ptr += CopyStringNoFormatT(Ptr, Index.Ptr, ',') + 1; char Title[255]; Ptr = Title; Index.Ptr += CopyStringNoFormatT(Ptr, Index.Ptr, '\n') + 1; // TODO(matt): Fully figure out why the Table of Contents doesn't always get all the stuff from the index // Steps to reproduce: // 1. mv riscy04{4..6}.hmml out of the way // 2. Call the program // 3. mv riscy046.hmml into the project directory // 4. See that riscy046 is in the index file, has its own player page, but is not mentioned in the Table of Contents CopyStringToBuffer(&CollationBuffers->Index, "
\n" " %s\n" "
\n", IndexedFile, Title); } CopyStringToBuffer(&CollationBuffers->Index, "
"); } fclose(IndexFile); FreeBuffer(&Index); return Found ? RC_NOOP : RC_REFRESHED; } int RefreshProject(arena *MemoryArena, buffers *CollationBuffers, template *IndexTemplateMetadata, template *PlayerTemplateMetadata, config Config) { DIR *ProjectDirHandle; if(!(ProjectDirHandle = opendir(Config.ProjectDir))) { LogError(Config, LOG_ERROR, "Unable to scan project directory %s: %s", Config.ProjectDir, strerror(errno)); fprintf(stderr, "Unable to scan project directory %s: %s\n", Config.ProjectDir, strerror(errno)); return RC_ERROR_DIRECTORY; } struct dirent *ProjectFiles; int FileIndex = 0; NextFile: while((ProjectFiles = readdir(ProjectDirHandle))) { // TODO(matt): Loft out into a function, maybe? char *Ptr; Ptr = ProjectFiles->d_name; Ptr += (StringLength(ProjectFiles->d_name) - StringLength(".hmml")); if(!(StringsDiffer(Ptr, ".hmml"))) { *Ptr = '\0'; char BaseFilename[255]; CopyString(BaseFilename, ProjectFiles->d_name); *Ptr = '.'; switch(HMMLToBuffers(MemoryArena, CollationBuffers, Config, ProjectFiles->d_name)) { // TODO(matt): Actually sort out the fatality of these cases, once we are always-on case RC_ERROR_FILE: case RC_ERROR_FATAL: closedir(ProjectDirHandle); return RC_ERROR_FATAL; case RC_ERROR_HMML: case RC_ERROR_MAX_REFS: case RC_ERROR_QUOTE: case RC_INVALID_REFERENCE: goto NextFile; case RC_SUCCESS: break; }; switch(RefreshIndex(MemoryArena, CollationBuffers, Config, BaseFilename)) { // TODO(matt): Actually sort out the fatality of these cases, once we are always-on case RC_ERROR_FILE: case RC_ERROR_MEMORY: closedir(ProjectDirHandle); return RC_ERROR_FATAL; case RC_NOOP: break; case RC_REFRESHED: { char OutputDir[255]; CopyString(OutputDir, "%s/%s", Config.BaseDir, BaseFilename); char OutputPath[255]; CopyString(OutputPath, "%s/index.html", OutputDir); DIR *OutputDirectoryHandle; if(!(OutputDirectoryHandle = opendir(OutputDir))) { if(MakeDir(OutputDir) == RC_ERROR_DIRECTORY) { LogError(Config, LOG_ERROR, "Unable to create directory %s: %s", OutputDir, strerror(errno)); fprintf(stderr, "Unable to create directory %s: %s\n", OutputDir, strerror(errno)); closedir(ProjectDirHandle); return RC_ERROR_DIRECTORY; }; } closedir(OutputDirectoryHandle); // TODO(matt): Implement checksumming? char IndexPagePath[255]; CopyString(IndexPagePath, "%s/index.html", Config.BaseDir); switch(BuffersToHTML(MemoryArena, *CollationBuffers, IndexTemplateMetadata, Config, IndexPagePath, PAGE_INDEX)) { // TODO(matt): Actually sort out the fatality of these cases, once we are always-on case RC_INVALID_TEMPLATE: LogError(Config, LOG_ERROR, "Invalid index template: %s", IndexTemplateMetadata->Filename); fprintf(stderr, "Invalid index template: %s\n", IndexTemplateMetadata->Filename); closedir(ProjectDirHandle); case RC_ERROR_MEMORY: case RC_ERROR_FILE: case RC_ARENA_FULL: return RC_ERROR_FATAL; case RC_SUCCESS: break; } switch(BuffersToHTML(MemoryArena, *CollationBuffers, PlayerTemplateMetadata, Config, OutputPath, PAGE_PLAYER)) { // TODO(matt): Actually sort out the fatality of these cases, once we are always-on case RC_INVALID_TEMPLATE: LogError(Config, LOG_ERROR, "Invalid player template: %s", PlayerTemplateMetadata->Filename); fprintf(stderr, "Invalid player template: %s\n", PlayerTemplateMetadata->Filename); closedir(ProjectDirHandle); case RC_ERROR_MEMORY: case RC_ERROR_FILE: case RC_ARENA_FULL: return RC_ERROR_FATAL; case RC_SUCCESS: break; } } break; } ++FileIndex; } } closedir(ProjectDirHandle); return RC_SUCCESS; } int MonitorDirectory(arena *MemoryArena, buffers *CollationBuffers, template *IndexTemplateMetadata, template *PlayerTemplateMetadata, config Config, int inotifyInstance, int WatchDescriptor) { buffer Events; if(ClaimBuffer(MemoryArena, &Events, "inotify Events", Kilobytes(4)) == RC_ARENA_FULL) { return RC_ARENA_FULL; }; struct inotify_event *Event; int BytesRead; while((BytesRead = read(inotifyInstance, Events.Location, Events.Size)) != -1 && errno == EAGAIN && BytesRead > 0) { for(Events.Ptr = Events.Location; Events.Ptr < Events.Location + BytesRead; Events.Ptr += sizeof(struct inotify_event) + Event->len) { Event = (struct inotify_event *)Events.Ptr; switch(RefreshProject(MemoryArena, CollationBuffers, IndexTemplateMetadata, PlayerTemplateMetadata, Config)) { case RC_ERROR_DIRECTORY: case RC_ERROR_FATAL: DeclaimBuffer(MemoryArena, &Events); return RC_ERROR_FATAL; case RC_SUCCESS: break; } } } DeclaimBuffer(MemoryArena, &Events); return RC_SUCCESS; } int main(int ArgC, char **Args) { // TODO(matt): Read all defaults from the config config DefaultConfig = { .BaseDir = ".", .CSSDir = ".", .Edition = EDITION_SINGLE, .ImagesDir = ".", .JSDir = ".", .LogLevel = LOG_DEBUG, .DefaultMedium = "programming", .Mode = getenv("CINERA_MODE") ? MODE_INTEGRATE : MODE_BARE, .OutLocation = "out.html", .OutIntegratedLocation = "out_integrated.html", .ForceIntegration = FALSE, .ProjectDir = ".", .TemplatePlayerLocation = "template_player.html", .TemplateIndexLocation = "template_index.html" }; if(getenv("XDG_CACHE_HOME")) { CopyString(DefaultConfig.CacheDir, "%s/cinera", getenv("XDG_CACHE_HOME")); } else { CopyString(DefaultConfig.CacheDir, "%s/.cache/cinera", getenv("HOME")); } config Config = DefaultConfig; if(ArgC < 2) { PrintUsage(Args[0], DefaultConfig); return RC_RIP; } char CommandLineArg; while((CommandLineArg = getopt(ArgC, Args, "b:c:fi:j:l:m:o:p:q:t:x:h")) != -1) { switch(CommandLineArg) { case 'b': Config.BaseDir = optarg; break; case 'c': Config.CSSDir = optarg; break; case 'f': Config.ForceIntegration = TRUE; break; case 'i': Config.ImagesDir = optarg; break; case 'j': Config.JSDir = optarg; break; case 'l': // TODO(matt): Make this actually take a string, rather than requiring the LogLevel number Config.LogLevel = StringToInt(optarg); break; case 'm': Config.DefaultMedium = optarg; break; case 'o': Config.OutLocation = optarg; Config.OutIntegratedLocation = optarg; break; case 'p': Config.ProjectDir = optarg; break; case 't': Config.TemplatePlayerLocation = optarg; Config.Mode = MODE_INTEGRATE; break; case 'x': Config.TemplateIndexLocation = optarg; Config.Mode = MODE_INTEGRATE; break; //case 'c': // Override config path, once we even have a default! case 'h': default: PrintUsage(Args[0], DefaultConfig); return 1; } } if(StringsDiffer(Config.BaseDir, ".") || StringsDiffer(Config.ProjectDir, ".")) { if(StringsDiffer(Config.BaseDir, ".") && StringsDiffer(Config.ProjectDir, ".")) { Config.Edition = EDITION_PROJECT; } else { fprintf(stderr, "%s: Both the Base Output Directory and the Project Directory must be set in order for us to enter Project Mode\n", Args[0]); return 1; } } if(Config.CSSDir[StringLength(Config.CSSDir) - 1] == '/') { Config.CSSDir[StringLength(Config.CSSDir) - 1] = '\0'; } bool ValidDefaultMedium = FALSE; for(int i = 0; i < ArrayCount(CategoryMedium); ++i) { if(!StringsDiffer(Config.DefaultMedium, CategoryMedium[i].Medium)) { ValidDefaultMedium = TRUE; break; } } if(!ValidDefaultMedium) { fprintf(stderr, "Specified default medium \"%s\" not available. Valid media are:\n", Config.DefaultMedium); for(int i = 0; i < ArrayCount(CategoryMedium); ++i) { fprintf(stderr, " %s\n", CategoryMedium[i].Medium); } fprintf(stderr, "To have \"%s\" added to the list, contact matt@handmadedev.org\n", Config.DefaultMedium); return 1; } if(Config.ImagesDir[StringLength(Config.ImagesDir) - 1] == '/') { Config.ImagesDir[StringLength(Config.ImagesDir) - 1] = '\0'; } if(Config.JSDir[StringLength(Config.JSDir) - 1] == '/') { Config.JSDir[StringLength(Config.JSDir) - 1] = '\0'; } // NOTE(matt): Templating // // Config will contain paths of multiple templates // App is running all the time, and picking up changes to the config as we go // If we find a new template, we first of all validate it // In our case here, we just want to straight up validate a template if Config.Mode == MODE_INTEGRATE // And, in that same state, we gotta keep a Template buffer around // NOTE(matt): Init MemoryArena arena MemoryArena; MemoryArena.Size = Megabytes(4); if(!(MemoryArena.Location = calloc(MemoryArena.Size, 1))) { LogError(Config, LOG_EMERGENCY, "%s: %s", Args[0], strerror(errno)); return RC_RIP; } MemoryArena.Ptr = MemoryArena.Location; buffer Errors; if(ClaimBuffer(&MemoryArena, &Errors, "Errors", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; }; // NOTE(matt): Setup buffers and ptrs //char *InPtr; // NOTE(matt): Tree structure of buffer dependencies // IncludesPlayer // Menus // Player // Script buffers CollationBuffers; if(ClaimBuffer(&MemoryArena, &CollationBuffers.IncludesPlayer, "IncludesPlayer", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; }; if(ClaimBuffer(&MemoryArena, &CollationBuffers.Menus, "Menus", Kilobytes(24)) == RC_ARENA_FULL) { goto RIP; }; if(ClaimBuffer(&MemoryArena, &CollationBuffers.Player, "Player", Kilobytes(256)) == RC_ARENA_FULL) { goto RIP; }; if(ClaimBuffer(&MemoryArena, &CollationBuffers.Script, "Script", Kilobytes(8)) == RC_ARENA_FULL) { goto RIP; }; if(ClaimBuffer(&MemoryArena, &CollationBuffers.IncludesIndex, "IncludesIndex", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; }; if(ClaimBuffer(&MemoryArena, &CollationBuffers.Index, "Index", Kilobytes(8)) == RC_ARENA_FULL) { goto RIP; }; *CollationBuffers.Title = '\0'; template *PlayerTemplateMetadata; template *IndexTemplateMetadata; if(Config.Mode == MODE_INTEGRATE) { if(ClaimTemplate(&MemoryArena, &PlayerTemplateMetadata, Config.TemplatePlayerLocation) == RC_ARENA_FULL) { goto RIP; }; switch(ValidateTemplate(&Errors, PlayerTemplateMetadata, Config, PAGE_PLAYER)) { case RC_INVALID_TEMPLATE: // Invalid template case RC_ERROR_FILE: // Could not load template case RC_ERROR_MEMORY: // Could not allocate memory for template goto RIP; case RC_SUCCESS: break; } if(Config.Edition == EDITION_PROJECT) { if(ClaimTemplate(&MemoryArena, &IndexTemplateMetadata, Config.TemplateIndexLocation) == RC_ARENA_FULL) { goto RIP; }; switch(ValidateTemplate(&Errors, IndexTemplateMetadata, Config, PAGE_INDEX)) { case RC_INVALID_TEMPLATE: // Invalid template case RC_ERROR_MEMORY: // Could not allocate memory for template case RC_ERROR_FILE: // Could not load template goto RIP; case RC_SUCCESS: break; } } } // NOTE(matt) // // Single Edition == Loop over Args[FileIndex] // Project Edition == Loop over Config.ProjectDir // // Integrating or not if(Config.Edition == EDITION_PROJECT) { switch(RefreshProject(&MemoryArena, &CollationBuffers, IndexTemplateMetadata, PlayerTemplateMetadata, Config)) { case RC_ERROR_DIRECTORY: case RC_ERROR_FATAL: goto RIP; case RC_SUCCESS: break; } int inotifyInstance = inotify_init1(IN_NONBLOCK); int WatchDescriptor = inotify_add_watch(inotifyInstance, Config.ProjectDir, IN_CLOSE_WRITE | IN_MOVED_TO); while(MonitorDirectory(&MemoryArena, &CollationBuffers, IndexTemplateMetadata, PlayerTemplateMetadata, Config, inotifyInstance, WatchDescriptor) == RC_SUCCESS) { // TODO(matt): Make this update frequency configurable sleep(1); } } else { if(optind == ArgC) { fprintf(stderr, "%s: requires at least one input .hmml file\n", Args[0]); PrintUsage(Args[0], DefaultConfig); goto RIP; } NextFile: for(int FileIndex = optind; FileIndex < ArgC; ++FileIndex) { switch(HMMLToBuffers(&MemoryArena, &CollationBuffers, Config, Args[FileIndex])) { // TODO(matt): Actually sort out the fatality of these cases, once we are always-on case RC_ERROR_FILE: case RC_ERROR_FATAL: goto RIP; case RC_ERROR_HMML: case RC_ERROR_MAX_REFS: case RC_ERROR_QUOTE: case RC_INVALID_REFERENCE: if(FileIndex < (ArgC - 1)) { goto NextFile; } else { goto RIP; } case RC_SUCCESS: break; }; switch(BuffersToHTML(&MemoryArena, CollationBuffers, PlayerTemplateMetadata, Config, 0, PAGE_PLAYER)) { // TODO(matt): Actually sort out the fatality of these cases, once we are always-on case RC_INVALID_TEMPLATE: LogError(Config, LOG_ERROR, "Invalid player template: %s", PlayerTemplateMetadata->Filename); case RC_ERROR_MEMORY: case RC_ERROR_FILE: case RC_ARENA_FULL: goto RIP; case RC_SUCCESS: break; }; } } if(Config.Mode == MODE_INTEGRATE) { DeclaimTemplate(&MemoryArena, &PlayerTemplateMetadata); if(Config.Edition == EDITION_PROJECT) { DeclaimTemplate(&MemoryArena, &IndexTemplateMetadata); } } DeclaimBuffer(&MemoryArena, &CollationBuffers.Index); DeclaimBuffer(&MemoryArena, &CollationBuffers.IncludesIndex); DeclaimBuffer(&MemoryArena, &CollationBuffers.Script); DeclaimBuffer(&MemoryArena, &CollationBuffers.Player); DeclaimBuffer(&MemoryArena, &CollationBuffers.Menus); DeclaimBuffer(&MemoryArena, &CollationBuffers.IncludesPlayer); DeclaimBuffer(&MemoryArena, &Errors); RIP: free(MemoryArena.Location); }