Annotation-System/hmml_to_html/hmml_to_html.c

2878 lines
116 KiB
C

#if 0
ctime -begin ${0%.*}.ctm
gcc -g -no-pie -fsanitize=address -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a
ctime -end ${0%.*}.ctm
exit
#endif
#define DEBUG 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
#define Kilobytes(Bytes) Bytes << 10
#define Megabytes(Bytes) Bytes << 20
enum
{
EDITION_SINGLE = 0,
EDITION_PROJECT = 1,
EDITION_NETWORK = 2
} EDITION;
typedef struct
{
char *Location;
char *Ptr;
char *ID;
int Size;
} buffer;
// 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;
// TODO(matt): Parse this stuff out of a config file
char *Credentials[ ][5] =
{
{ "Miblo", "Matt Mascarenhas", "http://miblodelcarpio.co.uk", "cinera_icon_patreon.png", "https://patreon.com/miblo"},
{ "miotatsu", "Mio Iwakura", "http://riscy.tv/", "cinera_icon_patreon.png", "https://patreon.com/miotatsu"},
{ "nothings", "Sean Barrett", "https://nothings.org/", "", ""},
{ "cmuratori", "Casey Muratori", "https://handmadehero.org", "cinera_icon_patreon.png", "https://patreon.com/cmuratori"},
{ "fierydrake", "Mike Tunnicliffe", "", "", ""},
{ "abnercoimbre", "Abner Coimbre", "https://handmade.network/m/abnercoimbre", "cinera_icon_patreon.png", "https://patreon.com/handmade_dev"},
};
#define EDITION EDITION_SINGLE
#define ArrayCount(A) sizeof(A)/sizeof(*(A))
#define ClaimBuffer(MemoryArena, ClaimedMemory, Buffer, ID, Size) if(__ClaimBuffer(MemoryArena, ClaimedMemory, 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;\
};
void
FreeBuffer(buffer *Buffer)
{
free(Buffer->Location);
}
int
__ClaimBuffer(buffer *MemoryArena, int *ClaimedMemory, buffer *Buffer, char *ID, int Size)
{
if(*ClaimedMemory + Size > MemoryArena->Size)
{
return 1;
}
Buffer->Location = MemoryArena->Location + *ClaimedMemory;
Buffer->Size = Size;
Buffer->ID = ID;
*ClaimedMemory += Buffer->Size;
*Buffer->Location = '\0';
Buffer->Ptr = Buffer->Location;
#if DEBUG
printf(" Claimed: %s: %d\n"
" Total ClaimedMemory: %d\n\n", Buffer->ID, Buffer->Size, *ClaimedMemory);
#endif
return 0;
}
void
DeclaimBuffer(buffer *Buffer, int *ClaimedMemory)
{
*Buffer->Location = '\0';
*ClaimedMemory -= Buffer->Size;
#if DEBUG
printf("Declaimed: %s\n"
" Total ClaimedMemory: %d\n\n", Buffer->ID, *ClaimedMemory);
#endif
}
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];
}
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++;
}
}
__attribute__ ((format (printf, 2, 3)))
void
CopyString(char Dest[], char *Format, ...)
{
va_list Args;
va_start(Args, Format);
vsprintf(Dest, Format, Args);
va_end(Args);
}
int
CopyStringNoFormat(char *Dest, char *String)
{
int Length = 0;
while(*String)
{
*Dest++ = *String++;
++Length;
}
*Dest = '\0';
return Length;
}
int
StringLength(char *String)
{
int i = 0;
while(String[i])
{
++i;
}
return i;
}
__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
CopyStringToBufferCSVSafe(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 += 2;
break;
case '\'':
CopyStringToBuffer(Dest, "&#39;");
String++;
break;
default:
*Dest->Ptr++ = *String++;
break;
}
}
}
int
StringsDiffer(char *A, char *B) // NOTE(matt): Two null-terminated strings
{
while(*A && *B && *A == *B)
{
++A, ++B;
}
return *A - *B;
}
bool
StringsDifferT(char *A, // NOTE(matt): Null-terminated string
char *B, // NOTE(matt): Not null-terminated string (e.g. one mid-buffer)
char Terminator // NOTE(matt): Caller definable terminator. Pass 0 to only match on the extent of A
)
{
int ALength = StringLength(A);
int i = 0;
while(i < ALength && A[i] && A[i] == B[i])
{
++i;
}
if((!Terminator && !A[i] && ALength == i) ||
(!A[i] && ALength == i && (B[i] == Terminator)))
{
return FALSE;
}
else
{
return TRUE;
}
}
typedef struct
{
unsigned int Hue:16;
unsigned int Saturation:8;
unsigned int Lightness:8;
} hsl_colour;
hsl_colour
CharToColour(char Char)
{
hsl_colour Colour;
if(Char >= 'a' && Char <= 'z')
{
Colour.Hue = (((float)Char - 'a') / ('z' - 'a') * 360);
Colour.Saturation = (((float)Char - 'a') / ('z' - 'a') * 26 + 74);
}
else if(Char >= 'A' && Char <= 'Z')
{
Colour.Hue = (((float)Char - 'A') / ('Z' - 'A') * 360);
Colour.Saturation = (((float)Char - 'A') / ('Z' - 'A') * 26 + 74);
}
else if(Char >= '0' && Char <= '9')
{
Colour.Hue = (((float)Char - '0') / ('9' - '0') * 360);
Colour.Saturation = (((float)Char - '0') / ('9' - '0') * 26 + 74);
}
else
{
Colour.Hue = 180;
Colour.Saturation = 50;
}
return Colour;
}
hsl_colour *
StringToColourHash(hsl_colour *Colour, char *String)
{
Colour->Hue = 0;
Colour->Saturation = 0;
Colour->Lightness = 26;
int i;
for(i = 0; String[i]; ++i)
{
Colour->Hue += CharToColour(String[i]).Hue;
Colour->Saturation += CharToColour(String[i]).Saturation;
}
Colour->Hue = Colour->Hue % 360;
Colour->Saturation = Colour->Saturation % 26 + 74;
return(Colour);
}
char *
SanitisePunctuation(char *String)
{
char *Ptr = String;
while(*Ptr)
{
if(*Ptr == ' ')
{
*Ptr = '_';
}
if((*Ptr < '0' || *Ptr > '9') &&
(*Ptr < 'a' || *Ptr > 'z') &&
(*Ptr < 'A' || *Ptr > 'Z'))
{
*Ptr = '-';
}
++Ptr;
}
return String;
}
int
BuildCredits(buffer *CreditsMenu, buffer *HostInfo, buffer *AnnotatorInfo, bool *HasCreditsMenu, char *ImagesDir, char *Host, char *Annotator)
{
// TODO(matt): Handle co-hosts and guests
bool FoundHost = FALSE;
bool FoundAnnotator = FALSE;
for(int CredentialIndex = 0; CredentialIndex < ArrayCount(Credentials); ++CredentialIndex)
{
if(!StringsDiffer(Host, Credentials[CredentialIndex][0]))
{
FoundHost = TRUE; // TODO(matt): Check if this is actually necessary...
CopyStringToBuffer(HostInfo,
" <span class=\"credit\">\n");
if(*Credentials[CredentialIndex][2])
{
CopyStringToBuffer(HostInfo,
" <a class=\"person\" href=\"%s\" target=\"_blank\">\n"
" <div class=\"role\">Host</div>\n"
" <div class=\"name\">%s</div>\n"
" </a>\n",
Credentials[CredentialIndex][2],
Credentials[CredentialIndex][1]);
}
else
{
CopyStringToBuffer(HostInfo,
" <div class=\"person\">\n"
" <div class=\"role\">Host</div>\n"
" <div class=\"name\">%s</div>\n"
" </div>\n",
Credentials[CredentialIndex][1]);
}
if(*Credentials[CredentialIndex][4] && *Credentials[CredentialIndex][3])
{
CopyStringToBuffer(HostInfo,
" <a class=\"support\" href=\"%s\" target=\"_blank\"><img src=\"%s/%s\"></a>\n",
Credentials[CredentialIndex][4],
ImagesDir,
Credentials[CredentialIndex][3]);
}
CopyStringToBuffer(HostInfo,
" </span>\n");
}
if(!StringsDiffer(Annotator, Credentials[CredentialIndex][0]))
{
FoundAnnotator = TRUE; // TODO(matt): Check if this is actually necessary...
CopyStringToBuffer(AnnotatorInfo,
" <span class=\"credit\">\n");
if(*Credentials[CredentialIndex][2])
{
CopyStringToBuffer(AnnotatorInfo,
" <a class=\"person\" href=\"%s\" target=\"_blank\">\n"
" <div class=\"role\">Annotator</div>\n"
" <div class=\"name\">%s</div>\n"
" </a>\n",
Credentials[CredentialIndex][2],
Credentials[CredentialIndex][1]);
}
else
{
CopyStringToBuffer(AnnotatorInfo,
" <div class=\"person\">\n"
" <div class=\"role\">Annotator</div>\n"
" <div class=\"name\">%s</div>\n"
" </div>\n",
Credentials[CredentialIndex][1]);
}
if(*Credentials[CredentialIndex][4] && *Credentials[CredentialIndex][3])
{
CopyStringToBuffer(AnnotatorInfo,
" <a class=\"support\" href=\"%s\" target=\"_blank\"><img src=\"%s/%s\"></a>\n",
Credentials[CredentialIndex][4],
ImagesDir,
Credentials[CredentialIndex][3]);
}
CopyStringToBuffer(AnnotatorInfo,
" </span>\n");
}
}
if(FoundHost || FoundAnnotator)
{
CopyStringToBuffer(CreditsMenu,
" <div class=\"menu credits\">\n"
" <div class=\"mouse_catcher\"></div>\n"
" <span>Credits</span>\n"
" <div class=\"credits_container\">\n");
if(FoundHost)
{
CopyBuffer(CreditsMenu, HostInfo);
}
if(FoundAnnotator)
{
CopyBuffer(CreditsMenu, AnnotatorInfo);
}
CopyStringToBuffer(CreditsMenu,
" </div>\n"
" </div>\n");
}
else
{
return 1;
}
*HasCreditsMenu = TRUE;
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);
//TODO(matt): Look into finding the best ISBN searcher the web has to offer
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;
}
char *CategoryMedium[][3] =
{
// medium icon written name
{ "afk", "&#8230;" , "Away from Keyboard"}, // TODO(matt): Filter this out by default
{ "authored", "&#128490;", "Chat Comment"}, // TODO(matt): Conditionally handle Chat vs Guest Comments
{ "blackboard", "&#128396;", "Blackboard"},
{ "experience", "&#127863;", "Experience"},
{ "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
{ "trivia", "&#127922;", "Trivia"},
};
void
BuildFilter(category_info *TopicsArray, int *UniqueTopics, category_info *MediaArray, int *UniqueMedia, char *Marker)
{
bool IsMedium = FALSE;
int i = 0;
for(i = 0; i < ArrayCount(CategoryMedium); ++i)
{
if(!StringsDiffer(CategoryMedium[i][0], Marker))
{
IsMedium = TRUE;
break;
}
}
int Offset;
if(IsMedium)
{
int j = 0;
for(j = 0; j < *UniqueMedia; ++j)
{
if(!StringsDiffer(CategoryMedium[i][0], MediaArray[j].Marker))
{
return;
}
if((Offset = StringsDiffer(CategoryMedium[i][2], MediaArray[j].WrittenText)) < 0)
{
int k;
for(k = *UniqueMedia; k > j; --k)
{
CopyString(MediaArray[k].Marker, MediaArray[k-1].Marker);
CopyString(MediaArray[k].WrittenText, MediaArray[k-1].WrittenText);
}
CopyString(MediaArray[k].Marker, CategoryMedium[i][0]);
CopyString(MediaArray[k].WrittenText, CategoryMedium[i][2]);
break;
}
}
if(j == *UniqueMedia)
{
CopyString(MediaArray[j].Marker, CategoryMedium[i][0]);
CopyString(MediaArray[j].WrittenText, CategoryMedium[i][2]);
}
++*UniqueMedia;
return;
}
else
{
int i = 0;
for(i = 0; i < *UniqueTopics; ++i)
{
if(!StringsDiffer(Marker, TopicsArray[i].Marker))
{
return;
}
if((Offset = StringsDiffer(Marker, TopicsArray[i].Marker)) < 0)
{
int j;
for(j = *UniqueTopics; j > i; --j)
{
CopyString(TopicsArray[j].Marker, TopicsArray[j-1].Marker);
}
CopyString(TopicsArray[j].Marker, Marker);
break;
}
}
if(i == *UniqueTopics)
{
CopyString(TopicsArray[i].Marker, Marker);
}
++*UniqueTopics;
return;
}
}
void
BuildCategories(buffer *AnnotationClass, buffer *Category, int *MarkerIndex, bool *HasCategory, bool *HasMedium, char *Marker)
{
// NOTE(matt): This guy could also sort, so that the dots appear in a consistent order in the annotations
// If so, the Category buffer would have to only contain the category names and no more until collation time
// BuildCategories() would have to parse the Category.Location out to an array, sort that array and write it back in
// The code in the "annotation loop" would then have to write both the head and tail of the category stuff
for(int i = 0; i < ArrayCount(CategoryMedium); ++i)
{
if(!StringsDiffer(CategoryMedium[i][0], Marker))
{
CopyStringToBuffer(AnnotationClass, " %s", SanitisePunctuation(Marker));
*HasMedium = TRUE;
++*MarkerIndex;
return;
}
}
if(*HasCategory == FALSE)
{
CopyStringToBuffer(Category, "<span class=\"categories\">");
*HasCategory = TRUE;
}
// NOTE(matt): Iterate through the Category->Location looking for "Marker", and bail if we find it
char *Ptr = Category->Location;
bool Found = FALSE;
while(*Ptr)
{
if(*Ptr == '\"')
{
++Ptr;
if(!StringsDifferT(SanitisePunctuation(Marker), Ptr, ' '))
{
Found = TRUE;
break;
}
else
{
while(*Ptr != '\"')
{
++Ptr;
}
}
}
++Ptr;
}
if(Found == FALSE)
{
CopyStringToBuffer(Category, "<div title=\"%s\" class=\"category %s\"></div>",
SanitisePunctuation(Marker),
SanitisePunctuation(Marker));
CopyStringToBuffer(AnnotationClass, " cat_%s",
SanitisePunctuation(Marker));
}
++*MarkerIndex;
return;
}
int
StringToInt(char *String)
{
int Result = 0;
while(*String)
{
Result = Result * 10 + (*String - '0');
++String;
}
return Result;
}
int
BuildQuote(quote_info *Info, char *Speaker, int ID, char *QuoteDir)
{
char Path[255] = {0};
sprintf(Path, "%s/#%s", QuoteDir, Speaker);
FILE *File;
if(!(File = fopen(Path, "r")))
{
perror(Path);
return 1;
}
fseek(File, 0, SEEK_END);
int Length = ftell(File);
fseek(File, 0, SEEK_SET);
char *Buffer;
if(!(Buffer = malloc(Length)))
{
perror("BuildQuote");
}
fread(Buffer, Length, 1, File);
fclose(File);
// TODO(matt): Search the quote store in reverse
char *InPtr = Buffer;
while(InPtr - Buffer < Length)
{
char InID[4] = { 0 };
char *OutPtr = InID;
while(*InPtr != ',')
{
*OutPtr++ = *InPtr++;
}
*OutPtr = '\0';
if(StringToInt(InID) == ID)
{
InPtr += 2;
OutPtr = Info->Date;
while(*InPtr != '"')
{
*OutPtr++ = *InPtr++;
}
*OutPtr = '\0';
InPtr += 3;
OutPtr = Info->Text;
while(*InPtr != '\n')
{
if(*InPtr == '\\')
{
++InPtr;
}
*OutPtr++ = *InPtr++;
}
*--OutPtr = '\0';
free(Buffer);
return 0;
}
else
{
while(*InPtr != '\n')
{
++InPtr;
}
++InPtr;
}
}
free(Buffer);
return 1;
}
void
GenerateTopicColours(buffer *Colour, char *Topic, char *TopicsDir)
{
for(int i = 0; i < ArrayCount(CategoryMedium); ++i)
{
if(!StringsDiffer(Topic, CategoryMedium[i][0]))
{
return;
}
}
FILE *TopicsFile;
char *TopicsBuffer;
// TODO(matt): Consider (optionally) pulling this path from the config
char TopicsPath[255];
CopyString(TopicsPath, "%s/cinera_topcs.css", TopicsDir);
if((TopicsFile = fopen("cinera_topics.css", "a+")))
{
fseek(TopicsFile, 0, SEEK_END);
int TopicsLength = ftell(TopicsFile);
fseek(TopicsFile, 0, SEEK_SET);
// TODO(matt): May this not just ClaimBuffer (if I can figure out how)?
if(!(TopicsBuffer = malloc(TopicsLength)))
{
perror("GenerateTopicColours");
return;
}
fread(TopicsBuffer, TopicsLength, 1, TopicsFile);
char *TopicsPtr = TopicsBuffer;
while(TopicsPtr - TopicsBuffer < TopicsLength)
{
TopicsPtr += StringLength(".category.");
if(!StringsDifferT(SanitisePunctuation(Topic), TopicsPtr, ' '))
{
free(TopicsBuffer);
fclose(TopicsFile);
return;
}
while(TopicsPtr - TopicsBuffer < TopicsLength && *TopicsPtr != '\n')
{
++TopicsPtr;
}
++TopicsPtr;
}
hsl_colour Colour;
StringToColourHash(&Colour, Topic);
fprintf(TopicsFile, ".category.%s { border: 1px solid hsl(%d, %d%%, %d%%); background: hsl(%d, %d%%, %d%%); }\n",
SanitisePunctuation(Topic), Colour.Hue, Colour.Saturation, Colour.Lightness, Colour.Hue, Colour.Saturation, Colour.Lightness);
fclose(TopicsFile);
free(TopicsBuffer);
}
else
{
perror("GenerateTopicColours");
return;
}
}
#define CONFIG 0
#if CONFIG
typedef struct
{
char *Username;
char *Display_Name;
char *Homepage;
char *Funding_Platform;
char *Funding_Username;
unsigned int Index;
} credentials;
typedef struct
{
credentials Credentials;
} config;
int
ParseConfig(buffer *Buffer, char *Username)
{
/*
Essentially, I want to pass a Username to this, and have it write the credentials into the Config buffer
Let's start by just grabbing the stuff and printing it out
*/
// TODO(matt): Actually figure out the "standard" config location
char Config_Location[255];
if(getenv("XDG_CONFIG_HOME"))
{
sprintf(Config_Location, "%s/hmml.conf", getenv("XDG_CONFIG_HOME"));
}
else if(getenv("HOME"))
{
sprintf(Config_Location, "%s/.config/hmml.conf", getenv("HOME"));
}
else
{
fprintf(stderr, "Config file location not set");
return 1;
}
FILE *InFile;
if(!(InFile = fopen(Config_Location, "r")))
{
perror(Config_Location);
return 2;
}
printf("Reading: %s\n", Config_Location);
fseek(InFile, 0, SEEK_END);
int InSize = ftell(InFile);
fseek(InFile, 0, SEEK_SET);
char *InBuffer;
//config Config = { 0 };
if(!(InBuffer = malloc(InSize)))
{
perror("ParseConfig");
return 3;
}
fread(InBuffer, InSize, 1, InFile);
fclose(InFile);
char *InPtr = InBuffer;
char OutBuffer[256];
//char *OutPtr = Config.Credentials.Display_Name;
char *OutPtr = OutBuffer;
bool Quoted = FALSE;
bool FoundCredentials, ParsingUsername = FALSE;
unsigned int ScopeDepth = 0;
while(InPtr - InBuffer < InSize)
{
switch(*InPtr)
{
case '#':
{
if(!Quoted)
{
printf(" We are commenting\n");
while(InPtr - InBuffer < InSize && *InPtr != '\n')
{
++InPtr;
}
++InPtr;
while(InPtr - InBuffer < InSize && (*InPtr == ' ' || *InPtr == '\n'))
{
++InPtr;
}
}
else
{
*OutPtr++ = *InPtr++;
}
break;
}
case '{':
{
if(!Quoted)
{
++ScopeDepth;
++InPtr;
while(*InPtr == '\n' || *InPtr == ' ')
{
++InPtr;
}
printf(" We have entered a scope\n");
}
else
{
*OutPtr++ = *InPtr++;
}
break;
}
case '}':
{
if(!Quoted)
{
--ScopeDepth;
++InPtr;
printf(" We have left a scope\n");
}
else
{
*OutPtr++ = *InPtr++;
}
break;
}
#if 1
case ' ':
{
if(!Quoted)
{
++InPtr;
*OutPtr = '\0';
OutPtr = OutBuffer;
printf("%s\n", OutBuffer);
// TODO(matt): Switch on the OutBuffer? I have a feeling that isn't actually possible, though
if(!StringsDiffer("credentials", OutBuffer))
{
FoundCredentials = TRUE;
printf(" We have found the credentials block\n");
}
if(ParsingUsername)
{
printf(" The username is %s\n", OutBuffer);
ParsingUsername = FALSE;
}
if(FoundCredentials && (!StringsDiffer("username", OutBuffer)))
{
ParsingUsername = TRUE;
printf(" We have found the username\n");
}
}
else
{
*OutPtr++ = *InPtr++;
}
break;
}
#endif
case '"':
{
if(!Quoted)
{
Quoted = TRUE;
printf(" We are quoting!\n");
}
else
{
Quoted = FALSE;
printf(" We are no longer quoting!\n");
}
++InPtr;
break;
}
case ';':
{
if(!Quoted)
{
printf(" We have reached the end of a setting\n");
++InPtr;
}
else
{
*OutPtr++ = *InPtr++;
}
}
case '\n':
{
if(!Quoted)
{
if(InPtr - InBuffer < InSize)
{
*OutPtr = '\0';
OutPtr = OutBuffer;
// TODO(matt)
if(!StringsDiffer("credentials", OutBuffer))
{
FoundCredentials = TRUE;
printf(" We have found the credentials block\n");
}
if(ParsingUsername)
{
printf(" The username is %s\n", OutBuffer);
ParsingUsername = FALSE;
}
if(FoundCredentials && (!StringsDiffer("username", OutBuffer)))
{
ParsingUsername = TRUE;
printf(" We have found the username\n");
}
printf("%s\n", OutBuffer);
++InPtr;
while(InPtr - InBuffer < InSize && *InPtr == ' ') // NOTE(matt): Skip indentation whitespace
{
++InPtr;
}
}
}
else
{
*OutPtr++ = *InPtr++;
}
if(InPtr - InBuffer == InSize)
{
printf(" We have reached the EOF\n");
}
break;
}
default:
{
*OutPtr++ = *InPtr++;
break;
}
}
}
#if 0
while(InPtr - InBuffer < InSize)
{
while(*InPtr != '\n')
{
*OutPtr++ = *InPtr++;
}
*OutPtr = '\0';
printf("%s\n", Config.Credentials.Display_Name);
}
#endif
free(InBuffer);
// Reading from the config file, parsing it inline (on the stack) and writing into the buffer *Config
return 0;
}
#endif
void
PrintUsage(char *BinaryLocation, char *DefaultCSSDir, char *DefaultImagesDir, char *DefaultJSDir, char *DefaultDefaultMedium, char *DefaultOutLocation, char *DefaultQuoteDir, char *DefaultTemplateLocation)
{
fprintf(stderr, "Usage: %s [option(s)] filename(s)\n"
"\n"
"Options:\n"
" -c <CSS directory path>\n"
" Override default CSS directory (\"%s\")\n"
" -i <images directory path>\n"
" Override default images directory (\"%s\")\n"
" -j <JS directory path>\n"
" Override default JS directory (\"%s\")\n"
" -m <default medium>\n"
" Override default default medium (\"%s\")\n"
" -o <output location>\n"
" Override default output location (\"%s\")\n"
" -q <quotes directory path>\n"
" Override default quotes directory (\"%s\")\n"
" -t <template location>\n"
" Override default template location (\"%s\")\n"
" and automatically enable integration\n"
//" -c config location\n"
" -h\n"
" display this help\n"
"\n"
"Environment Variables:\n"
" CINERA_MODE\n"
" =INTEGRATE\n"
" Enable integration\n",
BinaryLocation, DefaultCSSDir, DefaultImagesDir, DefaultJSDir, DefaultDefaultMedium, DefaultQuoteDir, DefaultOutLocation, DefaultTemplateLocation);
}
int
main(int ArgC, char **Args)
{
// TODO(matt): Read all defaults from the config and probably validate the
// template up front
char *DefaultCSSDir = ".";
char *CSSDir = DefaultCSSDir;
char *DefaultImagesDir = ".";
char *ImagesDir = DefaultImagesDir;
char *DefaultJSDir = ".";
char *JSDir = DefaultJSDir;
char *DefaultDefaultMedium = "programming";
char *DefaultMedium = DefaultDefaultMedium;
char *DefaultTemplateLocation = "template.html";
char *TemplateLocation = DefaultTemplateLocation;
char *DefaultOutLocation = "out.html";
char *OutLocation = DefaultOutLocation;
char *DefaultOutIntegratedLocation = "out_integrated.html";
char *OutIntegratedLocation = DefaultOutIntegratedLocation;
char *DefaultQuoteDir = "/home/matt/git/GitHub/insofaras/25fc16d58a297a486334";
char *QuoteDir = DefaultQuoteDir;
char *CINERA_MODE = getenv("CINERA_MODE");
if(ArgC < 2)
{
PrintUsage(Args[0], DefaultCSSDir, DefaultImagesDir, DefaultJSDir, DefaultDefaultMedium, DefaultQuoteDir, DefaultOutLocation, DefaultTemplateLocation);
return 1;
}
char CommandLineArg;
while((CommandLineArg = getopt(ArgC, Args, "c:i:j:m:o:q:t:h")) != -1)
{
switch(CommandLineArg)
{
case 'c':
CSSDir = optarg;
break;
case 'i':
ImagesDir = optarg;
break;
case 'j':
JSDir = optarg;
break;
case 'm':
DefaultMedium = optarg;
break;
case 'o':
OutLocation = optarg;
OutIntegratedLocation = optarg;
break;
case 'q':
QuoteDir = optarg;
case 't':
TemplateLocation = optarg;
CINERA_MODE="INTEGRATE";
break;
//case 'c':
// Override config path, once we even have a default!
case 'h':
default:
PrintUsage(Args[0], DefaultCSSDir, DefaultImagesDir, DefaultJSDir, DefaultDefaultMedium, DefaultQuoteDir, DefaultOutLocation, DefaultTemplateLocation);
return 1;
}
}
if(optind == ArgC)
{
fprintf(stderr, "%s: requires at least one input .hmml file\n", Args[0]);
PrintUsage(Args[0], DefaultCSSDir, DefaultImagesDir, DefaultJSDir, DefaultDefaultMedium, DefaultQuoteDir, DefaultOutLocation, DefaultTemplateLocation);
return 1;
}
if(CSSDir[StringLength(CSSDir) - 1] == '/')
{
CSSDir[StringLength(CSSDir) - 1] = '\0';
}
bool ValidDefaultMedium = FALSE;
for(int i = 0; i < ArrayCount(CategoryMedium); ++i)
{
if(!StringsDiffer(DefaultMedium, CategoryMedium[i][0]))
{
ValidDefaultMedium = TRUE;
break;
}
}
if(!ValidDefaultMedium)
{
fprintf(stderr, "Specified default medium \"%s\" not available. Valid media are:\n", DefaultMedium);
for(int i = 0; i < ArrayCount(CategoryMedium); ++i)
{
fprintf(stderr, " %s\n", CategoryMedium[i][0]);
}
fprintf(stderr, "To have \"%s\" added to the list, contact matt@handmadedev.org\n", DefaultMedium);
return 1;
}
if(ImagesDir[StringLength(ImagesDir) - 1] == '/')
{
ImagesDir[StringLength(ImagesDir) - 1] = '\0';
}
if(JSDir[StringLength(JSDir) - 1] == '/')
{
JSDir[StringLength(JSDir) - 1] = '\0';
}
if(QuoteDir[StringLength(QuoteDir) - 1] == '/')
{
QuoteDir[StringLength(QuoteDir) - 1] = '\0';
}
// NOTE(matt): Init MemoryArena
buffer MemoryArena;
MemoryArena.Size = Megabytes(4);
if(!(MemoryArena.Location = calloc(MemoryArena.Size, 1)))
{
perror(Args[0]);
return 1;
}
int ClaimedMemory = 0;
// NOTE(matt): Setup buffers and ptrs
char *InPtr;
#if CONFIG
buffer Config;
#endif
// NOTE(matt): Tree structure of buffer dependencies
// Master
// Includes
// Menus
// QuoteMenu
// ReferenceMenu
// FilterMenu
// FilterTopics
// FilterMedia
// Player
// Colour
// Annotation
// AnnotationHeader
// AnnotationClass
// AnnotationData
// Text
// Category
// Script
// FilterState
buffer Master;
buffer Includes;
buffer Menus;
buffer QuoteMenu;
buffer ReferenceMenu;
buffer FilterMenu;
buffer FilterTopics;
buffer FilterMedia;
buffer CreditsMenu;
buffer HostInfo;
buffer AnnotatorInfo;
buffer Player;
buffer Colour;
buffer Annotation;
buffer AnnotationHeader;
buffer AnnotationClass;
buffer AnnotationData;
buffer Text;
buffer Category;
buffer Script;
buffer FilterState;
for(int FileIndex = optind; FileIndex < ArgC; ++FileIndex)
{
FILE *InFile;
if(!(InFile = fopen(Args[FileIndex], "r")))
{
perror(Args[FileIndex]);
free(MemoryArena.Location);
return 1;
}
HMML_Output HMML = hmml_parse_file(InFile);
fclose(InFile);
#if CONFIG
ClaimBuffer(MemoryArena, &ClaimedMemory, &Config, "Config", Kilobytes(1));
#endif
if(HMML.well_formed)
{
#if DEBUG
printf(
"================================================================================\n"
"%s\n"
"================================================================================\n",
Args[FileIndex]);
#endif
// NOTE(matt): Tree structure of "global" buffer dependencies
// Master
// Includes
// Menus
// QuoteMenu
// ReferenceMenu
// FilterMenu
// FilterTopics
// FilterMedia
// CreditsMenu
// Player
// Colour
// Annotation
// Script
// FilterState
ClaimBuffer(&MemoryArena, &ClaimedMemory, &Master, "Master", Kilobytes(512));
ClaimBuffer(&MemoryArena, &ClaimedMemory, &Includes, "Includes", Kilobytes(1));
ClaimBuffer(&MemoryArena, &ClaimedMemory, &Menus, "Menus", Kilobytes(24));
ClaimBuffer(&MemoryArena, &ClaimedMemory, &QuoteMenu, "QuoteMenu", Kilobytes(16));
ClaimBuffer(&MemoryArena, &ClaimedMemory, &ReferenceMenu, "ReferenceMenu", Kilobytes(16));
ClaimBuffer(&MemoryArena, &ClaimedMemory, &FilterMenu, "FilterMenu", Kilobytes(16));
ClaimBuffer(&MemoryArena, &ClaimedMemory, &FilterTopics, "FilterTopics", Kilobytes(8));
ClaimBuffer(&MemoryArena, &ClaimedMemory, &FilterMedia, "FilterMedia", Kilobytes(8));
ClaimBuffer(&MemoryArena, &ClaimedMemory, &CreditsMenu, "CreditsMenu", Kilobytes(8));
ClaimBuffer(&MemoryArena, &ClaimedMemory, &HostInfo, "HostInfo", Kilobytes(1));
ClaimBuffer(&MemoryArena, &ClaimedMemory, &AnnotatorInfo, "AnnotatorInfo", Kilobytes(1));
ClaimBuffer(&MemoryArena, &ClaimedMemory, &Player, "Player", Kilobytes(256));
ClaimBuffer(&MemoryArena, &ClaimedMemory, &Colour, "Colour", 32);
ClaimBuffer(&MemoryArena, &ClaimedMemory, &Annotation, "Annotation", Kilobytes(8));
ClaimBuffer(&MemoryArena, &ClaimedMemory, &Script, "Script", Kilobytes(8));
ClaimBuffer(&MemoryArena, &ClaimedMemory, &FilterState, "FilterState", Kilobytes(4));
ref_info ReferencesArray[200] = { 0 };
category_info TopicsArray[56] = { 0 };
category_info MediaArray[8] = { 0 };
bool HasQuoteMenu = FALSE;
bool HasReferenceMenu = FALSE;
bool HasFilterMenu = FALSE;
bool HasCreditsMenu = FALSE;
int QuoteIdentifier = 0x3b1;
int RefIdentifier = 1;
int UniqueRefs = 0;
int UniqueTopics = 0;
int UniqueMedia = 0;
CopyStringToBuffer(&Menus,
" <div class=\"title %s\">\n"
" <span class=\"episode_name\">", HMML.metadata.project);
CopyStringToBufferHTMLSafe(&Menus, HMML.metadata.title);
CopyStringToBuffer(&Menus, "</span>\n"
" <span id=\"focus-warn\">⚠ Click here to regain focus ⚠</span>\n");
CopyStringToBuffer(&Player,
" <div class=\"player_container\">\n"
" <div class=\"video_container\" data-videoId=\"%s\"></div>\n"
" <div class=\"markers_container %s\">\n", HMML.metadata.id, HMML.metadata.project);
// TODO(matt): Handle multiple annotators
if(HMML.metadata.annotator_count > 0)
{
BuildCredits(&CreditsMenu, &HostInfo, &AnnotatorInfo, &HasCreditsMenu, ImagesDir, HMML.metadata.member, HMML.metadata.annotators[0]);
}
else
{
fprintf(stderr, "%s: Missing annotator in [video] node\n", Args[FileIndex]);
hmml_free(&HMML);
free(MemoryArena.Location);
return 1;
}
#if DEBUG
printf(" --- Entering Annotations Loop ---\n");
#endif
for(int AnnotationIndex = 0; AnnotationIndex < HMML.annotation_count; ++AnnotationIndex)
{
#if DEBUG
printf("%d\n", AnnotationIndex);
#endif
HMML_Annotation *Anno = HMML.annotations + AnnotationIndex;
bool HasCategory = FALSE;
bool HasMedium = FALSE;
bool HasQuote = FALSE;
bool HasReference = FALSE;
quote_info QuoteInfo = { 0 };
// NOTE(matt): Tree structure of "annotation local" buffer dependencies
// AnnotationHeader
// AnnotationClass
// AnnotationData
// Text
// Category
ClaimBuffer(&MemoryArena, &ClaimedMemory, &AnnotationHeader, "AnnotationHeader", 512);
ClaimBuffer(&MemoryArena, &ClaimedMemory, &AnnotationClass, "AnnotationClass", 256);
ClaimBuffer(&MemoryArena, &ClaimedMemory, &AnnotationData, "AnnotationData", 512);
ClaimBuffer(&MemoryArena, &ClaimedMemory, &Text, "Text", Kilobytes(4));
ClaimBuffer(&MemoryArena, &ClaimedMemory, &Category, "Category", 512);
CopyStringToBuffer(&AnnotationHeader,
" <div data-timestamp=\"%d\"",
TimecodeToSeconds(Anno->time));
CopyStringToBuffer(&AnnotationClass,
" class=\"marker");
if(Anno->author)
{
if(!HasFilterMenu)
{
HasFilterMenu = TRUE;
}
BuildFilter(TopicsArray, &UniqueTopics, MediaArray, &UniqueMedia, "authored");
CopyStringToBuffer(&AnnotationClass, " authored");
hsl_colour AuthorColour;
StringToColourHash(&AuthorColour, Anno->author);
if(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);
}
if(!HasMedium)
{
HasMedium = TRUE;
}
}
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(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(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)
{
GenerateTopicColours(&Colour, Anno->markers[MarkerIndex].marker, CSSDir);
// TODO(matt): Maybe stuff this into BuildCategories
if(!HasFilterMenu)
{
HasFilterMenu = TRUE;
}
BuildFilter(TopicsArray, &UniqueTopics, MediaArray, &UniqueMedia, Anno->markers[MarkerIndex].marker);
BuildCategories(&AnnotationClass, &Category, &MarkerIndex, &HasCategory, &HasMedium, 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)
{
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", Args[FileIndex], Anno->line);
hmml_free(&HMML);
free(MemoryArena.Location);
return 1;
}
++ReferencesArray[RefIdentifier - 1].IdentifierCount;
++UniqueRefs;
HasReferenceMenu = TRUE;
}
else
{
for(int i = 0; i < UniqueRefs; ++i)
{
if(ReferencesArray[i].IdentifierCount == REF_MAX_IDENTIFIER)
{
fprintf(stderr, "%s:%d: Too many timecodes associated with one reference (increase REF_MAX_IDENTIFIER)\n", Args[FileIndex], Anno->line);
hmml_free(&HMML);
free(MemoryArena.Location);
return 1;
}
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
{
fprintf(stderr, "%s:%d: Reference must have an ISBN or URL\n", Args[FileIndex], Anno->line);
hmml_free(&HMML);
free(MemoryArena.Location);
return 1;
}
}
if(BuildReference(ReferencesArray, RefIdentifier, UniqueRefs, *CurrentRef, *Anno) == 1)
{
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", Args[FileIndex], Anno->line);
hmml_free(&HMML);
free(MemoryArena.Location);
return 1;
}
++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
{
fprintf(stderr, "%s:%d: Reference must have an ISBN or URL\n", Args[FileIndex], Anno->line);
hmml_free(&HMML);
free(MemoryArena.Location);
return 1;
}
HasReference = TRUE;
}
else
{
if(CurrentRef->isbn)
{
CopyStringToBuffer(&AnnotationData, ",%s", CurrentRef->isbn);
}
else if(CurrentRef->url)
{
CopyStringToBuffer(&AnnotationData, ",%s", CurrentRef->url);
}
else
{
fprintf(stderr, "%s:%d: Reference must have an ISBN or URL", Args[FileIndex], Anno->line);
hmml_free(&HMML);
free(MemoryArena.Location);
return 1;
}
}
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++;
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;
if(BuildQuote(&QuoteInfo, Anno->quote.author ? Anno->quote.author : HMML.metadata.stream_username ? HMML.metadata.stream_username : HMML.metadata.member, Anno->quote.id, QuoteDir) == 1)
{
fprintf(stderr, "%s:%d: Quote #%s %d not found! Consider pulling the latest quotes\n",
Args[FileIndex],
Anno->line,
Anno->quote.author ? Anno->quote.author : HMML.metadata.stream_username ? HMML.metadata.stream_username : HMML.metadata.member,
Anno->quote.id);
hmml_free(&HMML);
free(MemoryArena.Location);
return 1;
}
CopyStringToBuffer(&QuoteMenu,
" <span data-id=\"&#%d;\" class=\"ref\">\n"
" <span class=\"ref_content\">\n"
" <div class=\"source\">Quote %d</div>\n"
" <div class=\"ref_title\">",
QuoteIdentifier,
Anno->quote.id);
CopyStringToBufferCSVSafe(&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",
Anno->quote.author ? Anno->quote.author : HMML.metadata.stream_username ? HMML.metadata.stream_username : HMML.metadata.member,
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)
{
GenerateTopicColours(&Colour, Anno->markers[MarkerIndex].marker, CSSDir);
// TODO(matt): Maybe stuff this into BuildCategories
if(!HasFilterMenu)
{
HasFilterMenu = TRUE;
}
if(Anno->markers[MarkerIndex].marker)
{
BuildFilter(TopicsArray, &UniqueTopics, MediaArray, &UniqueMedia, Anno->markers[MarkerIndex].marker);
}
BuildCategories(&AnnotationClass, &Category, &MarkerIndex, &HasCategory, &HasMedium, Anno->markers[MarkerIndex].marker);
}
if(!HasMedium)
{
BuildFilter(TopicsArray, &UniqueTopics, MediaArray, &UniqueMedia, DefaultMedium);
BuildCategories(&AnnotationClass, &Category, &MarkerIndex, &HasCategory, &HasMedium, DefaultMedium);
}
CopyStringToBuffer(&AnnotationClass, "\"");
CopyBuffer(&AnnotationHeader, &AnnotationClass);
if(HasQuote || HasReference)
{
CopyStringToBuffer(&AnnotationData, "\"");
CopyBuffer(&AnnotationHeader, &AnnotationData);
}
CopyStringToBuffer(&AnnotationHeader, ">\n");
CopyBuffer(&Annotation, &AnnotationHeader);
CopyStringToBuffer(&Annotation,
" <div class=\"content\"><span class=\"timecode\">%s</span>",
Anno->time);
if(HasCategory)
{
CopyStringToBuffer(&Category, "</span>");
CopyBuffer(&Text, &Category);
}
// NOTE(matt): This feels a bit janky...
*Text.Ptr = '\0';
CopyBuffer(&Annotation, &Text);
CopyStringToBuffer(&Annotation, "</div>\n"
" <div class=\"progress faded\">\n"
" <div class=\"content\"><span class=\"timecode\">%s</span>",
Anno->time);
CopyBuffer(&Annotation, &Text);
CopyStringToBuffer(&Annotation, "</div>\n"
" </div>\n"
" <div class=\"progress main\">\n"
" <div class=\"content\"><span class=\"timecode\">%s</span>",
Anno->time);
CopyBuffer(&Annotation, &Text);
CopyStringToBuffer(&Annotation, "</div>\n"
" </div>\n"
" </div>\n");
CopyBuffer(&Player, &Annotation);
// NOTE(matt): Tree structure of "annotation local" buffer dependencies
// Category
// Text
// AnnotationData
// AnnotationClass
// AnnotationHeader
DeclaimBuffer(&Category, &ClaimedMemory);
DeclaimBuffer(&Text, &ClaimedMemory);
DeclaimBuffer(&AnnotationData, &ClaimedMemory);
DeclaimBuffer(&AnnotationClass, &ClaimedMemory);
DeclaimBuffer(&AnnotationHeader, &ClaimedMemory);
Annotation.Ptr = Annotation.Location;
}
#if DEBUG
printf(" --- End of Annotations Loop ---\n\n");
#endif
if(HasQuoteMenu)
{
CopyStringToBuffer(&QuoteMenu,
" </div>\n"
" </div>\n");
CopyBuffer(&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(&Menus, &ReferenceMenu);
}
if(HasFilterMenu)
{
CopyStringToBuffer(&FilterState,
" var filterState = {\n");
for(int i = 0; i < UniqueTopics; ++i)
{
CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": false },\n",
TopicsArray[i].Marker, "topic");
}
for(int i = 0; i < UniqueMedia; ++i)
{
CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": false },\n",
MediaArray[i].Marker, "medium");
}
CopyStringToBuffer(&FilterState,
" };\n");
CopyStringToBuffer(&FilterMenu,
" <div class=\"menu filter\">\n"
" <span><img src=\"%s/cinera_icon_filter.png\"></span>\n"
" <div class=\"filter_container\">\n"
" <div class=\"filter_mode inclusive\">Filter mode: </div>\n"
" <div class=\"filters\">\n", ImagesDir);
{
bool HasTopic = FALSE;
bool HasMedium = FALSE;
for(int i = 0; i < UniqueTopics; ++i)
{
if(!HasTopic)
{
CopyStringToBuffer(&FilterMenu,
" <div class=\"filter_topics\">\n"
" <div class=\"filter_title\">Topics</div>\n");
HasTopic = TRUE;
}
CopyStringToBuffer(&FilterTopics,
" <div class=\"filter_content %s\">\n"
" <span class=\"icon category %s\"></span><span class=\"text\">%s</span>\n"
" </div>\n",
TopicsArray[i].Marker,
TopicsArray[i].Marker,
TopicsArray[i].Marker);
}
for(int i = 0; i < UniqueMedia; ++i)
{
if(!HasMedium)
{
CopyStringToBuffer(&FilterMedia,
" <div class=\"filter_media\">\n"
" <div class=\"filter_title\">Media</div>\n");
HasMedium = TRUE;
}
int j;
for(j = 0; j < ArrayCount(CategoryMedium); ++j)
{
if(!StringsDiffer(MediaArray[i].Marker, CategoryMedium[j][0]))
{
break;
}
}
CopyStringToBuffer(&FilterMedia,
" <div class=\"filter_content %s\">\n"
" <span class=\"icon\">%s</span><span class=\"text\">%s</span>\n"
" </div>\n",
MediaArray[i].Marker,
CategoryMedium[j][1],
CategoryMedium[j][2]
);
}
if(HasTopic)
{
CopyStringToBuffer(&FilterTopics,
" </div>\n");
CopyBuffer(&FilterMenu, &FilterTopics);
}
if(HasMedium)
{
CopyStringToBuffer(&FilterMedia,
" </div>\n");
CopyBuffer(&FilterMenu, &FilterMedia);
}
}
CopyStringToBuffer(&FilterMenu,
" </div>\n"
" </div>\n"
" </div>\n");
}
CopyBuffer(&Menus, &FilterMenu);
if(HasCreditsMenu)
{
CopyBuffer(&Menus, &CreditsMenu);
}
#if CONFIG
// TODO(matt): Here is where I test ParseConfig
ParseConfig(&Config, HMML.metadata.annotator);
#endif
CopyStringToBuffer(&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(&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(&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(&Menus,
"\n"
" <h2>Menu toggling</h2>\n");
if(HasQuoteMenu)
{
CopyStringToBuffer(&Menus,
" <span class=\"help_key\">q</span> <span class=\"help_text\">Quotes</span>\n");
}
else
{
CopyStringToBuffer(&Menus,
" <span class=\"help_key unavailable\">q</span> <span class=\"help_text unavailable\">Quotes</span>\n");
}
if(HasReferenceMenu)
{
CopyStringToBuffer(&Menus,
" <span class=\"help_key\">r</span> <span class=\"help_text\">References</span>\n");
}
else
{
CopyStringToBuffer(&Menus,
" <span class=\"help_key unavailable\">r</span> <span class=\"help_text unavailable\">References</span>\n");
}
if(HasFilterMenu)
{
CopyStringToBuffer(&Menus,
" <span class=\"help_key\">f</span> <span class=\"help_text\">Filter</span>\n");
}
else
{
CopyStringToBuffer(&Menus,
" <span class=\"help_key unavailable\">f</span> <span class=\"help_text unavailable\">Filter</span>\n");
}
if(HasCreditsMenu)
{
CopyStringToBuffer(&Menus,
" <span class=\"help_key\">c</span> <span class=\"help_text\">Credits</span>\n");
}
else
{
CopyStringToBuffer(&Menus,
" <span class=\"help_key unavailable\">c</span> <span class=\"help_text unavailable\">Credits</span>\n");
}
CopyStringToBuffer(&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(&Menus,
" <h2>Quotes ");
if(HasReferenceMenu)
{
CopyStringToBuffer(&Menus, "and References Menus</h2>\n");
}
else
{
CopyStringToBuffer(&Menus, "<span class=\"unavailable\">and References</span> Menus</h2>\n");
}
}
else
{
CopyStringToBuffer(&Menus,
" <h2><span class=\"unavailable\">Quotes");
if(HasReferenceMenu)
{
CopyStringToBuffer(&Menus, " and</span> References Menus</h2>\n");
}
else
{
CopyStringToBuffer(&Menus, " and References Menus</span></h2>\n");
}
}
if(HasQuoteMenu || HasReferenceMenu)
{
CopyStringToBuffer(&Menus,
" <span style=\"width: auto\" class=\"help_key\">Enter</span> <span class=\"help_text\">Jump to timecode</span><br>\n");
}
else
{
CopyStringToBuffer(&Menus,
" <span style=\"width: auto\" class=\"help_key unavailable\">Enter</span> <span class=\"help_text unavailable\">Jump to timecode</span><br>\n");
}
if(HasReferenceMenu)
{
CopyStringToBuffer(&Menus,
" <h2>References ");
if(HasCreditsMenu)
{
CopyStringToBuffer(&Menus, "and Credits Menus</h2>\n");
}
else
{
CopyStringToBuffer(&Menus, "<span class=\"unavailable\">and Credits</span> Menus</h2>\n");
}
}
else
{
CopyStringToBuffer(&Menus,
" <h2><span class=\"unavailable\">References");
if(HasCreditsMenu)
{
CopyStringToBuffer(&Menus, " and</span> Credits Menus</h2>\n");
}
else
{
CopyStringToBuffer(&Menus, " and Credits Menus</span></h2>\n");
}
}
if(HasReferenceMenu || HasCreditsMenu)
{
CopyStringToBuffer(&Menus,
" <span class=\"help_key\">o</span> <span class=\"help_text\">Open URL (in new tab)</span>\n");
}
else
{
CopyStringToBuffer(&Menus,
" <span class=\"help_key unavailable\">o</span> <span class=\"help_text unavailable\">Open URL (in new tab)</span>\n");
}
CopyStringToBuffer(&Menus,
"\n");
if(HasFilterMenu)
{
CopyStringToBuffer(&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(&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");
}
if(HasCreditsMenu)
{
CopyStringToBuffer(&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(&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(&Menus,
" </div>\n"
" </div>\n"
" </div>");
CopyStringToBuffer(&Player,
" </div>\n"
" </div>");
// TODO(matt): Maybe do something about indentation levels
CopyStringToBuffer(&Includes,
"<meta charset=\"UTF-8\">\n"
"\n"
" <!-- Load the player -->\n"
" <script type=\"text/javascript\" src=\"%s/cinera.js\"></script>\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%s/cinera.css\">\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%s/cinera__%s.css\">\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%s/cinera_topics.css\">",
JSDir,
CSSDir,
CSSDir,
HMML.metadata.project,
CSSDir);
CopyStringToBuffer(&Script,
" <script type=\"text/javascript\">\n"
"var menuState = [];\n");
CopyStringToBuffer(&Script,
"var quotesMenu = document.querySelector(\".quotes_container\");\n");
if(HasQuoteMenu)
{
CopyStringToBuffer(&Script,
"if(quotesMenu)\n"
"{\n"
"menuState.push(quotesMenu);\n"
"var quoteItems = quotesMenu.querySelectorAll(\".ref\");\n"
"for(var i = 0; i < quoteItems.length; ++i)\n"
"{\n"
" quoteItems[i].addEventListener(\"mouseenter\", function(ev) {\n"
" mouseOverQuotes(this);\n"
" })\n"
"};\n"
"var lastFocusedQuote = null;\n"
"}\n");
}
CopyStringToBuffer(&Script,
"var referencesMenu = document.querySelector(\".references_container\");\n");
if(HasReferenceMenu)
{
CopyStringToBuffer(&Script,
"if(referencesMenu)\n"
"{\n"
"menuState.push(referencesMenu);\n"
"var referenceItems = referencesMenu.querySelectorAll(\".ref\");\n"
"for(var i = 0; i < referenceItems.length; ++i)\n"
"{\n"
" referenceItems[i].addEventListener(\"mouseenter\", function(ev) {\n"
" mouseOverReferences(this);\n"
" })\n"
"};\n"
"var lastFocusedReference = null;\n"
"var lastFocusedIdentifier = null;\n"
"}\n");
}
CopyStringToBuffer(&Script,
"var filterMenu = document.querySelector(\".filter_container\");\n");
if(HasFilterMenu)
{
CopyStringToBuffer(&Script,
"if(filterMenu)\n"
"{\n"
" menuState.push(filterMenu);\n"
" var lastFocusedCategory = null;\n"
" var lastFocusedTopic = null;\n"
" var lastFocusedMedium = null;\n"
"\n"
" var filter = filterMenu.parentNode;\n"
"\n"
" var filterModeElement = filter.querySelector(\".filter_mode\");\n"
" filterModeElement.addEventListener(\"click\", function(ev) {\n"
" toggleFilterMode();\n"
" });\n"
"\n"
" var filterMode = filterModeElement.classList[1];\n"
" var filterItems = filter.querySelectorAll(\".filter_content\");\n"
" for(var i = 0; i < filterItems.length; ++i)\n"
" {\n"
" filterItems[i].addEventListener(\"mouseenter\", function(ev) {\n"
" navigateFilter(this);\n"
" })\n"
"\n"
" filterItems[i].addEventListener(\"click\", function(ev) {\n"
" filterItemToggle(this);\n"
" });\n"
"\n"
"%s"
" }\n"
"}\n", FilterState.Location);
}
CopyStringToBuffer(&Script,
"var creditsMenu = document.querySelector(\".credits_container\");\n");
if(HasCreditsMenu)
{
CopyStringToBuffer(&Script,
"if(creditsMenu)\n"
"{\n"
" menuState.push(creditsMenu);\n"
" var lastFocusedCreditItem = null;\n"
"\n"
" var creditItems = creditsMenu.querySelectorAll(\".person, .support\");\n"
" for(var i = 0; i < creditItems.length; ++i)\n"
" {\n"
" creditItems[i].addEventListener(\"mouseenter\", function(ev) {\n"
" if(this != lastFocusedCreditItem)\n"
" {\n"
" lastFocusedCreditItem.classList.remove(\"focused\");\n"
" lastFocusedCreditItem = this;\n"
" focusedElement = lastFocusedCreditItem;\n"
" focusedElement.classList.add(\"focused\");\n"
" }\n"
" })\n"
" }\n"
"}\n");
}
CopyStringToBuffer(&Script,
"var sourceMenus = document.querySelectorAll(\".menu\");\n"
"\n"
"var helpButton = document.querySelector(\".help\");\n"
"var helpDocumentation = helpButton.querySelector(\".help_container\");\n"
"helpButton.addEventListener(\"click\", function(ev) {\n"
" handleMouseOverMenu(this, ev.type);\n"
"})\n"
"\n"
"var focusedElement = null;\n"
"var focusedIdentifier = null;\n"
"\n"
"var playerContainer = document.querySelector(\".player_container\")\n"
"var player = new Player(playerContainer, onRefChanged);\n"
"window.addEventListener(\"resize\", function() { player.updateSize(); });\n"
"document.addEventListener(\"keydown\", function(ev) {\n"
" var key = ev.key;\n"
" if(ev.getModifierState(\"Shift\") && key == \" \")\n"
" {\n"
" key = \"capitalSpace\";\n"
" }\n"
"\n"
" if(handleKey(key) == true && focusedElement)\n"
" {\n"
" ev.preventDefault();\n"
" }\n"
"});\n"
"\n"
"for(var i = 0; i < sourceMenus.length; ++i)\n"
"{\n"
" sourceMenus[i].addEventListener(\"mouseenter\", function(ev) {\n"
" handleMouseOverMenu(this, ev.type);\n"
" })\n"
" sourceMenus[i].addEventListener(\"mouseleave\", function(ev) {\n"
" handleMouseOverMenu(this, ev.type);\n"
" })\n"
"};\n"
"\n"
"var refTimecodes = document.querySelectorAll(\".refs .ref .timecode\");\n"
"for (var i = 0; i < refTimecodes.length; ++i) {\n"
" refTimecodes[i].addEventListener(\"click\", function(ev) {\n"
" if (player) {\n"
" var time = ev.currentTarget.getAttribute(\"data-timestamp\");\n"
" mouseSkipToTimecode(player, time, ev);\n"
" }\n"
" });\n"
"}\n"
"\n"
"var refSources = document.querySelectorAll(\".refs .ref\"); // This is for both quotes and refs\n"
"for (var i = 0; i < refSources.length; ++i) {\n"
" refSources[i].addEventListener(\"click\", function(ev) {\n"
" if (player) {\n"
" player.pause();\n"
" }\n"
" });\n"
"}\n"
"\n"
"var testMarkers = playerContainer.querySelectorAll(\".marker\");\n"
"\n"
"window.addEventListener(\"blur\", function(){\n"
" document.getElementById(\"focus-warn\").style.display = \"block\";\n"
"});\n"
"\n"
"window.addEventListener(\"focus\", function(){\n"
" document.getElementById(\"focus-warn\").style.display = \"none\";\n"
"});\n"
"\n"
"var colouredItems = playerContainer.querySelectorAll(\".author, .member, .project\");\n"
"for(i = 0; i < colouredItems.length; ++i)\n"
"{\n"
" setTextLightness(colouredItems[i]);\n"
"}\n"
"\n"
"var topicDots = document.querySelectorAll(\".category\");\n"
"for(var i = 0; i < topicDots.length; ++i)\n"
"{\n"
" setDotLightness(topicDots[i]);\n"
"}\n"
"\n"
"if(location.hash) {\n"
" player.setTime(location.hash.startsWith('#') ? location.hash.substr(1) : location.hash);\n"
"}\n"
" </script>");
#if DEBUG
printf("Buffer Collation\n\n");
#endif
if(CINERA_MODE && !StringsDiffer(CINERA_MODE, "INTEGRATE"))
{
FILE *TemplateFile;
if(!(TemplateFile = fopen(TemplateLocation, "r")))
{
perror(Args[0]); hmml_free(&HMML); free(MemoryArena.Location); return 1;
}
fseek(TemplateFile, 0, SEEK_END);
buffer Template;
Template.ID = "Template";
Template.Size = ftell(TemplateFile);
fseek(TemplateFile, 0, SEEK_SET);
if(!(Template.Location = malloc(Template.Size)))
{
perror(Args[0]); hmml_free(&HMML); free(MemoryArena.Location); return 1;
}
Template.Ptr = Template.Location;
fread(Template.Location, Template.Size, 1, TemplateFile);
fclose(TemplateFile);
buffer Output;
Output.Size = Template.Size + Master.Size;
Output.ID = "Output";
if(!(Output.Location = malloc(Output.Size)))
{
perror(Args[0]); free(Template.Location); hmml_free(&HMML); free(MemoryArena.Location); return 1;
}
Output.Ptr = Output.Location;
char *IncludesTag = "__CINERA_INCLUDES__";
char *TitleTag = "__CINERA_TITLE__";
char *MenusTag = "__CINERA_MENUS__";
char *PlayerTag = "__CINERA_PLAYER__";
char *ScriptTag = "__CINERA_SCRIPT__";
bool FoundIncludes = FALSE;
bool FoundMenus = FALSE;
bool FoundPlayer = FALSE;
bool FoundScript = FALSE;
while(Template.Ptr - Template.Location < Template.Size)
{
if(*Template.Ptr == '!' && (Template.Ptr > Template.Location && !StringsDifferT("<!--", &Template.Ptr[-1], 0)))
{
char *CommentStart = &Output.Ptr[-1];
while(Template.Ptr - Template.Location < Template.Size)
{
if(!(StringsDifferT(IncludesTag, Template.Ptr, 0)))
{
if(FoundIncludes == TRUE)
{
fprintf(stderr, "Template contains more than one <!-- __CINERA_INCLUDES__ --> tag\n");
free(Template.Location); free(Output.Location); hmml_free(&HMML); free(MemoryArena.Location); return 1;
}
FoundIncludes = TRUE;
Output.Ptr = CommentStart;
CopyBuffer(&Output, &Includes);
while(Template.Ptr - Template.Location < Template.Size)
{
if(!StringsDifferT("-->", Template.Ptr, 0))
{
Template.Ptr += StringLength("-->");
break;
}
++Template.Ptr;
}
break;
}
else if(!(StringsDifferT(TitleTag, Template.Ptr, 0)))
{
Output.Ptr = CommentStart;
CopyStringToBuffer(&Output, HMML.metadata.title);
while(Template.Ptr - Template.Location < Template.Size)
{
if(!StringsDifferT("-->", Template.Ptr, 0))
{
Template.Ptr += StringLength("-->");
break;
}
++Template.Ptr;
}
break;
}
else if(!(StringsDifferT(MenusTag, Template.Ptr, 0)))
{
if(FoundMenus == TRUE)
{
fprintf(stderr, "Template contains more than one <!-- __CINERA_MENUS__ --> tag\n");
free(Template.Location); free(Output.Location); hmml_free(&HMML); free(MemoryArena.Location); return 1;
}
FoundMenus = TRUE;
Output.Ptr = CommentStart;
CopyBuffer(&Output, &Menus);
while(Template.Ptr - Template.Location < Template.Size)
{
if(!StringsDifferT("-->", Template.Ptr, 0))
{
Template.Ptr += StringLength("-->");
break;
}
++Template.Ptr;
}
break;
}
else if(!(StringsDifferT(PlayerTag, Template.Ptr, 0)))
{
if(FoundPlayer == TRUE)
{
fprintf(stderr, "Template contains more than one <!-- __CINERA_PLAYER__ --> tag\n");
free(Template.Location); free(Output.Location); hmml_free(&HMML); free(MemoryArena.Location); return 1;
}
FoundPlayer = TRUE;
Output.Ptr = CommentStart;
CopyBuffer(&Output, &Player);
while(Template.Ptr - Template.Location < Template.Size)
{
if(!StringsDifferT("-->", Template.Ptr, 0))
{
Template.Ptr += StringLength("-->");
break;
}
++Template.Ptr;
}
break;
}
else if(!(StringsDifferT(ScriptTag, Template.Ptr, 0)))
{
if(FoundPlayer == FALSE)
{
fprintf(stderr, "<!-- __CINERA_SCRIPT__ --> must come after <!-- __CINERA_PLAYER__ -->\n");
free(Template.Location); free(Output.Location); hmml_free(&HMML); free(MemoryArena.Location); return 1;
}
if(FoundScript == TRUE)
{
fprintf(stderr, "Template contains more than one <!-- __CINERA_SCRIPT__ --> tag\n");
free(Template.Location); free(Output.Location); hmml_free(&HMML); free(MemoryArena.Location); return 1;
}
FoundScript = TRUE;
Output.Ptr = CommentStart;
CopyBuffer(&Output, &Script);
while(Template.Ptr - Template.Location < Template.Size)
{
if(!StringsDifferT("-->", Template.Ptr, 0))
{
Template.Ptr += StringLength("-->");
break;
}
++Template.Ptr;
}
break;
}
else if(!StringsDifferT("-->", Template.Ptr, 0))
{
break;
}
*Output.Ptr++ = *Template.Ptr++;
}
}
else
{
*Output.Ptr++ = *Template.Ptr++;
}
}
if(FoundIncludes && FoundMenus && FoundPlayer && FoundScript)
{
FILE *OutFile;
if(!(OutFile = fopen(OutIntegratedLocation, "w")))
{
perror(OutIntegratedLocation); free(Template.Location); free(Output.Location); hmml_free(&HMML); free(MemoryArena.Location); return 1;
}
fwrite(Output.Location, Output.Ptr - Output.Location, 1, OutFile);
fclose(OutFile);
}
else
{
fprintf(stderr, "Template is missing necessary tags:\n");
if(!FoundIncludes) { fprintf(stderr, " <!-- __CINERA_INCLUDES__ -->\n"); }
if(!FoundMenus) { fprintf(stderr, " <!-- __CINERA_MENUS__ -->\n"); }
if(!FoundPlayer) { fprintf(stderr, " <!-- __CINERA_PLAYER__ -->\n"); }
if(!FoundScript) { fprintf(stderr, " <!-- __CINERA_SCRIPT__ -->\n"); }
free(Template.Location); free(Output.Location); hmml_free(&HMML); free(MemoryArena.Location); return 1;
}
free(Template.Location);
free(Output.Location);
}
else
{
// NOTE(matt): Perform the normal collation into Master, and write-out to out.html
CopyStringToBuffer(&Master,
"<html>\n"
" <head>\n");
//NOTE(matt): Here is where we do all our CopyBuffer() calls
CopyBuffer(&Master, &Includes);
CopyStringToBuffer(&Master,
"\n"
" </head>\n"
" <body>\n");
CopyBuffer(&Master, &Menus);
CopyStringToBuffer(&Master, "\n");
CopyBuffer(&Master, &Player);
CopyStringToBuffer(&Master, "\n");
CopyBuffer(&Master, &Script);
CopyStringToBuffer(&Master, "\n");
//
CopyStringToBuffer(&Master,
" </body>\n"
"</html>\n");
FILE *OutFile;
if(!(OutFile = fopen(OutLocation, "w")))
{
perror(OutLocation); hmml_free(&HMML); free(MemoryArena.Location); return 1;
}
fwrite(Master.Location, Master.Ptr - Master.Location, 1, OutFile);
fclose(OutFile);
}
// NOTE(matt): Tree structure of "global" buffer dependencies
// FilterState
// Script
// Annotation
// Colour
// Player
// AnnotatorInfo
// HostInfo
// CreditsMenu
// FilterMedia
// FilterTopics
// FilterMenu
// ReferenceMenu
// QuoteMenu
// Menus
// Includes
// Master
DeclaimBuffer(&FilterState, &ClaimedMemory);
DeclaimBuffer(&Script, &ClaimedMemory);
DeclaimBuffer(&Annotation, &ClaimedMemory);
DeclaimBuffer(&Colour, &ClaimedMemory);
DeclaimBuffer(&Player, &ClaimedMemory);
DeclaimBuffer(&AnnotatorInfo, &ClaimedMemory);
DeclaimBuffer(&HostInfo, &ClaimedMemory);
DeclaimBuffer(&CreditsMenu, &ClaimedMemory);
DeclaimBuffer(&FilterMedia, &ClaimedMemory);
DeclaimBuffer(&FilterTopics, &ClaimedMemory);
DeclaimBuffer(&FilterMenu, &ClaimedMemory);
DeclaimBuffer(&ReferenceMenu, &ClaimedMemory);
DeclaimBuffer(&QuoteMenu, &ClaimedMemory);
DeclaimBuffer(&Menus, &ClaimedMemory);
DeclaimBuffer(&Includes, &ClaimedMemory);
DeclaimBuffer(&Master, &ClaimedMemory);
}
else
{
fprintf(stderr, "%s:%d: %s\n", Args[FileIndex], HMML.error.line, HMML.error.message);
}
hmml_free(&HMML);
}
free(MemoryArena.Location);
}