Annotation-System/cinera/cinera.c

4535 lines
168 KiB
C

#if 0
ctime -begin ${0%.*}.ctm
gcc -g -fsanitize=address -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl
#gcc -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl
ctime -end ${0%.*}.ctm
exit
#endif
typedef struct
{
unsigned int Major, Minor, Patch;
} version;
version CINERA_APP_VERSION = {
.Major = 0,
.Minor = 5,
.Patch = 8
};
#define CINERA_DB_VERSION 1
#define DEBUG 0
#define DEBUG_MEM 0
typedef unsigned int bool;
#define TRUE 1
#define FALSE 0
#include <stdarg.h> // NOTE(matt): varargs
#include <stdio.h> // NOTE(matt): printf, sprintf, vsprintf, fprintf, perror
#include <stdlib.h> // NOTE(matt): calloc, malloc, free
#include "hmmlib.h"
#include <getopt.h> // NOTE(matt): getopts
//#include "config.h" // TODO(matt): Implement config.h
#include <curl/curl.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h> // NOTE(matt): strerror
#include <errno.h> //NOTE(matt): errno
#include <sys/inotify.h> // NOTE(matt): inotify
#include <unistd.h> // 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_PROJECT,
RC_ERROR_QUOTE,
RC_ERROR_SEEK,
RC_FOUND,
RC_UNFOUND,
RC_INVALID_REFERENCE,
RC_INVALID_TEMPLATE,
RC_NOOP,
RC_RIP,
RC_SUCCESS
} returns;
typedef struct
{
int Edition;
int LogLevel;
int Mode;
int UpdateInterval;
bool ForceIntegration;
char *RootDir; // Absolute
char *RootURL;
char *ProjectID;
char *BaseDir; // Absolute
char *CSSDir; // Relative to RootDir and RootURL
char *ImagesDir; // Relative to RootDir and RootURL
char *JSDir; // Relative to RootDir and RootURL
char *TemplateIndexLocation; // Relative to RootDir and RootURL
char *TemplatePlayerLocation; // Relative to RootDir and RootURL
char *Theme;
char CacheDir[256];
char *DefaultMedium;
char *OutLocation;
char *OutIntegratedLocation;
char *ProjectDir;
char *URLPrefix; /* NOTE(matt): This will become a full blown customisable output URL.
For now it simply replaces the ProjectID */
} config;
typedef struct
{
void *Location;
void *Ptr;
char *ID;
int Size;
} arena;
// NOTE(matt): Globals
config Config;
arena MemoryArena;
typedef struct
{
char *Location;
char *Ptr;
char *ID;
int Size;
} buffer;
typedef struct
{
buffer Buffer;
FILE *Handle;
char Path[256];
int FileSize;
} file_buffer;
enum
{
// Contents Page
TAG_INDEX,
// Player Page
TAG_INCLUDES,
TAG_MENUS,
TAG_PLAYER,
TAG_SCRIPT,
// Anywhere
TAG_PROJECT,
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_PROJECT, "__CINERA_PROJECT__" },
{ TAG_TITLE, "__CINERA_TITLE__" },
};
typedef struct
{
int Offset;
int TagCode;
} tag_offset;
typedef struct
{
char Filename[256];
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_metadata;
typedef struct
{
template_metadata Metadata;
buffer Buffer;
} template;
typedef struct
{
buffer IncludesIndex;
buffer Search;
buffer Index; // NOTE(matt): This buffer is malloc'd separately, rather than claimed from the memory_arena
buffer ScriptIndex;
buffer IncludesPlayer;
buffer Menus;
buffer Player;
buffer ScriptPlayer;
char Title[256];
char ProjectName[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[] =
{
{ "/a_waterman", "Andrew Waterman", "https://www.linkedin.com/in/andrew-waterman-76805788", "", ""},
{ "/y_lee", "Yunsup Lee", "https://www.linkedin.com/in/yunsup-lee-385b692b/", "", ""},
{ "AndrewJDR", "Andrew Johnson", "", "", ""},
{ "AsafGartner", "Asaf Gartner", "", "", ""},
{ "BretHudson", "Bret Hudson", "http://www.brethudson.com/", "cinera_sprite_patreon.png", "https://www.patreon.com/indieFunction"},
{ "ChronalDragon", "Andrew Chronister", "http://chronal.net/", "", ""},
{ "Kelimion", "Jeroen van Rijn", "https://handmade.network/home", "", ""},
{ "Mannilie", "Emmanuel Vaccaro", "http://emmanuelvaccaro.com/", "", ""},
{ "Miblo", "Matt Mascarenhas", "http://miblodelcarpio.co.uk", "cinera_sprite_patreon.png", "https://patreon.com/miblo"},
{ "Mr4thDimention", "Allen Webster", "http://www.4coder.net/", "cinera_sprite_patreon.png", "https://www.patreon.com/mr4thdimention"},
{ "ZedZull", "Jay Waggle", "", "", ""},
{ "abnercoimbre", "Abner Coimbre", "https://handmade.network/m/abnercoimbre", "cinera_sprite_patreon.png", "https://patreon.com/handmade_dev"},
{ "brianwill", "Brian Will", "http://brianwill.net/blog/", "", ""},
{ "cbloom", "Charles Bloom", "http://cbloomrants.blogspot.co.uk/", "", ""},
{ "cmuratori", "Casey Muratori", "https://handmadehero.org", "cinera_sprite_patreon.png", "https://patreon.com/cmuratori"},
{ "csnover", "Colin Snover", "https://zetafleet.com/", "", ""},
{ "debiatan", "Miguel Lechón", "http://blog.debiatan.net/", "", ""},
{ "dspecht", "Dustin Specht", "", "", ""},
{ "effect0r", "Cory Henderlite", "", "", ""},
{ "ffsjs", "ffsjs", "", "", ""},
{ "fierydrake", "Mike Tunnicliffe", "", "", ""},
{ "garlandobloom", "Matthew VanDevander", "https://lowtideproductions.com/", "cinera_sprite_patreon.png", "https://www.patreon.com/mv"},
{ "ikerms", "Iker Murga", "", "", ""},
{ "insofaras", "Alex Baines", "https://abaines.me.uk/", "", ""},
{ "jacebennett", "Jace Bennett", "", "", ""},
{ "jon", "Jonathan Blow", "http://the-witness.net/news/", "", ""},
{ "jpike", "Jacob Pike", "", "", ""},
{ "martincohen", "Martin Cohen", "http://blog.coh.io/", "", ""},
{ "miotatsu", "Mio Iwakura", "http://riscy.tv/", "cinera_sprite_patreon.png", "https://patreon.com/miotatsu"},
{ "nothings", "Sean Barrett", "https://nothings.org/", "", ""},
{ "philipbuuck", "Philip Buuck", "http://philipbuuck.com/", "", ""},
{ "powerc9000", "Clay Murray", "http://claymurray.website/", "", ""},
{ "quelsolaar", "Eskil Steenberg", "http://quelsolaar.com/", "", ""},
{ "rygorous", "Fabian Giesen", "https://fgiesen.wordpress.com/", "", ""},
{ "schme", "Kasper Sauramo", "", "", ""},
{ "sssmcgrath", "Shawn McGrath", "http://www.dyadgame.com/", "", ""},
{ "thehappiecat", "Anne", "https://www.youtube.com/c/TheHappieCat", "cinera_sprite_patreon.png", "https://www.patreon.com/thehappiecat"},
{ "theinternetftw", "Ben Craddock", "", "", ""},
{ "wheatdog", "Tim Liou", "http://stringbulbs.com/", "", ""},
{ "williamchyr", "William Chyr", "http://williamchyr.com/", "", ""},
{ "wonchun", "Won Chun", "https://twitter.com/won3d", "", ""},
};
typedef struct
{
char *Medium;
char *Icon;
char *WrittenName;
} category_medium;
category_medium CategoryMedium[] =
{
// medium icon written name
{ "admin", "&#128505;", "Administrivia"},
{ "afk", "&#8230;" , "Away from Keyboard"},
{ "authored", "&#128490;", "Chat Comment"}, // TODO(matt): Conditionally handle Chat vs Guest Comments
{ "blackboard", "&#128396;", "Blackboard"},
{ "experience", "&#127863;", "Experience"},
{ "hat", "&#127913;", "Hat"},
{ "multimedia", "&#127916;", "Media Clip"},
{ "owl", "&#129417;", "Owl of Shame"},
{ "programming", "&#128430;", "Programming"}, // TODO(matt): Potentially make this configurable per project
{ "rant", "&#128162;", "Rant"},
{ "research", "&#128214;", "Research"},
{ "run", "&#127939;", "In-Game"}, // TODO(matt): Potentially make this configurable per project
{ "speech", "&#128489;", "Speech"},
{ "trivia", "&#127922;", "Trivia"},
};
enum
{
NS_CALENDRICAL,
NS_LINEAR,
NS_SEASONAL,
} numbering_schemes;
typedef struct
{
char *ProjectID;
char *FullName;
char *Unit; // e.g. Day, Episode, Session
int NumberingScheme; // numbering_schemes
char *Medium;
char *AltURLPrefix; // NOTE(matt): This currently just straight up replaces the ProjectID in the player pages' output directories
} project_info;
project_info ProjectInfo[] =
{
{ "book", "Book Club", "Day", NS_LINEAR, "research", "" },
{ "pcalc", "pcalc", "Day", NS_LINEAR, "programming", "" },
{ "riscy", "RISCY BUSINESS", "Day", NS_LINEAR, "programming", "" },
{ "chat", "Handmade Chat", "Day", NS_LINEAR, "speech", "" },
{ "code", "Handmade Hero", "Day", NS_LINEAR, "programming", "day" },
{ "intro-to-c", "Intro to C on Windows", "Day", NS_LINEAR, "programming", "day" },
{ "misc", "Handmade Miscellany", "", NS_LINEAR, "admin", "" },
{ "ray", "Handmade Ray", "Day", NS_LINEAR, "programming", "" },
{ "hmdshow", "HandmadeDev Show", "", NS_SEASONAL, "speech", "" },
{ "obbg", "Open Block Building Game", "Episode", NS_LINEAR, "programming", "" },
{ "sysadmin", "SysAdmin", "Session", NS_LINEAR, "admin", "" },
};
#define ArrayCount(A) sizeof(A)/sizeof(*(A))
__attribute__ ((format (printf, 2, 3)))
int
CopyString(char Dest[], char *Format, ...)
{
int Length = 0;
va_list Args;
va_start(Args, Format);
Length = vsprintf(Dest, Format, Args);
va_end(Args);
return Length;
}
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, "&lt;");
String++;
break;
case '>':
CopyStringToBuffer(Dest, "&gt;");
String++;
break;
case '&':
CopyStringToBuffer(Dest, "&amp;");
String++;
break;
case '\"':
CopyStringToBuffer(Dest, "&quot;");
String++;
break;
case '\'':
CopyStringToBuffer(Dest, "&#39;");
String++;
break;
default:
*Dest->Ptr++ = *String++;
break;
}
}
}
void
CopyStringToBufferJSONSafe(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 '\\':
case '\"':
*Dest->Ptr++ = '\\';
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)
{
#if DEBUG
char LogPath[256];
CopyString(LogPath, "%s/%s", Config.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, 2, 3)))
void
LogError(int LogLevel, char *Format, ...)
{
if(Config.LogLevel >= LogLevel)
{
char LogPath[256];
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);
Buffer->Location = '\0';
#if DEBUG_MEM
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, " Freed %s\n", Buffer->ID);
fclose(MemLog);
printf(" Freed %s\n", Buffer->ID);
#endif
}
#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(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
float PercentageUsed = (float)(MemoryArena->Ptr - MemoryArena->Location) / MemoryArena->Size * 100;
printf(" ClaimBuffer(%s): %d\n"
" Total ClaimedMemory: %ld (%.2f%%, leaving %ld free)\n\n", Buffer->ID, Buffer->Size, MemoryArena->Ptr - MemoryArena->Location, PercentageUsed, MemoryArena->Size - (MemoryArena->Ptr - MemoryArena->Location));
#endif
return RC_SUCCESS;
}
void
DeclaimBuffer(buffer *Buffer)
{
*Buffer->Location = '\0';
MemoryArena.Ptr -= Buffer->Size;
float PercentageUsed = (float)(Buffer->Ptr - Buffer->Location) / Buffer->Size * 100;
#if DEBUG
printf("DeclaimBuffer(%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);
if(PercentageUsed >= 80.0f)
{
// TODO(matt): Implement either dynamically growing buffers, or phoning home to matt@handmadedev.org
LogError(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);
}
Buffer->Size = 0;
}
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
ClaimTemplate(template **Template, char *Location)
{
*Template = (template *)MemoryArena.Ptr;
(*Template)->Buffer.Location = MemoryArena.Ptr + sizeof(template);
(*Template)->Buffer.Ptr = (*Template)->Buffer.Location;
(*Template)->Buffer.ID = Location;
if(Location[0] != '/')
{
CopyString((*Template)->Metadata.Filename, "%s/%s", Config.RootDir, Location);
}
else
{
CopyString((*Template)->Metadata.Filename, "%s", Location);
}
FILE *File;
if(!(File = fopen((*Template)->Metadata.Filename, "r")))
{
LogError(LOG_ERROR, "Unable to open template %s: %s", (*Template)->Metadata.Filename, strerror(errno));
fprintf(stderr, "Unable to open template %s: %s\n", (*Template)->Metadata.Filename, strerror(errno));
return RC_ERROR_FILE;
}
fseek(File, 0, SEEK_END);
(*Template)->Buffer.Size = ftell(File);
if(MemoryArena.Ptr - MemoryArena.Location + sizeof(template) + (*Template)->Buffer.Size > MemoryArena.Size)
{
return RC_ARENA_FULL;
}
MemoryArena.Ptr += sizeof(template) + (*Template)->Buffer.Size;
fseek(File, 0, SEEK_SET);
fread((*Template)->Buffer.Location, (*Template)->Buffer.Size, 1, File);
fclose(File);
#if DEBUG
printf(" ClaimTemplate(%s): %ld\n"
" Total ClaimedMemory: %ld\n\n", (*Template)->Metadata.Filename, sizeof(template) + (*Template)->Buffer.Size, MemoryArena->Ptr - MemoryArena->Location);
#endif
return RC_SUCCESS;
}
int
DeclaimTemplate(template *Template)
{
MemoryArena.Ptr -= (sizeof(template) + (*Template).Buffer.Size);
#if DEBUG
printf("DeclaimTemplate(%s)\n"
" Total ClaimedMemory: %ld\n\n",
(*Template).Metadata.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 = 74;
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
{
INCLUDE_CSS,
INCLUDE_Images,
INCLUDE_JS,
} include_types;
enum
{
PAGE_PLAYER = 1 << 0,
PAGE_INDEX = 1 << 1
} pages;
void
ConstructURLPrefix(buffer *URLPrefix, int IncludeType, int PageType)
{
ClaimBuffer(URLPrefix, "URLPrefix", 1024);
if(StringsDiffer(Config.RootURL, ""))
{
URLPrefix->Ptr += CopyString(URLPrefix->Ptr, "%s/", Config.RootURL);
}
else
{
if(Config.Edition == EDITION_PROJECT)
{
if(PageType == PAGE_PLAYER)
{
URLPrefix->Ptr += CopyString(URLPrefix->Ptr, "../");
}
URLPrefix->Ptr += CopyString(URLPrefix->Ptr, "../");
}
}
switch(IncludeType)
{
case INCLUDE_CSS:
if(StringsDiffer(Config.CSSDir, ""))
{
URLPrefix->Ptr += CopyString(URLPrefix->Ptr, "%s/", Config.CSSDir);
}
break;
case INCLUDE_Images:
if(StringsDiffer(Config.ImagesDir, ""))
{
URLPrefix->Ptr += CopyString(URLPrefix->Ptr, "%s/", Config.ImagesDir);
}
break;
case INCLUDE_JS:
if(StringsDiffer(Config.JSDir, ""))
{
URLPrefix->Ptr += CopyString(URLPrefix->Ptr, "%s/", Config.JSDir);
}
break;
}
}
enum
{
CreditsError_NoHost,
CreditsError_NoAnnotator,
CreditsError_NoCredentials
} credits_errors;
int
SearchCredentials(buffer *CreditsMenu, bool *HasCreditsMenu, 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,
" <div class=\"menu credits\">\n"
" <div class=\"mouse_catcher\"></div>\n"
" <span>Credits</span>\n"
" <div class=\"credits_container\">\n");
*HasCreditsMenu = TRUE;
}
CopyStringToBuffer(CreditsMenu,
" <span class=\"credit\">\n");
if(*Credentials[CredentialIndex].HomepageURL)
{
CopyStringToBuffer(CreditsMenu,
" <a class=\"person\" href=\"%s\" target=\"_blank\">\n"
" <div class=\"role\">%s</div>\n"
" <div class=\"name\">%s</div>\n"
" </a>\n",
Credentials[CredentialIndex].HomepageURL,
Role,
Credentials[CredentialIndex].CreditedName);
}
else
{
CopyStringToBuffer(CreditsMenu,
" <div class=\"person\">\n"
" <div class=\"role\">%s</div>\n"
" <div class=\"name\">%s</div>\n"
" </div>\n",
Role,
Credentials[CredentialIndex].CreditedName);
}
if(*Credentials[CredentialIndex].SupportIcon && *Credentials[CredentialIndex].SupportURL)
{
buffer URLPrefix;
ConstructURLPrefix(&URLPrefix, INCLUDE_Images, PAGE_INDEX);
CopyStringToBuffer(CreditsMenu,
" <a class=\"support\" href=\"%s\" target=\"_blank\"><div class=\"support_icon\" style=\"background-image: url(%s%s);\"></div></a>\n",
Credentials[CredentialIndex].SupportURL,
URLPrefix.Location,
Credentials[CredentialIndex].SupportIcon);
DeclaimBuffer(&URLPrefix);
}
CopyStringToBuffer(CreditsMenu,
" </span>\n");
}
}
return Found ? 0 : CreditsError_NoCredentials;
}
int
BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Metadata)
// TODO(matt): Make this take the Credentials, once we are parsing them from a config
{
if(Metadata->member)
{
if(SearchCredentials(CreditsMenu, HasCreditsMenu, 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,
" </div>\n"
" </div>\n");
}
return CreditsError_NoHost;
}
if(Metadata->co_host_count > 0)
{
for(int i = 0; i < Metadata->co_host_count; ++i)
{
if(SearchCredentials(CreditsMenu, HasCreditsMenu, 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(CreditsMenu, HasCreditsMenu, 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(CreditsMenu, HasCreditsMenu, 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,
" </div>\n"
" </div>\n");
}
return CreditsError_NoAnnotator;
}
if(*HasCreditsMenu == TRUE)
{
CopyStringToBuffer(CreditsMenu,
" </div>\n"
" </div>\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 *GlobalTopics, categories *LocalTopics, categories *GlobalMedia, categories *LocalMedia, 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 < LocalMedia->Count; ++CategoryIndex)
{
if(!StringsDiffer(CategoryMedium[CategoryMediumIndex].Medium, LocalMedia->Category[CategoryIndex].Marker))
{
return;
}
if((StringsDiffer(CategoryMedium[CategoryMediumIndex].WrittenName, LocalMedia->Category[CategoryIndex].WrittenText)) < 0)
{
int CategoryCount;
for(CategoryCount = LocalMedia->Count; CategoryCount > CategoryIndex; --CategoryCount)
{
CopyString(LocalMedia->Category[CategoryCount].Marker, LocalMedia->Category[CategoryCount-1].Marker);
CopyString(LocalMedia->Category[CategoryCount].WrittenText, LocalMedia->Category[CategoryCount-1].WrittenText);
}
CopyString(LocalMedia->Category[CategoryCount].Marker, CategoryMedium[CategoryMediumIndex].Medium);
CopyString(LocalMedia->Category[CategoryCount].WrittenText, CategoryMedium[CategoryMediumIndex].WrittenName);
break;
}
}
if(CategoryIndex == LocalMedia->Count)
{
CopyString(LocalMedia->Category[CategoryIndex].Marker, CategoryMedium[CategoryMediumIndex].Medium);
CopyString(LocalMedia->Category[CategoryIndex].WrittenText, CategoryMedium[CategoryMediumIndex].WrittenName);
}
++LocalMedia->Count;
for(CategoryIndex = 0; CategoryIndex < GlobalMedia->Count; ++CategoryIndex)
{
if(!StringsDiffer(CategoryMedium[CategoryMediumIndex].Medium, GlobalMedia->Category[CategoryIndex].Marker))
{
return;
}
if((StringsDiffer(CategoryMedium[CategoryMediumIndex].WrittenName, GlobalMedia->Category[CategoryIndex].WrittenText)) < 0)
{
int CategoryCount;
for(CategoryCount = GlobalMedia->Count; CategoryCount > CategoryIndex; --CategoryCount)
{
CopyString(GlobalMedia->Category[CategoryCount].Marker, GlobalMedia->Category[CategoryCount-1].Marker);
CopyString(GlobalMedia->Category[CategoryCount].WrittenText, GlobalMedia->Category[CategoryCount-1].WrittenText);
}
CopyString(GlobalMedia->Category[CategoryCount].Marker, CategoryMedium[CategoryMediumIndex].Medium);
CopyString(GlobalMedia->Category[CategoryCount].WrittenText, CategoryMedium[CategoryMediumIndex].WrittenName);
break;
}
}
if(CategoryIndex == GlobalMedia->Count)
{
CopyString(GlobalMedia->Category[CategoryIndex].Marker, CategoryMedium[CategoryMediumIndex].Medium);
CopyString(GlobalMedia->Category[CategoryIndex].WrittenText, CategoryMedium[CategoryMediumIndex].WrittenName);
}
++GlobalMedia->Count;
}
else
{
for(CategoryIndex = 0; CategoryIndex < LocalTopics->Count; ++CategoryIndex)
{
if(!StringsDiffer(Marker, LocalTopics->Category[CategoryIndex].Marker))
{
return;
}
if((StringsDiffer(Marker, LocalTopics->Category[CategoryIndex].Marker)) < 0)
{
int CategoryCount;
for(CategoryCount = LocalTopics->Count; CategoryCount > CategoryIndex; --CategoryCount)
{
CopyString(LocalTopics->Category[CategoryCount].Marker, LocalTopics->Category[CategoryCount-1].Marker);
}
CopyString(LocalTopics->Category[CategoryCount].Marker, Marker);
break;
}
}
if(CategoryIndex == LocalTopics->Count)
{
CopyString(LocalTopics->Category[CategoryIndex].Marker, Marker);
}
++LocalTopics->Count;
for(CategoryIndex = 0; CategoryIndex < GlobalTopics->Count; ++CategoryIndex)
{
if(!StringsDiffer(Marker, GlobalTopics->Category[CategoryIndex].Marker))
{
return;
}
if((StringsDiffer(Marker, GlobalTopics->Category[CategoryIndex].Marker)) < 0)
{
int CategoryCount;
for(CategoryCount = GlobalTopics->Count; CategoryCount > CategoryIndex; --CategoryCount)
{
CopyString(GlobalTopics->Category[CategoryCount].Marker, GlobalTopics->Category[CategoryCount-1].Marker);
}
CopyString(GlobalTopics->Category[CategoryCount].Marker, Marker);
break;
}
}
if(CategoryIndex == GlobalTopics->Count)
{
CopyString(GlobalTopics->Category[CategoryIndex].Marker, Marker);
}
++GlobalTopics->Count;
}
return;
}
void
BuildCategories(buffer *AnnotationClass, buffer *TopicDots, categories *LocalTopics, categories *LocalMedia, int *MarkerIndex)
{
if(LocalTopics->Count > 0)
{
CopyStringToBuffer(TopicDots, "<span class=\"categories\">");
for(int i = 0; i < LocalTopics->Count; ++i)
{
CopyStringToBuffer(TopicDots, "<div title=\"%s\" class=\"category %s\"></div>",
SanitisePunctuation(LocalTopics->Category[i].Marker),
SanitisePunctuation(LocalTopics->Category[i].Marker));
CopyStringToBuffer(AnnotationClass, " cat_%s",
SanitisePunctuation(LocalTopics->Category[i].Marker));
}
CopyStringToBuffer(TopicDots, "</span>");
}
for(int i = 0; i < LocalMedia->Count; ++i)
{
if(!StringsDiffer(LocalMedia->Category[i].Marker, "afk")) // TODO(matt): Initially hidden config
{
CopyStringToBuffer(AnnotationClass, " off_%s skip", SanitisePunctuation(LocalMedia->Category[i].Marker)); // TODO(matt): Bulletproof this?
}
else
{
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)
{
// TODO(matt): Rebuild cache option
char QuoteCacheDir[256];
CopyString(QuoteCacheDir, "%s/quotes", Config.CacheDir);
char QuoteCachePath[256];
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;
}
#if DEBUG_MEM
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, " Allocated QuoteStaging (%d)\n", QuoteStaging.Size);
fclose(MemLog);
printf(" Allocated QuoteStaging (%d)\n", QuoteStaging.Size);
#endif
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)
{
for(int i = 0; i < ArrayCount(CategoryMedium); ++i)
{
if(!StringsDiffer(Topic, CategoryMedium[i].Medium))
{
return RC_NOOP;
}
}
file_buffer Topics;
Topics.Buffer.ID = "Topics";
if(StringsDiffer(Config.CSSDir, ""))
{
CopyString(Topics.Path, "%s/%s/cinera_topics.css", Config.RootDir, Config.CSSDir);
}
else
{
CopyString(Topics.Path, "%s/cinera_topics.css", Config.RootDir);
}
if((Topics.Handle = fopen(Topics.Path, "a+")))
{
fseek(Topics.Handle, 0, SEEK_END);
Topics.FileSize = ftell(Topics.Handle);
Topics.Buffer.Size = Topics.FileSize;
fseek(Topics.Handle, 0, SEEK_SET);
if(!(Topics.Buffer.Location = malloc(Topics.Buffer.Size)))
{
return RC_ERROR_MEMORY;
}
#if DEBUG_MEM
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, " Allocated Topics (%d)\n", Topics.Size);
fclose(MemLog);
printf(" Allocated Topics (%d)\n", Topics.Size);
#endif
Topics.Buffer.Ptr = Topics.Buffer.Location;
fread(Topics.Buffer.Location, Topics.Buffer.Size, 1, Topics.Handle);
while(Topics.Buffer.Ptr - Topics.Buffer.Location < Topics.Buffer.Size)
{
Topics.Buffer.Ptr += StringLength(".category.");
if(!StringsDifferT(SanitisePunctuation(Topic), Topics.Buffer.Ptr, ' '))
{
FreeBuffer(&Topics.Buffer);
fclose(Topics.Handle);
return RC_NOOP;
}
while(Topics.Buffer.Ptr - Topics.Buffer.Location < Topics.Buffer.Size && *Topics.Buffer.Ptr != '\n')
{
++Topics.Buffer.Ptr;
}
++Topics.Buffer.Ptr;
}
hsl_colour Colour;
StringToColourHash(&Colour, Topic);
fprintf(Topics.Handle, ".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(Topics.Handle);
FreeBuffer(&Topics.Buffer);
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"
" Paths: \n"
" -r <root directory>\n"
" Override default root directory (\"%s\")\n"
" -u <root URL>\n"
" Override default root URL (\"%s\")\n"
" -b <base output directory>\n"
" Override project's default base output directory (\"%s\")\n"
" -c <CSS directory path>\n"
" Override default CSS directory (\"%s\"), relative to root\n"
" -i <images directory path>\n"
" Override default images directory (\"%s\"), relative to root\n"
" -j <JS directory path>\n"
" Override default JS directory (\"%s\"), relative to root\n"
" -t <player template location>\n"
" Override default player template location (\"%s\"), relative to root\n"
" and automatically enable integration\n"
" -x <index template location>\n"
" Override default index template location (\"%s\"), relative to root\n"
" and automatically enable integration\n"
"\n"
" -o <output location>\n"
" Override default output player location for SINGLE_EDITION (\"%s\")\n"
" -d <project directory>\n"
" Override default project directory (\"%s\")\n"
"\n"
" -f\n"
" Force integration with an incomplete template\n"
" -p <project ID>\n"
" Set the project ID, corresponding to the \"project\" field in the HMML files\n"
" -s <style>\n"
" Set the style / theme, corresponding to a cinera__*.css file\n"
" This is equal to the \"project\" field in the HMML files by default\n"
" -l <n>\n"
" Override default log level (%d), where n is from 0 (terse) to 7 (verbose)\n"
" -m <default medium>\n"
" Override default default medium (\"%s\")\n"
" -U <seconds>\n"
" Override default update interval (\"%d\")\n"
//" -c config location\n"
" -v\n"
" display version and exit\n"
" -h\n"
" display this help\n"
"\n"
"Environment Variables:\n"
" CINERA_MODE\n"
" =INTEGRATE\n"
" Enable integration\n"
"\n"
"Template:\n"
" A complete index template shall contain exactly one each of the following tags:\n"
" <!-- __CINERA_INCLUDES__ --> - to put inside your own <head></head>\n"
" <!-- __CINERA_INDEX__ -->\n"
"\n"
" A complete player template shall contain exactly one each of the following tags:\n"
" <!-- __CINERA_INCLUDES__ --> - to put inside your own <head></head>\n"
" <!-- __CINERA_MENUS__ -->\n"
" <!-- __CINERA_PLAYER__ -->\n"
" <!-- __CINERA_SCRIPT__ --> (must come after <!-- __CINERA_PLAYER__ -->)\n"
"\n"
" Other available tags:\n"
" <!-- __CINERA_PROJECT__ -->\n"
" <!-- __CINERA_TITLE__ -->\n"
"\n"
"HMML Specification:\n"
" https://git.handmade.network/Annotation-Pushers/Annotation-System/wikis/hmmlspec\n",
BinaryLocation, DefaultConfig->RootDir, DefaultConfig->RootURL, DefaultConfig->BaseDir, DefaultConfig->CSSDir, DefaultConfig->ImagesDir, DefaultConfig->JSDir, DefaultConfig->TemplatePlayerLocation, DefaultConfig->TemplateIndexLocation, DefaultConfig->OutLocation, DefaultConfig->ProjectDir, DefaultConfig->LogLevel, DefaultConfig->DefaultMedium, DefaultConfig->UpdateInterval);
}
void
DepartComment(buffer *Template)
{
while(Template->Ptr - Template->Location < Template->Size)
{
if(!StringsDifferT("-->", Template->Ptr, 0))
{
Template->Ptr += StringLength("-->");
break;
}
++Template->Ptr;
}
}
int
ValidateTemplate(buffer *Errors, template **Template, int PageType)
{
if(ClaimTemplate(Template,
PageType == PAGE_INDEX ? Config.TemplateIndexLocation : Config.TemplatePlayerLocation) == RC_ARENA_FULL){ return RC_ARENA_FULL; };
RewindBuffer(Errors);
bool HaveErrors = FALSE;
bool FoundIncludes = FALSE;
bool FoundMenus = FALSE;
bool FoundPlayer = FALSE;
bool FoundScript = FALSE;
bool FoundIndex = FALSE;
char *Previous = (*Template)->Buffer.Location;
while((*Template)->Buffer.Ptr - (*Template)->Buffer.Location < (*Template)->Buffer.Size)
{
Here:
if(*(*Template)->Buffer.Ptr == '!' && ((*Template)->Buffer.Ptr > (*Template)->Buffer.Location && !StringsDifferT("<!--", &(*Template)->Buffer.Ptr[-1], 0)))
{
char *CommentStart = &(*Template)->Buffer.Ptr[-1];
while((*Template)->Buffer.Ptr - (*Template)->Buffer.Location < (*Template)->Buffer.Size && StringsDifferT("-->", (*Template)->Buffer.Ptr, 0))
{
for(int i = 0; i < ArrayCount(Tags); ++i)
{
if(!(StringsDifferT(Tags[i].Tag, (*Template)->Buffer.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:
(*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].Offset = CommentStart - Previous;
(*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].TagCode = TAG_INDEX;
(*Template)->Metadata.TagCount++;
DepartComment(&(*Template)->Buffer);
Previous = (*Template)->Buffer.Ptr;
FoundIndex = TRUE;
goto Here;
case TAG_INCLUDES:
if(!Config.ForceIntegration && FoundIncludes == TRUE)
{
CopyStringToBuffer(Errors, "(*Template) contains more than one <!-- %s --> tag\n", Tags[i].Tag);
HaveErrors = TRUE;
}
(*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].Offset = CommentStart - Previous;
(*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].TagCode = TAG_INCLUDES;
(*Template)->Metadata.TagCount++;
DepartComment(&(*Template)->Buffer);
Previous = (*Template)->Buffer.Ptr;
FoundIncludes = TRUE;
goto Here;
case TAG_MENUS:
if(!Config.ForceIntegration && FoundMenus == TRUE)
{
CopyStringToBuffer(Errors, "(*Template) contains more than one <!-- %s --> tag\n", Tags[i].Tag);
HaveErrors = TRUE;
}
(*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].Offset = CommentStart - Previous;
(*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].TagCode = TAG_MENUS;
(*Template)->Metadata.TagCount++;
DepartComment(&(*Template)->Buffer);
Previous = (*Template)->Buffer.Ptr;
FoundMenus = TRUE;
goto Here;
case TAG_PLAYER:
if(!Config.ForceIntegration && FoundPlayer == TRUE)
{
CopyStringToBuffer(Errors, "(*Template) contains more than one <!-- %s --> tag\n", Tags[i].Tag);
HaveErrors = TRUE;
}
(*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].Offset = CommentStart - Previous;
(*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].TagCode = TAG_PLAYER;
(*Template)->Metadata.TagCount++;
DepartComment(&(*Template)->Buffer);
Previous = (*Template)->Buffer.Ptr;
FoundPlayer = TRUE;
goto Here;
case TAG_SCRIPT:
if(!Config.ForceIntegration && FoundPlayer == FALSE)
{
CopyStringToBuffer(Errors, "<!-- %s --> must come after <!-- __CINERA_PLAYER__ -->\n", Tags[i].Tag);
HaveErrors = TRUE;
}
if(!Config.ForceIntegration && FoundScript == TRUE)
{
CopyStringToBuffer(Errors, "(*Template) contains more than one <!-- %s --> tag\n", Tags[i].Tag);
HaveErrors = TRUE;
}
(*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].Offset = CommentStart - Previous;
(*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].TagCode = TAG_SCRIPT;
(*Template)->Metadata.TagCount++;
DepartComment(&(*Template)->Buffer);
Previous = (*Template)->Buffer.Ptr;
FoundScript = TRUE;
goto Here;
case TAG_PROJECT:
(*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].Offset = CommentStart - Previous;
(*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].TagCode = TAG_PROJECT;
(*Template)->Metadata.TagCount++;
DepartComment(&(*Template)->Buffer);
Previous = (*Template)->Buffer.Ptr;
goto Here;
case TAG_TITLE:
(*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].Offset = CommentStart - Previous;
(*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].TagCode = TAG_TITLE;
(*Template)->Metadata.TagCount++;
DepartComment(&(*Template)->Buffer);
Previous = (*Template)->Buffer.Ptr;
goto Here;
};
}
}
++(*Template)->Buffer.Ptr;
}
}
else
{
++(*Template)->Buffer.Ptr;
}
}
if(FoundIndex)
{
(*Template)->Metadata.Validity |= PAGE_INDEX;
}
if(!HaveErrors && FoundIncludes && FoundMenus && FoundPlayer && FoundScript)
{
(*Template)->Metadata.Validity |= PAGE_PLAYER;
}
if(!Config.ForceIntegration)
{
if(PageType == PAGE_INDEX && !((*Template)->Metadata.Validity & PAGE_INDEX))
{
CopyStringToBuffer(Errors, "Index template %s must include one <!-- __CINERA_INDEX__ --> tag\n", (*Template)->Metadata.Filename);
fprintf(stderr, "%s", Errors->Location);
return RC_INVALID_TEMPLATE;
}
else if(PageType == PAGE_PLAYER && !((*Template)->Metadata.Validity & PAGE_PLAYER))
{
if(!FoundIncludes){ CopyStringToBuffer(Errors, "Player template %s must include one <!-- __CINERA_INCLUDES__ --> tag\n", (*Template)->Metadata.Filename); };
if(!FoundMenus){ CopyStringToBuffer(Errors, "Player template %s must include one <!-- __CINERA_MENUS__ --> tag\n", (*Template)->Metadata.Filename); };
if(!FoundPlayer){ CopyStringToBuffer(Errors, "Player template %s must include one <!-- __CINERA_PLAYER__ --> tag\n", (*Template)->Metadata.Filename); };
if(!FoundScript){ CopyStringToBuffer(Errors, "Player template %s must include one <!-- __CINERA_SCRIPT__ --> tag\n", (*Template)->Metadata.Filename); };
fprintf(stderr, "%s", Errors->Location);
return RC_INVALID_TEMPLATE;
}
}
return RC_SUCCESS;
}
int
HMMLToBuffers(buffers *CollationBuffers, char *Filename)
{
RewindBuffer(&CollationBuffers->IncludesPlayer);
RewindBuffer(&CollationBuffers->Menus);
RewindBuffer(&CollationBuffers->Player);
RewindBuffer(&CollationBuffers->ScriptPlayer);
RewindBuffer(&CollationBuffers->IncludesIndex);
RewindBuffer(&CollationBuffers->ScriptIndex);
char Filepath[256];
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(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);
int ProjectIndex;
for(ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex)
{
if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID))
{
CopyString(CollationBuffers->ProjectName, ProjectInfo[ProjectIndex].FullName);
break;
}
}
#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(&QuoteMenu, "QuoteMenu", Kilobytes(32)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
if(ClaimBuffer(&ReferenceMenu, "ReferenceMenu", Kilobytes(32)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
if(ClaimBuffer(&FilterMenu, "FilterMenu", Kilobytes(16)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
if(ClaimBuffer(&FilterTopics, "FilterTopics", Kilobytes(8)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
if(ClaimBuffer(&FilterMedia, "FilterMedia", Kilobytes(8)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
if(ClaimBuffer(&CreditsMenu, "CreditsMenu", Kilobytes(8)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
if(ClaimBuffer(&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,
" <div class=\"cineraMenus %s\">\n"
" <span class=\"episode_name\">", StringsDiffer(Config.Theme, "") ? Config.Theme : HMML.metadata.project);
CopyStringToBufferHTMLSafe(&CollationBuffers->Menus, HMML.metadata.title);
CopyStringToBuffer(&CollationBuffers->Menus, "</span>\n"
" <span id=\"focus-warn\">⚠ Click here to regain focus ⚠</span>\n");
CopyStringToBuffer(&CollationBuffers->Player,
" <div class=\"cineraPlayerContainer\">\n"
" <div class=\"video_container\" data-videoId=\"%s\"></div>\n"
" <div class=\"markers_container %s\">\n", HMML.metadata.id, StringsDiffer(Config.Theme, "") ? Config.Theme : HMML.metadata.project);
int CreditsErrorCode = BuildCredits(&CreditsMenu, &HasCreditsMenu, &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
RewindBuffer(&CollationBuffers->Search);
char *FilenamePtr = Filename;
FilenamePtr += StringLength(Filename) - StringLength(".hmml");
*FilenamePtr = '\0';
CopyStringToBuffer(&CollationBuffers->Search, "name: \"%s\"\n"
"title: \"", Filename);
*FilenamePtr = '.';
CopyStringToBufferJSONSafe(&CollationBuffers->Search, HMML.metadata.title);
CopyStringToBuffer(&CollationBuffers->Search, "\"\n"
"markers:\n");
for(int AnnotationIndex = 0; AnnotationIndex < HMML.annotation_count; ++AnnotationIndex)
{
#if DEBUG
printf("%d\n", AnnotationIndex);
#endif
HMML_Annotation *Anno = HMML.annotations + AnnotationIndex;
CopyStringToBuffer(&CollationBuffers->Search, "\"%d\": \"", TimecodeToSeconds(Anno->time));
CopyStringToBufferJSONSafe(&CollationBuffers->Search, Anno->text);
CopyStringToBuffer(&CollationBuffers->Search, "\"\n");
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(&Annotation, "Annotation", Kilobytes(8)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
if(ClaimBuffer(&AnnotationHeader, "AnnotationHeader", 512) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
if(ClaimBuffer(&AnnotationClass, "AnnotationClass", 256) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
if(ClaimBuffer(&AnnotationData, "AnnotationData", 512) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
if(ClaimBuffer(&Text, "Text", Kilobytes(4)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
if(ClaimBuffer(&TopicDots, "TopicDots", 512) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
CopyStringToBuffer(&AnnotationHeader,
" <div data-timestamp=\"%d\"",
TimecodeToSeconds(Anno->time));
CopyStringToBuffer(&AnnotationClass,
" class=\"marker");
if(Anno->author)
{
if(!HasFilterMenu)
{
HasFilterMenu = TRUE;
}
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, "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,
"<a class=\"author\" href=\"https://handmade.network/m/%s\" target=\"blank\" style=\"color: hsl(%d, %d%%, %d%%); text-decoration: none\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</a> ",
Anno->author,
AuthorColour.Hue, AuthorColour.Saturation, AuthorColour.Lightness,
AuthorColour.Hue, AuthorColour.Saturation,
Anno->author);
}
else
{
CopyStringToBuffer(&Text,
"<span class=\"author\" style=\"color: hsl(%d, %d%%, %d%%);\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</span> ",
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,
"<a class=\"member\" href=\"https://handmade.network/m/%s\" target=\"blank\" style=\"color: hsl(%d, %d%%, %d%%); text-decoration: none\" data-hue=\"%d\" data-saturation=\"%d%%\">%.*s</a>",
Anno->markers[MarkerIndex].marker,
MemberColour.Hue, MemberColour.Saturation, MemberColour.Lightness,
MemberColour.Hue, MemberColour.Saturation,
StringLength(Readable), InPtr);
}
else
{
CopyStringToBuffer(&Text,
"<span class=\"member\" style=\"color: hsl(%d, %d%%, %d%%);\" data-hue=\"%d\" data-saturation=\"%d%%\">%.*s</span>",
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,
"<a class=\"project\" href=\"https://%s.handmade.network/\" target=\"blank\" style=\"color: hsl(%d, %d%%, %d%%); text-decoration: none\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</a>",
Anno->markers[MarkerIndex].marker,
ProjectColour.Hue, ProjectColour.Saturation, ProjectColour.Lightness,
ProjectColour.Hue, ProjectColour.Saturation,
Readable);
}
else
{
CopyStringToBuffer(&Text,
"<span class=\"project\" style=\"color: hsl(%d, %d%%, %d%%);\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</span>",
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))
{
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, &LocalTopics, &Media, &LocalMedia, Anno->markers[MarkerIndex].marker);
}
}
while(RefIndex < Anno->reference_count &&
InPtr - Anno->text == Anno->references[RefIndex].offset)
{
HMML_Reference *CurrentRef = Anno->references + RefIndex;
if(!HasReferenceMenu)
{
CopyStringToBuffer(&ReferenceMenu,
" <div class=\"menu references\">\n"
" <span>References &#9660;</span>\n"
" <div class=\"mouse_catcher\"></div>\n"
" <div class=\"refs references_container\">\n");
if(BuildReference(ReferencesArray, RefIdentifier, UniqueRefs, CurrentRef, Anno) == 1)
{
LogError(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(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(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(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(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(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, "<sup>,%d</sup>", RefIdentifier);
}
else
{
CopyStringToBuffer(&Text, "<sup>%d</sup>", RefIdentifier);
}
++RefIndex;
++RefIdentifier;
}
if(*InPtr)
{
switch(*InPtr)
{
case '<':
CopyStringToBuffer(&Text, "&lt;");
InPtr++;
break;
case '>':
CopyStringToBuffer(&Text, "&gt;");
InPtr++;
break;
case '&':
CopyStringToBuffer(&Text, "&amp;");
InPtr++;
break;
case '\"':
CopyStringToBuffer(&Text, "&quot;");
InPtr++;
break;
case '\'':
CopyStringToBuffer(&Text, "&#39;");
InPtr++;
break;
default:
*Text.Ptr++ = *InPtr++;
*Text.Ptr = '\0';
break;
}
}
}
if(Anno->is_quote)
{
if(!HasQuoteMenu)
{
CopyStringToBuffer(&QuoteMenu,
" <div class=\"menu quotes\">\n"
" <span>Quotes &#9660;</span>\n"
" <div class=\"mouse_catcher\"></div>\n"
" <div class=\"refs quotes_container\">\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) == 1)
{
LogError(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,
" <a target=\"_blank\" class=\"ref\" href=\"https://dev.abaines.me.uk/quotes/%s/%d\">\n"
" <span data-id=\"&#%d;\">\n"
" <span class=\"ref_content\">\n"
" <div class=\"source\">Quote %d</div>\n"
" <div class=\"ref_title\">",
Speaker,
Anno->quote.id,
QuoteIdentifier,
Anno->quote.id);
CopyStringToBufferHTMLSafe(&QuoteMenu, QuoteInfo.Text);
CopyStringToBuffer(&QuoteMenu, "</div>\n"
" <div class=\"quote_byline\">&mdash;%s, %s</div>\n"
" </span>\n"
" <div class=\"ref_indices\">\n"
" <span data-timestamp=\"%d\" class=\"timecode\"><span class=\"ref_index\">[&#%d;]</span><span class=\"time\">%s</span></span>\n"
" </div>\n"
" </span>\n"
" </a>\n",
Speaker,
QuoteInfo.Date,
TimecodeToSeconds(Anno->time),
QuoteIdentifier,
Anno->time);
if(!Anno->text[0])
{
CopyStringToBuffer(&Text, "&#8220;");
CopyStringToBufferHTMLSafe(&Text, QuoteInfo.Text);
CopyStringToBuffer(&Text, "&#8221;");
}
CopyStringToBuffer(&Text, "<sup>&#%d;</sup>", QuoteIdentifier);
++QuoteIdentifier;
}
while(MarkerIndex < Anno->marker_count)
{
switch(GenerateTopicColours(Anno->markers[MarkerIndex].marker))
{
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, &LocalTopics, &Media, &LocalMedia, Anno->markers[MarkerIndex].marker);
++MarkerIndex;
}
if(LocalMedia.Count == 0)
{
InsertCategory(&Topics, &LocalTopics, &Media, &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,
" <div class=\"cineraContent\"><span class=\"timecode\">%s</span>",
Anno->time);
CopyBuffer(&Annotation, &Text);
if(LocalTopics.Count > 0)
{
CopyBuffer(&Annotation, &TopicDots);
}
CopyStringToBuffer(&Annotation, "</div>\n"
" <div class=\"progress faded\">\n"
" <div class=\"cineraContent\"><span class=\"timecode\">%s</span>",
Anno->time);
CopyBuffer(&Annotation, &Text);
if(LocalTopics.Count > 0)
{
CopyBuffer(&Annotation, &TopicDots);
}
CopyStringToBuffer(&Annotation, "</div>\n"
" </div>\n"
" <div class=\"progress main\">\n"
" <div class=\"cineraContent\"><span class=\"timecode\">%s</span>",
Anno->time);
CopyBuffer(&Annotation, &Text);
if(LocalTopics.Count > 0)
{
CopyBuffer(&Annotation, &TopicDots);
}
CopyStringToBuffer(&Annotation, "</div>\n"
" </div>\n"
" </div>\n");
CopyBuffer(&CollationBuffers->Player, &Annotation);
// NOTE(matt): Tree structure of "annotation local" buffer dependencies
// TopicDots
// Text
// AnnotationData
// AnnotationClass
// AnnotationHeader
// Annotation
DeclaimBuffer(&TopicDots);
DeclaimBuffer(&Text);
DeclaimBuffer(&AnnotationData);
DeclaimBuffer(&AnnotationClass);
DeclaimBuffer(&AnnotationHeader);
DeclaimBuffer(&Annotation);
}
CopyStringToBuffer(&CollationBuffers->Search, "---\n");
#if DEBUG
printf("\n\n --- End of Annotations Loop ---\n\n\n\n");
#endif
if(HasQuoteMenu)
{
CopyStringToBuffer(&QuoteMenu,
" </div>\n"
" </div>\n");
CopyBuffer(&CollationBuffers->Menus, &QuoteMenu);
}
if(HasReferenceMenu)
{
for(int i = 0; i < UniqueRefs; ++i)
{
CopyStringToBuffer(&ReferenceMenu,
" <a data-id=\"%s\" href=\"%s\" target=\"_blank\" class=\"ref\">\n"
" <span class=\"ref_content\">\n",
ReferencesArray[i].ID,
ReferencesArray[i].URL);
if(*ReferencesArray[i].Source)
{
CopyStringToBuffer(&ReferenceMenu,
" <div class=\"source\">");
CopyStringToBufferHTMLSafe(&ReferenceMenu, ReferencesArray[i].Source);
CopyStringToBuffer(&ReferenceMenu, "</div>\n"
" <div class=\"ref_title\">");
CopyStringToBufferHTMLSafe(&ReferenceMenu, ReferencesArray[i].RefTitle);
CopyStringToBuffer(&ReferenceMenu, "</div>\n");
}
else
{
CopyStringToBuffer(&ReferenceMenu,
" <div class=\"ref_title\">");
CopyStringToBufferHTMLSafe(&ReferenceMenu, ReferencesArray[i].RefTitle);
CopyStringToBuffer(&ReferenceMenu, "</div>\n");
}
CopyStringToBuffer(&ReferenceMenu,
" </span>\n");
for(int j = 0; j < ReferencesArray[i].IdentifierCount;)
{
CopyStringToBuffer(&ReferenceMenu,
" <div class=\"ref_indices\">\n ");
for(int k = 0; k < 3 && j < ReferencesArray[i].IdentifierCount; ++k, ++j)
{
CopyStringToBuffer(&ReferenceMenu,
"<span data-timestamp=\"%d\" class=\"timecode\"><span class=\"ref_index\">[%d]</span><span class=\"time\">%s</span></span>",
TimecodeToSeconds(ReferencesArray[i].Identifier[j].Timecode),
ReferencesArray[i].Identifier[j].Identifier,
ReferencesArray[i].Identifier[j].Timecode);
}
CopyStringToBuffer(&ReferenceMenu, "\n"
" </div>\n");
}
CopyStringToBuffer(&ReferenceMenu,
" </a>\n");
}
CopyStringToBuffer(&ReferenceMenu,
" </div>\n"
" </div>\n");
CopyBuffer(&CollationBuffers->Menus, &ReferenceMenu);
}
if(HasFilterMenu)
{
CopyStringToBuffer(&FilterState, "<script>\n"
" var filterInitState = {\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)
{
if(!StringsDiffer(Media.Category[i].Marker, "afk")) // TODO(matt): Make this configurable?
{
CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": true },\n",
Media.Category[i].Marker, "medium");
}
else
{
CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": false },\n",
Media.Category[i].Marker, "medium");
}
}
CopyStringToBuffer(&FilterState,
" };\n"
"\n"
" 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)
{
if(!StringsDiffer(Media.Category[i].Marker, "afk")) // TODO(matt): Make this configurable?
{
CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": true },\n",
Media.Category[i].Marker, "medium");
}
else
{
CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": false },\n",
Media.Category[i].Marker, "medium");
}
}
CopyStringToBuffer(&FilterState,
" };\n"
" </script>\n");
buffer URLPrefix;
ConstructURLPrefix(&URLPrefix, INCLUDE_Images, PAGE_INDEX);
CopyStringToBuffer(&FilterMenu,
" <div class=\"menu filter\">\n"
" <span><img src=\"%scinera_icon_filter.png\"></span>\n"
" <div class=\"filter_container\">\n"
" <div class=\"filter_mode inclusive\">Filter mode: </div>\n"
" <div class=\"filters\">\n",
URLPrefix.Location);
DeclaimBuffer(&URLPrefix);
if(Topics.Count > 0)
{
CopyStringToBuffer(&FilterMenu,
" <div class=\"filter_topics\">\n"
" <div class=\"filter_title\">Topics</div>\n");
for(int i = 0; i < Topics.Count; ++i)
{
CopyStringToBuffer(&FilterTopics,
" <div class=\"filter_content %s\">\n"
" <span class=\"icon category %s\"></span><span class=\"text\">%s</span>\n"
" </div>\n",
Topics.Category[i].Marker,
Topics.Category[i].Marker,
Topics.Category[i].Marker);
}
CopyStringToBuffer(&FilterTopics,
" </div>\n");
CopyBuffer(&FilterMenu, &FilterTopics);
}
if(Media.Count > 0)
{
CopyStringToBuffer(&FilterMedia,
" <div class=\"filter_media\">\n"
" <div class=\"filter_title\">Media</div>\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;
}
}
if(!StringsDiffer(Media.Category[i].Marker, "afk")) // TODO(matt): Initially hidden config
{
CopyStringToBuffer(&FilterMedia,
" <div class=\"filter_content %s off\">\n"
" <span class=\"icon\">%s</span><span class=\"text\">%s</span>\n"
" </div>\n",
Media.Category[i].Marker,
CategoryMedium[j].Icon,
CategoryMedium[j].WrittenName
);
}
else
{
CopyStringToBuffer(&FilterMedia,
" <div class=\"filter_content %s\">\n"
" <span class=\"icon\">%s</span><span class=\"text\">%s</span>\n"
" </div>\n",
Media.Category[i].Marker,
CategoryMedium[j].Icon,
CategoryMedium[j].WrittenName
);
}
}
CopyStringToBuffer(&FilterMedia,
" </div>\n");
CopyBuffer(&FilterMenu, &FilterMedia);
}
CopyStringToBuffer(&FilterMenu,
" </div>\n"
" </div>\n"
" </div>\n");
CopyBuffer(&CollationBuffers->Menus, &FilterMenu);
}
if(HasCreditsMenu)
{
CopyBuffer(&CollationBuffers->Menus, &CreditsMenu);
}
CopyStringToBuffer(&CollationBuffers->Menus,
" <div class=\"help\">\n"
" <span>?</span>\n"
" <div class=\"help_container\">\n"
" <span class=\"help_key\">?</span><h1>Keyboard Navigation</h1>\n"
"\n"
" <h2>Global Keys</h2>\n"
" <span class=\"help_key\">W</span>, <span class=\"help_key\">A</span>, <span class=\"help_key\">P</span> / <span class=\"help_key\">S</span>, <span class=\"help_key\">D</span>, <span class=\"help_key\">N</span> <span class=\"help_text\">Jump to previous / next marker</span><br>\n");
if(HasFilterMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <span class=\"help_key\">z</span> <span class=\"help_text\">Toggle filter mode</span> <span class=\"help_key\">V</span> <span class=\"help_text\">Revert filter to original state</span>\n");
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <span class=\"help_key unavailable\">z</span> <span class=\"help_text unavailable\">Toggle filter mode</span> <span class=\"help_key unavailable\">V</span> <span class=\"help_text unavailable\">Revert filter to original state</span>\n");
}
CopyStringToBuffer(&CollationBuffers->Menus,
"\n"
" <h2>Menu toggling</h2>\n");
if(HasQuoteMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <span class=\"help_key\">q</span> <span class=\"help_text\">Quotes</span>\n");
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <span class=\"help_key unavailable\">q</span> <span class=\"help_text unavailable\">Quotes</span>\n");
}
if(HasReferenceMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <span class=\"help_key\">r</span> <span class=\"help_text\">References</span>\n");
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <span class=\"help_key unavailable\">r</span> <span class=\"help_text unavailable\">References</span>\n");
}
if(HasFilterMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <span class=\"help_key\">f</span> <span class=\"help_text\">Filter</span>\n");
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <span class=\"help_key unavailable\">f</span> <span class=\"help_text unavailable\">Filter</span>\n");
}
if(HasCreditsMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <span class=\"help_key\">c</span> <span class=\"help_text\">Credits</span>\n");
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <span class=\"help_key unavailable\">c</span> <span class=\"help_text unavailable\">Credits</span>\n");
}
CopyStringToBuffer(&CollationBuffers->Menus,
"\n"
" <h2>Movement</h2>\n"
" <div class=\"help_paragraph\">\n"
" <div class=\"key_block\">\n"
" <div class=\"key_column\" style=\"flex-grow: 1\">\n"
" <span class=\"help_key\">a</span>\n"
" </div>\n"
" <div class=\"key_column\" style=\"flex-grow: 2\">\n"
" <span class=\"help_key\">w</span><br>\n"
" <span class=\"help_key\">s</span>\n"
" </div>\n"
" <div class=\"key_column\" style=\"flex-grow: 1\">\n"
" <span class=\"help_key\">d</span>\n"
" </div>\n"
" </div>\n"
" <div class=\"key_block\">\n"
" <span class=\"help_key\">h</span>\n"
" <span class=\"help_key\">j</span>\n"
" <span class=\"help_key\">k</span>\n"
" <span class=\"help_key\">l</span>\n"
" </div>\n"
" <div class=\"key_block\">\n"
" <div style=\"flex-grow: 1\">\n"
" <span class=\"help_key\">←</span>\n"
" </div>\n"
" <div style=\"flex-grow: 2\">\n"
" <span class=\"help_key\">↑</span><br>\n"
" <span class=\"help_key\">↓</span>\n"
" </div>\n"
" <div style=\"flex-grow: 1\">\n"
" <span class=\"help_key\">→</span>\n"
" </div>\n"
" </div>\n"
" </div>\n"
" <br>\n");
if(HasQuoteMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <h2>Quotes ");
if(HasReferenceMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus, "and References Menus</h2>\n");
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus, "<span class=\"unavailable\">and References</span> Menus</h2>\n");
}
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <h2><span class=\"unavailable\">Quotes");
if(HasReferenceMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus, " and</span> References Menus</h2>\n");
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus, " and References Menus</span></h2>\n");
}
}
if(HasQuoteMenu || HasReferenceMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <span style=\"width: auto\" class=\"help_key\">Enter</span> <span class=\"help_text\">Jump to timecode</span><br>\n");
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <span style=\"width: auto\" class=\"help_key unavailable\">Enter</span> <span class=\"help_text unavailable\">Jump to timecode</span><br>\n");
}
CopyStringToBuffer(&CollationBuffers->Menus, "\n");
if(HasQuoteMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <h2>Quotes");
if(HasReferenceMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus, ", References ");
if(HasCreditsMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus, "and Credits Menus</h2>");
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus, "<span class=\"unavailable\">and Credits</span> Menus</h2>");
}
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus, "<span class=\"unavailable\">, References ");
if(HasCreditsMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus, "</span>and Credits Menus</h2>");
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus, "and Credits</span> Menus</h2>");
}
}
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <h2><span class=\"unavailable\">Quotes");
if(HasReferenceMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus, ", </span>References ");
if(HasCreditsMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus, "and Credits Menus</h2>");
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus, "<span class=\"unavailable\">and Credits</span> Menus</h2>");
}
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus, ", References ");
if(HasCreditsMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus, "</span>and Credits Menus</h2>");
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus, "and Credits Menus</h2></span>");
}
}
}
CopyStringToBuffer(&CollationBuffers->Menus, "\n");
if(HasQuoteMenu || HasReferenceMenu || HasCreditsMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <span class=\"help_key\">o</span> <span class=\"help_text\">Open URL (in new tab)</span>\n");
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <span class=\"help_key unavailable\">o</span> <span class=\"help_text unavailable\">Open URL (in new tab)</span>\n");
}
CopyStringToBuffer(&CollationBuffers->Menus,
"\n");
if(HasFilterMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <h2>Filter Menu</h2>\n"
" <span class=\"help_key\">x</span>, <span style=\"width: auto\" class=\"help_key\">Space</span> <span class=\"help_text\">Toggle category and focus next</span><br>\n"
" <span class=\"help_key\">X</span>, <span style=\"width: auto; margin-right: 0px\" class=\"help_key\">Shift</span><span style=\"width: auto\" class=\"help_key\">Space</span> <span class=\"help_text\">Toggle category and focus previous</span><br>\n"
" <span class=\"help_key\">v</span> <span class=\"help_text\">Invert topics / media as per focus</span>\n");
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <h2><span class=\"unavailable\">Filter Menu</span></h2>\n"
" <span class=\"help_key unavailable\">x</span>, <span style=\"width: auto\" class=\"help_key unavailable\">Space</span> <span class=\"help_text unavailable\">Toggle category and focus next</span><br>\n"
" <span class=\"help_key unavailable\">X</span>, <span style=\"width: auto; margin-right: 0px\" class=\"help_key unavailable\">Shift</span><span style=\"width: auto\" class=\"help_key unavailable\">Space</span> <span class=\"help_text unavailable\">Toggle category and focus previous</span><br>\n"
" <span class=\"help_key unavailable\">v</span> <span class=\"help_text unavailable\">Invert topics / media as per focus</span>\n");
}
CopyStringToBuffer(&CollationBuffers->Menus, "\n");
if(HasCreditsMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <h2>Credits Menu</h2>\n"
" <span style=\"width: auto\" class=\"help_key\">Enter</span> <span class=\"help_text\">Open URL (in new tab)</span><br>\n");
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <h2><span class=\"unavailable\">Credits Menu</span></h2>\n"
" <span style=\"width: auto\" class=\"help_key unavailable\">Enter</span> <span class=\"help_text unavailable\">Open URL (in new tab)</span><br>\n");
}
CopyStringToBuffer(&CollationBuffers->Menus,
" </div>\n"
" </div>\n"
" </div>");
CopyStringToBuffer(&CollationBuffers->Player,
" </div>\n"
" </div>");
// TODO(matt): Maybe do something about indentation levels
buffer URLPrefix;
ConstructURLPrefix(&URLPrefix, INCLUDE_CSS, PAGE_INDEX);
CopyStringToBuffer(&CollationBuffers->IncludesIndex,
"<link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera.css\">\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera__%s.css\">\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera_topics.css\">\n"
" <style>body { overflow-y: scroll; }</style>\n"
"\n"
" <meta charset=\"UTF-8\">\n"
" <meta name=\"generator\" content=\"Cinera %d.%d.%d\">\n",
URLPrefix.Location,
URLPrefix.Location, StringsDiffer(Config.Theme, "") ? Config.Theme : HMML.metadata.project,
URLPrefix.Location,
CINERA_APP_VERSION.Major,
CINERA_APP_VERSION.Minor,
CINERA_APP_VERSION.Patch);
DeclaimBuffer(&URLPrefix);
ConstructURLPrefix(&URLPrefix, INCLUDE_CSS, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"<link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera.css\">\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera__%s.css\">\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera_topics.css\">\n"
"\n"
" <meta charset=\"UTF-8\">\n"
" <meta name=\"generator\" content=\"Cinera %d.%d.%d\">\n",
URLPrefix.Location,
URLPrefix.Location, StringsDiffer(Config.Theme, "") ? Config.Theme : HMML.metadata.project,
URLPrefix.Location,
CINERA_APP_VERSION.Major,
CINERA_APP_VERSION.Minor,
CINERA_APP_VERSION.Patch);
DeclaimBuffer(&URLPrefix);
if(Topics.Count || Media.Count)
{
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
" <meta name=\"keywords\" content=\"");
if(Topics.Count > 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");
}
ConstructURLPrefix(&URLPrefix, INCLUDE_JS, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
" <script type=\"text/javascript\" src=\"%scinera_player_pre.js\"></script>\n",
URLPrefix.Location);
CopyStringToBuffer(&CollationBuffers->ScriptPlayer,
" <script type=\"text/javascript\" src=\"%scinera_player_post.js\"></script>\n",
URLPrefix.Location);
DeclaimBuffer(&URLPrefix);
if(HasFilterMenu)
{
CopyBuffer(&CollationBuffers->ScriptPlayer, &FilterState);
}
// NOTE(matt): Tree structure of "global" buffer dependencies
// FilterState
// CreditsMenu
// FilterMedia
// FilterTopics
// FilterMenu
// ReferenceMenu
// QuoteMenu
Cleanup:
DeclaimBuffer(&FilterState);
DeclaimBuffer(&CreditsMenu);
DeclaimBuffer(&FilterMedia);
DeclaimBuffer(&FilterTopics);
DeclaimBuffer(&FilterMenu);
DeclaimBuffer(&ReferenceMenu);
DeclaimBuffer(&QuoteMenu);
}
else
{
LogError(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(buffers *CollationBuffers, template *Template, char *OutputPath, int PageType)
{
#if DEBUG
printf("\n\n --- Buffer Collation ---\n"
" %s\n\n\n", OutputPath ? OutputPath : Config->OutLocation);
#endif
#if DEBUG_MEM
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, "\nEntered BuffersToHTML(%s)\n", OutputPath ? OutputPath : Config->OutLocation);
fclose(MemLog);
#endif
if(Config.Mode == MODE_INTEGRATE)
{
if(Template->Metadata.Validity & PageType)
{
buffer Output;
Output.Size = Template->Buffer.Size + (Kilobytes(512));
Output.ID = "Output";
if(!(Output.Location = malloc(Output.Size)))
{
LogError(LOG_ERROR, "BuffersToHTML(): %s",
strerror(errno));
return RC_ERROR_MEMORY;
}
#if DEBUG_MEM
MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, " Allocated Output (%d)\n", Output.Size);
fclose(MemLog);
printf(" Allocated Output (%d)\n", Output.Size);
#endif
Output.Ptr = Output.Location;
Template->Buffer.Ptr = Template->Buffer.Location;
for(int i = 0; i < Template->Metadata.TagCount; ++i)
{
int j = 0;
while(Template->Metadata.Tag[i].Offset > j)
{
*Output.Ptr++ = *Template->Buffer.Ptr++;
++j;
}
switch(Template->Metadata.Tag[i].TagCode)
{
case TAG_TITLE:
CopyStringToBuffer(&Output, CollationBuffers->Title);
break;
case TAG_PROJECT:
CopyStringToBuffer(&Output, CollationBuffers->ProjectName);
break;
case TAG_INDEX:
CopyBuffer(&Output, &CollationBuffers->Index);
break;
case TAG_INCLUDES:
CopyBuffer(&Output, PageType == PAGE_PLAYER ? &CollationBuffers->IncludesPlayer : &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->ScriptPlayer);
break;
}
DepartComment(&Template->Buffer);
}
while(Template->Buffer.Ptr - Template->Buffer.Location < Template->Buffer.Size)
{
*Output.Ptr++ = *Template->Buffer.Ptr++;
}
FILE *OutFile;
if(!(OutFile = fopen(Config.Edition == EDITION_PROJECT ? OutputPath : Config.OutIntegratedLocation, "w")))
{
LogError(LOG_ERROR, "Unable to open output file %s: %s", Config.Edition == EDITION_PROJECT ? OutputPath : Config.OutIntegratedLocation, strerror(errno));
free(Output.Location);
#if DEBUG_MEM
MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, " Freed Output\n");
fclose(MemLog);
printf(" Freed Output\n");
#endif
return RC_ERROR_FILE;
}
fwrite(Output.Location, Output.Ptr - Output.Location, 1, OutFile);
fclose(OutFile);
free(Output.Location);
#if DEBUG_MEM
MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, " Freed Output\n");
fclose(MemLog);
printf(" Freed Output\n");
#endif
return RC_SUCCESS;
}
else
{
return RC_INVALID_TEMPLATE;
}
}
else
{
buffer Master;
if(ClaimBuffer(&Master, "Master", Kilobytes(512)) == RC_ARENA_FULL) { return RC_ARENA_FULL; };
CopyStringToBuffer(&Master,
"<html>\n"
" <head>\n");
CopyBuffer(&Master, PageType == PAGE_PLAYER ? &CollationBuffers->IncludesPlayer : &CollationBuffers->IncludesIndex);
CopyStringToBuffer(&Master, "\n");
CopyStringToBuffer(&Master,
" </head>\n"
" <body>\n");
if(PageType == PAGE_PLAYER)
{
CopyBuffer(&Master, &CollationBuffers->Menus);
CopyStringToBuffer(&Master, "\n");
CopyBuffer(&Master, &CollationBuffers->Player);
CopyStringToBuffer(&Master, "\n");
CopyBuffer(&Master, &CollationBuffers->ScriptPlayer);
CopyStringToBuffer(&Master, "\n");
}
else
{
CopyBuffer(&Master, &CollationBuffers->Index);
}
CopyStringToBuffer(&Master,
" </body>\n"
"</html>\n");
FILE *OutFile;
if(!(OutFile = fopen(Config.Edition == EDITION_PROJECT ? OutputPath : Config.OutLocation, "w")))
{
LogError(LOG_ERROR, "Unable to open output file %s: %s", Config.Edition == EDITION_PROJECT ? OutputPath : Config.OutLocation, strerror(errno));
DeclaimBuffer(&Master);
return RC_ERROR_FILE;
}
fwrite(Master.Location, Master.Ptr - Master.Location, 1, OutFile);
fclose(OutFile);
DeclaimBuffer(&Master);
return RC_SUCCESS;
}
}
int
ReadFileIntoBuffer(file_buffer *File, int BufferPadding)
{
if(!(File->Handle = fopen(File->Path, "r")))
{
return RC_ERROR_FILE;
}
fseek(File->Handle, 0, SEEK_END);
File->FileSize = ftell(File->Handle);
File->Buffer.Size = File->FileSize + 1 + BufferPadding; // NOTE(matt): +1 to accommodate a NULL terminator
fseek(File->Handle, 0, SEEK_SET);
// TODO(matt): Consider using the MemoryArena? Maybe have separate ReadFileIntoMemory() and ReadFileIntoArena()
if(!(File->Buffer.Location = malloc(File->Buffer.Size)))
{
fclose(File->Handle);
return RC_ERROR_MEMORY;
}
File->Buffer.Ptr = File->Buffer.Location;
fread(File->Buffer.Location, File->FileSize, 1, File->Handle);
File->Buffer.Location[File->FileSize] = '\0';
fclose(File->Handle);
return RC_SUCCESS;
}
// NOTE(matt): Currently unused
int
WriteBufferToFile(file_buffer *File, buffer *Buffer, int BytesToWrite, bool KeepFileHandleOpen)
{
if(!(File->Handle = fopen(File->Path, "w")))
{
return RC_ERROR_FILE;
}
fwrite(Buffer->Location, BytesToWrite, 1, File->Handle);
if(!KeepFileHandleOpen)
{
fclose(File->Handle);
}
return RC_SUCCESS;
}
enum
{
C_SEEK_FORWARDS,
C_SEEK_BACKWARDS
} seek_directions;
enum
{
C_SEEK_START, // First character of string
C_SEEK_BEFORE, // Character before first character
C_SEEK_END, // Last character of string
C_SEEK_AFTER // Character after last character
} seek_positions;
int
SeekBufferForString(buffer *Buffer, char *String,
int Direction, /* seek_directions */
int Position /* seek_positions */)
{
// TODO(matt): Optimise? Some means of analysing the String to increment
// the pointer in bigger strides
// Perhaps count up runs of consecutive chars and seek for the char with
// the longest run, in strides of that run-length
char *InitialLocation = Buffer->Ptr;
if(Direction == C_SEEK_FORWARDS)
{
while(Buffer->Ptr - Buffer->Location < Buffer->Size - StringLength(String)
&& StringsDifferT(String, Buffer->Ptr, 0))
{
++Buffer->Ptr;
}
}
else
{
while(Buffer->Ptr > Buffer->Location
&& StringsDifferT(String, Buffer->Ptr, 0))
{
--Buffer->Ptr;
}
}
if(StringsDifferT(String, Buffer->Ptr, 0))
{
Buffer->Ptr = InitialLocation;
return RC_UNFOUND;
}
switch(Position)
{
case C_SEEK_START:
break;
case C_SEEK_BEFORE:
if(Buffer->Ptr > Buffer->Location)
{
--Buffer->Ptr;
break;
}
else
{
return RC_ERROR_SEEK; // Ptr remains at string start
}
case C_SEEK_END:
Buffer->Ptr += StringLength(String) - 1;
break;
case C_SEEK_AFTER:
if(Buffer->Size >= Buffer->Ptr - Buffer->Location + StringLength(String))
{
Buffer->Ptr += StringLength(String);
break;
}
else
{
return RC_ERROR_SEEK; // Ptr remains at string start
// NOTE(matt): Should it, however, be left at the end of the string?
}
}
return RC_SUCCESS;
}
// TODO(matt): Increment CINERA_DB_VERSION!
typedef struct
{
unsigned int DBVersion; // NOTE(matt): Put this first to aid reliability
version AppVersion;
version HMMLVersion;
unsigned int EntryCount;
} index_header;
typedef struct
{
int Size;
char BaseFilename[32];
} index_metadata;
typedef struct
{
file_buffer File;
file_buffer Metadata;
index_header Header;
index_metadata Entry;
} index;
// TODO(matt): Increment CINERA_DB_VERSION!
int
InsertIntoIndex(buffers *CollationBuffers, char *BaseFilename)
{
// NOTE(matt): The index will be stored in two files:
// 1) ProjectID.index
// 2) ProjectID.metadata
//
// .index is all of the stuff needed for the search
// .metadata is: int DBVersion
// version AppVersion
// version HMMLVersion
// int EntryCount
// for each Entry
// int Size from start of "name:" to end of "---\n")
// char BaseFilename[32] (0-padded)
//
index Index = { 0 };
Index.Metadata.Buffer.ID = "IndexMetadata";
CopyString(Index.Metadata.Path, "%s/%s.metadata", Config.BaseDir, Config.ProjectID);
int IndexMetadataFileReadCode = ReadFileIntoBuffer(&Index.Metadata, 0);
switch(IndexMetadataFileReadCode)
{
case RC_ERROR_MEMORY:
return RC_ERROR_MEMORY;
case RC_ERROR_FILE:
case RC_SUCCESS:
break;
}
Index.File.Buffer.ID = "IndexFile";
CopyString(Index.File.Path, "%s/%s.index", Config.BaseDir, Config.ProjectID);
int IndexFileReadCode = ReadFileIntoBuffer(&Index.File, 0);
switch(IndexFileReadCode)
{
case RC_ERROR_MEMORY:
return RC_ERROR_MEMORY;
case RC_ERROR_FILE:
case RC_SUCCESS:
break;
}
int MetadataInsertionOffset = -1;
int IndexEntryInsertionStart = -1;
int IndexEntryInsertionEnd = -1;
Index.Header.EntryCount = 0;
char *IndexEntryStart;
bool Found = FALSE;
char InputFile[StringLength(BaseFilename) + StringLength(".hmml")];
CopyString(InputFile, "%s.hmml", BaseFilename);
HMMLToBuffers(CollationBuffers, InputFile);
Index.Entry.Size = CollationBuffers->Search.Ptr - CollationBuffers->Search.Location;
for(int i = 0; i < ArrayCount(Index.Entry.BaseFilename); ++i)
{
Index.Entry.BaseFilename[i] = '\0';
}
CopyString(Index.Entry.BaseFilename, BaseFilename);
int EntryIndex;
if(IndexMetadataFileReadCode == RC_SUCCESS && IndexFileReadCode == RC_SUCCESS)
{
// TODO(matt): Index validation?
// Maybe at least if(!StringsDiffer(..., "name: \"");
// and check that we won't crash through the end of the file when skipping to the next entry
Index.Header = *(index_header *)Index.Metadata.Buffer.Ptr;
Index.Metadata.Buffer.Ptr += sizeof(Index.Header);
Index.File.Buffer.Ptr += StringLength("---\n");
IndexEntryStart = Index.File.Buffer.Ptr;
for(EntryIndex = 0; EntryIndex < Index.Header.EntryCount; ++EntryIndex)
{
index_metadata This = *(index_metadata *)Index.Metadata.Buffer.Ptr;
if(!StringsDiffer(This.BaseFilename, BaseFilename))
{
// Reinsert
MetadataInsertionOffset = Index.Metadata.Buffer.Ptr - Index.Metadata.Buffer.Location;
IndexEntryInsertionStart = IndexEntryStart - Index.File.Buffer.Location;
IndexEntryInsertionEnd = IndexEntryInsertionStart + Index.Entry.Size;
Found = TRUE;
break;
}
else if(StringsDiffer(This.BaseFilename, BaseFilename) > 0)
{
// Insert
MetadataInsertionOffset = Index.Metadata.Buffer.Ptr - Index.Metadata.Buffer.Location;
IndexEntryInsertionStart = IndexEntryStart - Index.File.Buffer.Location;
break;
}
else
{
Index.Metadata.Buffer.Ptr += sizeof(Index.Entry);
IndexEntryStart += This.Size;
Index.File.Buffer.Ptr = IndexEntryStart;
}
}
}
else
{
// NOTE(matt): Initialising new index_header
Index.Header.DBVersion = CINERA_DB_VERSION;
Index.Header.AppVersion = CINERA_APP_VERSION;
Index.Header.HMMLVersion.Major = hmml_version.Major;
Index.Header.HMMLVersion.Minor = hmml_version.Minor;
Index.Header.HMMLVersion.Patch = hmml_version.Patch;
DIR *OutputDirectoryHandle;
if(!(OutputDirectoryHandle = opendir(Config.BaseDir)))
{
if(MakeDir(Config.BaseDir) == RC_ERROR_DIRECTORY)
{
LogError(LOG_ERROR, "Unable to create directory %s: %s", Config.BaseDir, strerror(errno));
fprintf(stderr, "Unable to create directory %s: %s\n", Config.BaseDir, strerror(errno));
return RC_ERROR_DIRECTORY;
};
}
closedir(OutputDirectoryHandle);
}
if(!(Index.Metadata.Handle = fopen(Index.Metadata.Path, "w"))) { FreeBuffer(&Index.Metadata.Buffer); return RC_ERROR_FILE; }
if(!(Index.File.Handle = fopen(Index.File.Path, "w"))) { FreeBuffer(&Index.File.Buffer); return RC_ERROR_FILE; }
if(!Found) { ++Index.Header.EntryCount; }
fwrite(&Index.Header, sizeof(Index.Header), 1, Index.Metadata.Handle);
if(IndexMetadataFileReadCode == RC_SUCCESS)
{
Index.Metadata.Buffer.Ptr = Index.Metadata.Buffer.Location + sizeof(Index.Header);
}
if(Found)
{
// NOTE(matt): We hit this during the start-up sync and when copying in a .hmml file over an already existing one, but
// would need to fool about with the inotify event processing to get to this branch in the case that saving
// a file triggers an IN_DELETE followed by an IN_CLOSE_WRITE event
// Reinsert
fwrite(Index.Metadata.Buffer.Ptr, MetadataInsertionOffset - sizeof(Index.Header), 1, Index.Metadata.Handle);
fwrite(&Index.Entry, sizeof(Index.Entry), 1, Index.Metadata.Handle);
fwrite(Index.Metadata.Buffer.Ptr - sizeof(Index.Header) + MetadataInsertionOffset + sizeof(Index.Entry), Index.Metadata.FileSize - MetadataInsertionOffset - sizeof(Index.Entry), 1, Index.Metadata.Handle);
fwrite(Index.File.Buffer.Location, IndexEntryInsertionStart, 1, Index.File.Handle);
fwrite(CollationBuffers->Search.Location, Index.Entry.Size, 1, Index.File.Handle);
fwrite(Index.File.Buffer.Location + IndexEntryInsertionEnd, Index.File.FileSize - IndexEntryInsertionEnd, 1, Index.File.Handle);
LogError(LOG_NOTICE, "Reinserted %s - %s", BaseFilename, CollationBuffers->Title);
fprintf(stderr, "\e[1;33mReinserted\e[0m %s - %s\n", BaseFilename, CollationBuffers->Title);
}
else if(MetadataInsertionOffset >= 0 && IndexEntryInsertionStart >= 0)
{
// Insert new
fwrite(Index.Metadata.Buffer.Ptr, MetadataInsertionOffset - sizeof(Index.Header), 1, Index.Metadata.Handle);
fwrite(&Index.Entry, sizeof(Index.Entry), 1, Index.Metadata.Handle);
fwrite(Index.Metadata.Buffer.Ptr - sizeof(Index.Header) + MetadataInsertionOffset, Index.Metadata.FileSize - MetadataInsertionOffset, 1, Index.Metadata.Handle);
fwrite(Index.File.Buffer.Location, IndexEntryInsertionStart, 1, Index.File.Handle);
fwrite(CollationBuffers->Search.Location, Index.Entry.Size, 1, Index.File.Handle);
fwrite(Index.File.Buffer.Location + IndexEntryInsertionStart, Index.File.FileSize - IndexEntryInsertionStart, 1, Index.File.Handle);
LogError(LOG_NOTICE, "Inserted %s - %s", BaseFilename, CollationBuffers->Title);
fprintf(stderr, "\e[1;32mInserted\e[0m %s - %s\n", BaseFilename, CollationBuffers->Title);
}
else
{
// Append new
if(IndexMetadataFileReadCode == RC_SUCCESS)
{
fwrite(Index.Metadata.Buffer.Ptr, Index.Metadata.FileSize - sizeof(Index.Header), 1, Index.Metadata.Handle);
fwrite(Index.File.Buffer.Location, Index.File.FileSize, 1, Index.File.Handle);
}
else
{
fprintf(Index.File.Handle, "---\n");
}
fwrite(&Index.Entry, sizeof(Index.Entry), 1, Index.Metadata.Handle);
fwrite(CollationBuffers->Search.Location, Index.Entry.Size, 1, Index.File.Handle);
LogError(LOG_NOTICE, "Appended %s - %s", BaseFilename, CollationBuffers->Title);
fprintf(stderr, "\e[1;32mAppended\e[0m %s - %s\n", BaseFilename, CollationBuffers->Title);
}
fclose(Index.Metadata.Handle);
fclose(Index.File.Handle);
FreeBuffer(&Index.Metadata.Buffer);
FreeBuffer(&Index.File.Buffer);
return RC_SUCCESS;
}
int
DeleteFromIndex(char *BaseFilename)
{
// TODO(matt): LogError()
index Index;
Index.Metadata.Buffer.ID = "IndexMetadata";
CopyString(Index.Metadata.Path, "%s/%s.metadata", Config.BaseDir, Config.ProjectID);
switch(ReadFileIntoBuffer(&Index.Metadata, 0))
{
case RC_ERROR_FILE:
return RC_ERROR_FILE;
case RC_ERROR_MEMORY:
LogError(LOG_ERROR, "DeleteFromIndex(): %s", strerror(errno));
return RC_ERROR_MEMORY;
case RC_SUCCESS:
break;
}
Index.File.Buffer.ID = "Index";
CopyString(Index.File.Path, "%s/%s.index", Config.BaseDir, Config.ProjectID);
switch(ReadFileIntoBuffer(&Index.File, 0))
{
case RC_ERROR_FILE:
return RC_ERROR_FILE;
case RC_ERROR_MEMORY:
LogError(LOG_ERROR, "DeleteFromIndex(): %s", strerror(errno));
return RC_ERROR_MEMORY;
case RC_SUCCESS:
break;
}
Index.Header = *(index_header *)Index.Metadata.Buffer.Ptr;
Index.Metadata.Buffer.Ptr += sizeof(Index.Header);
bool Found = FALSE;
int DeleteMetadataFrom = -1;
int DeleteFileFrom = -1;
int DeleteFileTo = -1;
int SizeAcc = 0;
for(int EntryIndex = 0; EntryIndex < Index.Header.EntryCount; ++EntryIndex, Index.Metadata.Buffer.Ptr += sizeof(index_metadata))
{
index_metadata This = *(index_metadata *)Index.Metadata.Buffer.Ptr;
if(!StringsDiffer(This.BaseFilename, BaseFilename))
{
Found = TRUE;
--Index.Header.EntryCount;
DeleteMetadataFrom = Index.Metadata.Buffer.Ptr - Index.Metadata.Buffer.Location;
DeleteFileFrom = StringLength("---\n") + SizeAcc;
DeleteFileTo = DeleteFileFrom + This.Size;
break;
}
SizeAcc += This.Size;
}
if(Found)
{
if(Index.Header.EntryCount == 0)
{
remove(Index.Metadata.Path);
remove(Index.File.Path);
}
else
{
if(!(Index.Metadata.Handle = fopen(Index.Metadata.Path, "w"))) { FreeBuffer(&Index.Metadata.Buffer); return RC_ERROR_FILE; }
if(!(Index.File.Handle = fopen(Index.File.Path, "w"))) { FreeBuffer(&Index.File.Buffer); return RC_ERROR_FILE; }
fwrite(&Index.Header, sizeof(Index.Header), 1, Index.Metadata.Handle);
Index.Metadata.Buffer.Ptr = Index.Metadata.Buffer.Location + sizeof(Index.Header);
fwrite(Index.Metadata.Buffer.Ptr, DeleteMetadataFrom - sizeof(Index.Header), 1, Index.Metadata.Handle);
fwrite(Index.Metadata.Buffer.Ptr + DeleteMetadataFrom - sizeof(Index.Header) + sizeof(Index.Entry), Index.Metadata.FileSize - DeleteMetadataFrom - sizeof(Index.Entry), 1, Index.Metadata.Handle);
fclose(Index.Metadata.Handle);
fwrite(Index.File.Buffer.Location, DeleteFileFrom, 1, Index.File.Handle);
fwrite(Index.File.Buffer.Location + DeleteFileTo, Index.File.FileSize - DeleteFileTo, 1, Index.File.Handle);
fclose(Index.File.Handle);
}
}
FreeBuffer(&Index.Metadata.Buffer);
FreeBuffer(&Index.File.Buffer);
return Found ? RC_SUCCESS : RC_NOOP;
}
int
IndexToBuffer(buffers *CollationBuffers)
{
// TODO(matt): Consider parsing the index into a linked list, or do something to save us having to iterate through the index
// file multiple times
index Index;
Index.Metadata.Buffer.ID = "IndexMetadata";
CopyString(Index.Metadata.Path, "%s/%s.metadata", Config.BaseDir, Config.ProjectID);
int IndexMetadataFileReadCode = ReadFileIntoBuffer(&Index.Metadata, 0);
Index.File.Buffer.ID = "IndexFile";
CopyString(Index.File.Path, "%s/%s.index", Config.BaseDir, Config.ProjectID);
int IndexFileReadCode = ReadFileIntoBuffer(&Index.File, 0);
if(IndexMetadataFileReadCode == RC_SUCCESS && IndexFileReadCode == RC_SUCCESS)
{
Index.Header = *(index_header*)Index.Metadata.Buffer.Ptr;
Index.Metadata.Buffer.Ptr += sizeof(Index.Header);
Index.File.Buffer.Ptr += StringLength("---\n");
char *IndexEntryStart = Index.File.Buffer.Ptr;
bool ProjectFound = FALSE;
int ProjectIndex;
for(ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex)
{
if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID))
{
ProjectFound = TRUE;
break;
}
}
if(!ProjectFound)
{
fprintf(stderr, "Missing Project Info for %s\n", Config.ProjectID);
return RC_ERROR_PROJECT;
}
int ThemeStringLength = StringsDiffer(Config.Theme, "") ? (StringLength(Config.Theme) * 2) : (StringLength(Config.ProjectID) * 2);
char queryContainer[512 + ThemeStringLength];
CopyString(queryContainer,
"<div class=\"cineraQueryContainer %s\">\n"
" <label for=\"query\">Query:</label>\n"
" <div class=\"inputContainer\">\n"
" <input type=\"text\" id=\"query\" autofocus=\"\">\n"
" <div class=\"spinner\">\n"
" Downloading data...\n"
" </div>\n"
" </div>\n"
" </div>\n"
" <div id=\"cineraResultsSummary\">Found: 0 episodes, 0 markers, 0h 0m 0s total.</div>\n"
" <div id=\"cineraResults\"></div>\n"
"\n"
" <dl id=\"cineraIndex\" class=\"%s\">\n",
StringsDiffer(Config.Theme, "") ? Config.Theme : Config.ProjectID,
StringsDiffer(Config.Theme, "") ? Config.Theme : Config.ProjectID);
buffer URLPrefix;
ClaimBuffer(&URLPrefix, "URLPrefix", 1024);
ConstructURLPrefix(&URLPrefix, INCLUDE_JS, PAGE_INDEX);
char Script[512 + StringLength(URLPrefix.Location) + (StringLength(Config.ProjectID) * 2)];
CopyString(Script, " </dl>\n"
" <script type=\"text/javascript\">\n"
" var projectID = \"%s\";\n"
" var theme = \"%s\";\n"
" var outputURLPrefix = \"%s\";\n"
" </script>\n"
" <script type=\"text/javascript\" src=\"%scinera_search.js\"></script>\n",
Config.ProjectID,
StringsDiffer(Config.Theme, "") ? Config.Theme : Config.ProjectID,
StringsDiffer(Config.URLPrefix, "") ? Config.URLPrefix : Config.ProjectID,
URLPrefix.Location);
DeclaimBuffer(&URLPrefix);
int EntryLength = 32 + StringLength(ProjectInfo[ProjectIndex].Unit) + 16 + 256;
CollationBuffers->Index.Size = StringLength(queryContainer) + (Index.Header.EntryCount * EntryLength) + StringLength(Script);
if(!(CollationBuffers->Index.Location = malloc(CollationBuffers->Index.Size)))
{
return(RC_ERROR_MEMORY);
}
CollationBuffers->Index.Ptr = CollationBuffers->Index.Location;
CopyStringToBuffer(&CollationBuffers->Index, queryContainer);
for(int EntryIndex = 0; EntryIndex < Index.Header.EntryCount; ++EntryIndex)
{
index_metadata This = *(index_metadata *)Index.Metadata.Buffer.Ptr;
char Number[16];
CopyString(Number, This.BaseFilename + StringLength(Config.ProjectID));
if(ProjectInfo[ProjectIndex].NumberingScheme == NS_LINEAR)
{
for(int i = 0; Number[i]; ++i)
{
if(Number[i] == '_')
{
Number[i] = '.';
}
}
}
SeekBufferForString(&Index.File.Buffer, "title: \"", C_SEEK_FORWARDS, C_SEEK_AFTER);
char Title[256];
CopyStringNoFormatT(Title, Index.File.Buffer.Ptr, '\n');
Title[StringLength(Title) - 1] = '\0';
char BaseFilename[255];
if(StringsDiffer(Config.URLPrefix, ""))
{
char *Ptr = This.BaseFilename + StringLength(Config.ProjectID);
CopyString(BaseFilename, "%s%s", Config.URLPrefix, Ptr);
}
else
{
CopyString(BaseFilename, "%s", This.BaseFilename);
}
if(StringsDiffer(ProjectInfo[ProjectIndex].Unit, ""))
{
CopyStringToBuffer(&CollationBuffers->Index,
" <dt>\n"
" <a href=\"%s\">", BaseFilename);
char Text[1024]; // NOTE(matt): Surely this will be big enough
CopyString(Text, "%s %s: %s",
ProjectInfo[ProjectIndex].Unit, // TODO(matt): Do we need to special-case the various numbering schemes?
Number,
Title);
CopyStringToBufferHTMLSafe(&CollationBuffers->Index, Text);
CopyStringToBuffer(&CollationBuffers->Index,
"</a>\n"
" </dt>\n");
}
else
{
CopyStringToBuffer(&CollationBuffers->Index,
" <dt>\n"
" <a href=\"%s\">%s</a>\n"
" </dt>\n",
BaseFilename,
Title);
}
Index.Metadata.Buffer.Ptr += sizeof(Index.Entry);
IndexEntryStart += This.Size;
Index.File.Buffer.Ptr = IndexEntryStart;
}
CopyStringToBuffer(&CollationBuffers->Index, Script);
FreeBuffer(&Index.Metadata.Buffer);
return RC_SUCCESS;
}
else
{
return RC_ERROR_FILE;
}
}
int
GeneratePlayerPage(buffers *CollationBuffers, template *PlayerTemplate, char *BaseFilename)
{
char OutputDir[256];
if(StringsDiffer(Config.URLPrefix, ""))
{
char *Ptr = BaseFilename + StringLength(Config.ProjectID);
CopyString(OutputDir, "%s/%s%s", Config.BaseDir, Config.URLPrefix, Ptr);
}
else
{
CopyString(OutputDir, "%s/%s", Config.BaseDir, BaseFilename);
}
char PlayerPagePath[256];
CopyString(PlayerPagePath, "%s/index.html", OutputDir);
DIR *OutputDirectoryHandle;
if(!(OutputDirectoryHandle = opendir(OutputDir)))
{
if(MakeDir(OutputDir) == RC_ERROR_DIRECTORY)
{
LogError(LOG_ERROR, "Unable to create directory %s: %s", OutputDir, strerror(errno));
fprintf(stderr, "Unable to create directory %s: %s\n", OutputDir, strerror(errno));
return RC_ERROR_DIRECTORY;
};
}
closedir(OutputDirectoryHandle);
BuffersToHTML(CollationBuffers, PlayerTemplate, PlayerPagePath, PAGE_PLAYER);
return RC_SUCCESS;
}
int
GenerateIndexPage(buffers *CollationBuffers, template *IndexTemplate)
{
char IndexPagePath[256];
CopyString(IndexPagePath, "%s/index.html", Config.BaseDir);
IndexToBuffer(CollationBuffers);
BuffersToHTML(CollationBuffers, IndexTemplate, IndexPagePath, PAGE_INDEX);
FreeBuffer(&CollationBuffers->Index);
return RC_SUCCESS;
}
int
DeletePlayerPageFromFilesystem(char *BaseFilename)
{
// NOTE(matt): Once we have the notion of an output filename format, we'll need to use that here
char PlayerDirPath[256];
if(StringsDiffer(Config.URLPrefix, ""))
{
char *Ptr = BaseFilename + StringLength(Config.ProjectID);
CopyString(PlayerDirPath, "%s/%s%s", Config.BaseDir, Config.URLPrefix, Ptr);
}
else
{
CopyString(PlayerDirPath, "%s/%s", Config.BaseDir, BaseFilename);
}
DIR *PlayerDir;
if((PlayerDir = opendir(PlayerDirPath))) // There is a directory for the Player, which there probably should be if not for manual intervention
{
char PlayerPagePath[256];
CopyString(PlayerPagePath, "%s/index.html", PlayerDirPath);
FILE *PlayerPage;
if((PlayerPage = fopen(PlayerPagePath, "r")))
{
fclose(PlayerPage);
remove(PlayerPagePath);
}
closedir(PlayerDir);
if((remove(PlayerDirPath) == -1))
{
LogError(LOG_NOTICE, "Mostly deleted %s. Unable to remove directory %s: %s", BaseFilename, PlayerDirPath, strerror(errno));
fprintf(stderr, "\e[1;30mMostly deleted\e[0m %s. \e[1;31mUnable to remove directory\e[0m %s: %s", BaseFilename, PlayerDirPath, strerror(errno));
}
else
{
LogError(LOG_INFORMATIONAL, "Deleted %s", BaseFilename);
fprintf(stderr, "\e[1;30mDeleted\e[0m %s\n", BaseFilename);
}
}
return RC_SUCCESS;
}
void
DeleteEntry(char *BaseFilename)
{
if(DeleteFromIndex(BaseFilename) == RC_SUCCESS)
{
DeletePlayerPageFromFilesystem(BaseFilename);
}
}
int
MonitorDirectory(buffers *CollationBuffers, template *IndexTemplate, template *PlayerTemplate, int inotifyInstance, int WatchDescriptor)
{
#if DEBUG_MEM
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, "\nCalled MonitorDirectory()\n");
fclose(MemLog);
#endif
// TODO(matt): Maybe straight up store the IndexPath in the Config to save us having to derive it near / at the usage site
buffer Events;
if(ClaimBuffer(&Events, "inotify Events", Kilobytes(32)) == 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 - Events.Location < Events.Size;
Events.Ptr += sizeof(struct inotify_event) + Event->len)
{
Event = (struct inotify_event *)Events.Ptr;
char *Ptr;
Ptr = Event->name;
Ptr += (StringLength(Event->name) - StringLength(".hmml"));
if(!(StringsDiffer(Ptr, ".hmml")))
{
*Ptr = '\0';
char BaseFilename[256];
CopyString(BaseFilename, Event->name);
*Ptr = '.';
// TODO(matt): Maybe handle IN_ALL_EVENTS
if(Event->mask & IN_DELETE || Event->mask & IN_MOVED_FROM)
{
DeleteEntry(BaseFilename);
}
else
{
InsertIntoIndex(CollationBuffers, BaseFilename);
GeneratePlayerPage(CollationBuffers, PlayerTemplate, BaseFilename);
}
GenerateIndexPage(CollationBuffers, IndexTemplate);
}
}
}
DeclaimBuffer(&Events);
return RC_NOOP;
}
typedef struct
{
bool Present;
char ID[32];
} index_entry; // Metadata, unless we actually want to bolster this?
int
DeleteDeadIndexEntries()
{
// TODO(matt): More rigorously figure out who we should delete
// Maybe compare the output directory and the input HMML names
index Index;
Index.Metadata.Buffer.ID = "IndexMetadata";
CopyString(Index.Metadata.Path, "%s/%s.metadata", Config.BaseDir, Config.ProjectID);
if(ReadFileIntoBuffer(&Index.Metadata, 0) == RC_ERROR_FILE)
{
return RC_ERROR_FILE;
}
Index.Header = *(index_header *)Index.Metadata.Buffer.Ptr;
if(Index.Header.DBVersion != CINERA_DB_VERSION)
{
fprintf(stderr, "\n\e[1;31mHandle conversion from CINERA_DB_VERSION %d to %d!\e[0m\n\n", Index.Header.DBVersion, CINERA_DB_VERSION);
exit(EXIT_FAILURE);
}
Index.Metadata.Buffer.Ptr += sizeof(Index.Header);
index_entry Entries[Index.Header.EntryCount];
for(int EntryIndex = 0; EntryIndex < Index.Header.EntryCount; ++EntryIndex)
{
index_metadata This = *(index_metadata *)Index.Metadata.Buffer.Ptr;
CopyStringNoFormat(Entries[EntryIndex].ID, This.BaseFilename);
Entries[EntryIndex].Present = FALSE;
Index.Metadata.Buffer.Ptr += sizeof(This);;
}
DIR *ProjectDirHandle;
if(!(ProjectDirHandle = opendir(Config.ProjectDir)))
{
LogError(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;
while((ProjectFiles = readdir(ProjectDirHandle)))
{
char *Ptr;
Ptr = ProjectFiles->d_name;
Ptr += (StringLength(ProjectFiles->d_name) - StringLength(".hmml"));
if(!(StringsDiffer(Ptr, ".hmml")))
{
*Ptr = '\0';
for(int i = 0; i < Index.Header.EntryCount; ++i)
{
if(!StringsDiffer(Entries[i].ID, ProjectFiles->d_name))
{
Entries[i].Present = TRUE;
break;
}
}
}
}
closedir(ProjectDirHandle);
bool Deleted = FALSE;
for(int i = 0; i < Index.Header.EntryCount; ++i)
{
if(Entries[i].Present == FALSE)
{
Deleted = TRUE;
DeleteEntry(Entries[i].ID);
}
}
FreeBuffer(&Index.Metadata.Buffer);
return Deleted ? RC_SUCCESS : RC_NOOP;
}
int
SyncIndexWithInput(buffers *CollationBuffers, template *IndexTemplate, template *PlayerTemplate)
{
bool Deleted = FALSE;
if(DeleteDeadIndexEntries() == RC_SUCCESS)
{
Deleted = TRUE;
}
DIR *ProjectDirHandle;
if(!(ProjectDirHandle = opendir(Config.ProjectDir)))
{
LogError(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;
bool Inserted = FALSE;
while((ProjectFiles = readdir(ProjectDirHandle)))
{
char *Ptr = ProjectFiles->d_name;
Ptr += (StringLength(ProjectFiles->d_name) - StringLength(".hmml"));
if(!(StringsDiffer(Ptr, ".hmml")))
{
*Ptr = '\0';
InsertIntoIndex(CollationBuffers, ProjectFiles->d_name);
GeneratePlayerPage(CollationBuffers, PlayerTemplate, ProjectFiles->d_name);
Inserted = TRUE;
}
}
closedir(ProjectDirHandle);
if(Deleted || Inserted)
{
GenerateIndexPage(CollationBuffers, IndexTemplate);
}
return RC_SUCCESS;
}
char *
StripTrailingSlash(char *String)
{
int Length = StringLength(String);
if(Length > 0 && String[Length - 1] == '/')
{
String[Length - 1] = '\0';
}
return String;
}
void
PrintVersions()
{
curl_version_info_data *CurlVersion = curl_version_info(CURLVERSION_NOW);
printf("Cinera: %d.%d.%d\n"
"Cinera DB: %d\n"
"hmmlib: %d.%d.%d\n"
"libcurl: %s\n",
CINERA_APP_VERSION.Major, CINERA_APP_VERSION.Minor, CINERA_APP_VERSION.Patch,
CINERA_DB_VERSION,
hmml_version.Major, hmml_version.Minor, hmml_version.Patch,
CurlVersion->version);
}
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_EMERGENCY,
.DefaultMedium = "programming",
.Mode = getenv("CINERA_MODE") ? MODE_INTEGRATE : MODE_BARE,
.OutLocation = "out.html",
.OutIntegratedLocation = "out_integrated.html",
.ForceIntegration = FALSE,
.ProjectDir = ".",
.ProjectID = "",
.RootDir = ".",
.RootURL = "",
.Theme = "",
.TemplatePlayerLocation = "template_player.html",
.TemplateIndexLocation = "template_index.html",
.UpdateInterval = 4,
.URLPrefix = ""
};
if(getenv("XDG_CACHE_HOME"))
{
CopyString(DefaultConfig.CacheDir, "%s/cinera", getenv("XDG_CACHE_HOME"));
}
else
{
CopyString(DefaultConfig.CacheDir, "%s/.cache/cinera", getenv("HOME"));
}
Config = DefaultConfig;
if(ArgC < 2)
{
PrintUsage(Args[0], &DefaultConfig);
return RC_RIP;
}
char CommandLineArg;
while((CommandLineArg = getopt(ArgC, Args, "b:c:d:fhi:j:l:m:o:p:r:u:s:t:U:vx:")) != -1)
{
switch(CommandLineArg)
{
case 'b':
Config.BaseDir = StripTrailingSlash(optarg);
break;
case 'c':
Config.CSSDir = StripTrailingSlash(optarg);
break;
case 'd':
Config.ProjectDir = StripTrailingSlash(optarg);
break;
case 'f':
Config.ForceIntegration = TRUE;
break;
case 'i':
Config.ImagesDir = StripTrailingSlash(optarg);
break;
case 'j':
Config.JSDir = StripTrailingSlash(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.ProjectID = optarg;
break;
case 'r':
Config.RootDir = StripTrailingSlash(optarg);
break;
case 's':
Config.Theme = optarg;
break;
case 't':
Config.TemplatePlayerLocation = optarg;
Config.Mode = MODE_INTEGRATE;
break;
case 'u':
Config.RootURL = StripTrailingSlash(optarg);
break;
case 'U':
Config.UpdateInterval = StringToInt(optarg);
break;
case 'v':
PrintVersions();
return RC_SUCCESS;
case 'x':
Config.TemplateIndexLocation = optarg;
Config.Mode = MODE_INTEGRATE;
break;
case 'h':
default:
PrintUsage(Args[0], &DefaultConfig);
return RC_SUCCESS;
}
}
if(StringsDiffer(Config.ProjectID, ""))
{
Config.Edition = EDITION_PROJECT;
for(int ProjectInfoIndex = 0; ProjectInfoIndex < ArrayCount(ProjectInfo); ++ProjectInfoIndex)
{
if(!StringsDiffer(Config.ProjectID, ProjectInfo[ProjectInfoIndex].ProjectID))
{
if(StringsDiffer(ProjectInfo[ProjectInfoIndex].Medium, ""))
{
Config.DefaultMedium = ProjectInfo[ProjectInfoIndex].Medium;
}
if(StringsDiffer(ProjectInfo[ProjectInfoIndex].AltURLPrefix, ""))
{
Config.URLPrefix = ProjectInfo[ProjectInfoIndex].AltURLPrefix;
}
break;
}
}
}
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;
}
// NOTE(matt): Init MemoryArena (it is global)
MemoryArena.Size = Megabytes(4);
if(!(MemoryArena.Location = calloc(MemoryArena.Size, 1)))
{
LogError(LOG_EMERGENCY, "%s: %s", Args[0], strerror(errno));
return RC_RIP;
}
MemoryArena.Ptr = MemoryArena.Location;
#if DEBUG_MEM
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, " Allocated MemoryArena (%d)\n", MemoryArena.Size);
fclose(MemLog);
printf(" Allocated MemoryArena (%d)\n", MemoryArena.Size);
#endif
#if DEBUG
printf("Allocated MemoryArena: %d\n\n", MemoryArena.Size);
#endif
buffer Errors;
if(ClaimBuffer(&Errors, "Errors", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; };
// NOTE(matt): Tree structure of buffer dependencies
// IncludesPlayer
// Menus
// Player
// ScriptPlayer
//
// IncludesIndex
// Index
buffers CollationBuffers;
if(ClaimBuffer(&CollationBuffers.IncludesPlayer, "IncludesPlayer", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; };
if(ClaimBuffer(&CollationBuffers.Menus, "Menus", Kilobytes(32)) == RC_ARENA_FULL) { goto RIP; };
if(ClaimBuffer(&CollationBuffers.Player, "Player", Kilobytes(256)) == RC_ARENA_FULL) { goto RIP; };
if(ClaimBuffer(&CollationBuffers.ScriptPlayer, "ScriptPlayer", Kilobytes(8)) == RC_ARENA_FULL) { goto RIP; };
if(ClaimBuffer(&CollationBuffers.IncludesIndex, "IncludesIndex", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; };
if(ClaimBuffer(&CollationBuffers.Search, "Search", Kilobytes(32)) == RC_ARENA_FULL) { goto RIP; };
if(ClaimBuffer(&CollationBuffers.ScriptIndex, "ScriptIndex", 256) == RC_ARENA_FULL) { goto RIP; };
*CollationBuffers.Title = '\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
template *PlayerTemplate;
template *IndexTemplate;
if(Config.Mode == MODE_INTEGRATE)
{
switch(ValidateTemplate(&Errors, &PlayerTemplate, 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)
{
switch(ValidateTemplate(&Errors, &IndexTemplate, 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)
{
#if DEBUG_MEM
FILE *MemLog = fopen("/home/matt/cinera_mem", "w+");
fprintf(MemLog, "Entered Project Edition\n");
fclose(MemLog);
#endif
// TODO(matt): Also log these startup messages?
PrintVersions();
printf( "\n"
"Globals\n"
" Cache Directory:\t\t%s\n"
"\n"
" Root\n"
" Directory:\t\t%s\n"
" URL:\t\t\t%s\n"
" Paths relative to root\n"
" CSS:\t\t\t%s\n"
" Images:\t\t\t%s\n"
" JS:\t\t\t%s\n",
Config.CacheDir,
Config.RootDir,
StringsDiffer(Config.RootURL, "") ? Config.RootURL : "[empty]",
StringsDiffer(Config.CSSDir, "") ? Config.CSSDir : "(same as root)",
StringsDiffer(Config.ImagesDir, "") ? Config.ImagesDir : "(same as root)",
StringsDiffer(Config.JSDir, "") ? Config.JSDir : "(same as root)");
if(Config.Mode == MODE_INTEGRATE)
{
printf(" Index Template:\t\t%s\n"
" Player Template:\t%s\n",
StringsDiffer(Config.TemplateIndexLocation, "") ? Config.TemplateIndexLocation : "(same as root)",
StringsDiffer(Config.TemplatePlayerLocation, "") ? Config.TemplatePlayerLocation : "(same as root)");
}
printf("\n"
"Project\n"
" ID:\t\t\t\t%s\n"
" Input Directory:\t\t%s\n"
" Output Base Directory:\t%s\n"
" Player Page URL Prefix:\t%s\n"
" Default Medium:\t\t%s\n"
" Style / Theme:\t\t%s\n"
"\n"
"┌╼ Synchronising with annotation files in Project Input Directory ╾┐\n",
Config.ProjectID,
Config.ProjectDir,
Config.BaseDir,
StringsDiffer(Config.URLPrefix, "") ? Config.URLPrefix : Config.ProjectID,
Config.DefaultMedium,
StringsDiffer(Config.Theme, "") ? Config.Theme: Config.ProjectID);
SyncIndexWithInput(&CollationBuffers, IndexTemplate, PlayerTemplate);
printf("\n┌╼ Monitoring Project Directory for \e[1;32mnew\e[0m, \e[1;33medited\e[0m and \e[1;30mdeleted\e[0m .hmml files ╾┐\n");
int inotifyInstance = inotify_init1(IN_NONBLOCK);
// NOTE(matt): Do we want to also watch IN_DELETE_SELF events?
int WatchDescriptor = inotify_add_watch(inotifyInstance, Config.ProjectDir, IN_CLOSE_WRITE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO);
while(MonitorDirectory(&CollationBuffers, IndexTemplate, PlayerTemplate, inotifyInstance, WatchDescriptor) != RC_ERROR_FATAL)
{
sleep(Config.UpdateInterval);
}
}
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(&CollationBuffers, 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(&CollationBuffers, PlayerTemplate, 0, PAGE_PLAYER))
{
// TODO(matt): Actually sort out the fatality of these cases, once we are always-on
case RC_INVALID_TEMPLATE:
LogError(LOG_ERROR, "Invalid player template: %s", PlayerTemplate->Metadata.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(PlayerTemplate);
if(Config.Edition == EDITION_PROJECT)
{
DeclaimTemplate(IndexTemplate);
}
}
DeclaimBuffer(&CollationBuffers.ScriptIndex);
DeclaimBuffer(&CollationBuffers.Search);
DeclaimBuffer(&CollationBuffers.IncludesIndex);
DeclaimBuffer(&CollationBuffers.ScriptPlayer);
DeclaimBuffer(&CollationBuffers.Player);
DeclaimBuffer(&CollationBuffers.Menus);
DeclaimBuffer(&CollationBuffers.IncludesPlayer);
DeclaimBuffer(&Errors);
RIP:
free(MemoryArena.Location);
#if DEBUG_MEM
MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, " Freed MemoryArena\n");
fclose(MemLog);
printf(" Freed MemoryArena\n");
#endif
}