4078 lines
151 KiB
C
4078 lines
151 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
|
|
|
|
#define CINERA_VERSION "0.4.0"
|
|
|
|
#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_QUOTE,
|
|
RC_ERROR_TEMPLATE,
|
|
RC_FAILURE,
|
|
RC_FOUND,
|
|
RC_UNFOUND,
|
|
RC_INVALID_TEMPLATE,
|
|
RC_INVALID_REFERENCE,
|
|
RC_NOOP,
|
|
RC_REFRESHED,
|
|
RC_RIP,
|
|
RC_SUCCESS
|
|
} returns;
|
|
|
|
typedef struct
|
|
{
|
|
int Edition;
|
|
int LogLevel;
|
|
int Mode;
|
|
int UpdateInterval;
|
|
bool ForceIntegration;
|
|
char *RootDir;
|
|
char *BaseDir; // Relative to RootDir
|
|
char *CSSDir; // Relative to RootDir
|
|
char *ImagesDir; // Relative to RootDir
|
|
char *ProjectID;
|
|
char *JSDir; // Relative to RootDir
|
|
char *TemplateIndexLocation; // Relative to RootDir
|
|
char *TemplatePlayerLocation; // Relative to RootDir
|
|
char CacheDir[255];
|
|
char *DefaultMedium;
|
|
char *OutLocation;
|
|
char *OutIntegratedLocation;
|
|
char *ProjectDir;
|
|
} 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[255];
|
|
int FileSize;
|
|
} file_buffer;
|
|
|
|
enum
|
|
{
|
|
// Contents Page
|
|
TAG_INDEX,
|
|
|
|
// Player Page
|
|
TAG_INCLUDES,
|
|
TAG_MENUS,
|
|
TAG_PLAYER,
|
|
TAG_SCRIPT,
|
|
|
|
// Anywhere
|
|
TAG_TITLE
|
|
} template_tags;
|
|
|
|
typedef struct
|
|
{
|
|
int Code; // template_tags
|
|
char *Tag;
|
|
} tag;
|
|
|
|
tag Tags[] = {
|
|
{ TAG_INDEX, "__CINERA_INDEX__" },
|
|
{ TAG_INCLUDES, "__CINERA_INCLUDES__" },
|
|
{ TAG_MENUS, "__CINERA_MENUS__" },
|
|
{ TAG_PLAYER, "__CINERA_PLAYER__" },
|
|
{ TAG_SCRIPT, "__CINERA_SCRIPT__" },
|
|
{ TAG_TITLE, "__CINERA_TITLE__" },
|
|
};
|
|
|
|
typedef struct
|
|
{
|
|
int Offset;
|
|
int TagCode;
|
|
} tag_offset;
|
|
|
|
typedef struct
|
|
{
|
|
char Filename[120];
|
|
tag_offset Tag[16];
|
|
int Validity; // NOTE(matt): Bitmask describing which page the template is valid for, i.e. contents and / or player page
|
|
int TagCount;
|
|
} template_metadata;
|
|
|
|
typedef struct
|
|
{
|
|
template_metadata Metadata;
|
|
buffer Buffer;
|
|
} template;
|
|
|
|
typedef struct
|
|
{
|
|
buffer IncludesIndex;
|
|
buffer Index;
|
|
buffer IncludesPlayer;
|
|
buffer Menus;
|
|
buffer Player;
|
|
buffer Script;
|
|
char Title[255];
|
|
} buffers;
|
|
|
|
// TODO(matt): Consider putting the ref_info and quote_info into linked lists on the heap, just to avoid all the hardcoded sizes
|
|
|
|
typedef struct
|
|
{
|
|
char Date[32];
|
|
char Text[512];
|
|
} quote_info;
|
|
|
|
typedef struct
|
|
{
|
|
char Timecode[8];
|
|
int Identifier;
|
|
} identifier;
|
|
|
|
#define REF_MAX_IDENTIFIER 64
|
|
|
|
typedef struct
|
|
{
|
|
char RefTitle[620];
|
|
char ID[512];
|
|
char URL[512];
|
|
char Source[256];
|
|
identifier Identifier[REF_MAX_IDENTIFIER];
|
|
int IdentifierCount;
|
|
} ref_info;
|
|
|
|
typedef struct
|
|
{
|
|
char Marker[32];
|
|
char WrittenText[32];
|
|
} category_info;
|
|
|
|
typedef struct
|
|
{
|
|
category_info Category[64];
|
|
int Count;
|
|
} categories;
|
|
|
|
// TODO(matt): Parse this stuff out of a config file
|
|
typedef struct
|
|
{
|
|
char *Username;
|
|
char *CreditedName;
|
|
char *HomepageURL;
|
|
char *SupportIcon;
|
|
char *SupportURL;
|
|
} credential_info;
|
|
|
|
credential_info Credentials[] =
|
|
{
|
|
{ "Miblo", "Matt Mascarenhas", "http://miblodelcarpio.co.uk", "cinera_icon_patreon.png", "https://patreon.com/miblo"},
|
|
{ "miotatsu", "Mio Iwakura", "http://riscy.tv/", "cinera_icon_patreon.png", "https://patreon.com/miotatsu"},
|
|
{ "nothings", "Sean Barrett", "https://nothings.org/", "", ""},
|
|
{ "cmuratori", "Casey Muratori", "https://handmadehero.org", "cinera_icon_patreon.png", "https://patreon.com/cmuratori"},
|
|
{ "fierydrake", "Mike Tunnicliffe", "", "", ""},
|
|
{ "abnercoimbre", "Abner Coimbre", "https://handmade.network/m/abnercoimbre", "cinera_icon_patreon.png", "https://patreon.com/handmade_dev"},
|
|
{ "/y_lee", "Yunsup Lee", "https://www.linkedin.com/in/yunsup-lee-385b692b/", "", ""},
|
|
{ "/a_waterman", "Andrew Waterman", "https://www.linkedin.com/in/andrew-waterman-76805788", "", ""},
|
|
{ "debiatan", "Miguel Lechón", "http://blog.debiatan.net/", "", ""},
|
|
};
|
|
|
|
typedef struct
|
|
{
|
|
char *Medium;
|
|
char *Icon;
|
|
char *WrittenName;
|
|
} category_medium;
|
|
|
|
category_medium CategoryMedium[] =
|
|
{
|
|
// medium icon written name
|
|
{ "afk", "…" , "Away from Keyboard"}, // TODO(matt): Filter this out by default
|
|
{ "authored", "🗪", "Chat Comment"}, // TODO(matt): Conditionally handle Chat vs Guest Comments
|
|
{ "blackboard", "🖌", "Blackboard"},
|
|
{ "experience", "🍷", "Experience"},
|
|
{ "owl", "🦉", "Owl of Shame"},
|
|
{ "programming", "🖮", "Programming"}, // TODO(matt): Potentially make this configurable per project
|
|
{ "rant", "💢", "Rant"},
|
|
{ "research", "📖", "Research"},
|
|
{ "run", "🏃", "In-Game"}, // TODO(matt): Potentially make this configurable per project
|
|
{ "speech", "🗩", "Speech"},
|
|
{ "trivia", "🎲", "Trivia"},
|
|
};
|
|
|
|
#define ArrayCount(A) sizeof(A)/sizeof(*(A))
|
|
|
|
__attribute__ ((format (printf, 2, 3)))
|
|
void
|
|
CopyString(char Dest[], char *Format, ...)
|
|
{
|
|
va_list Args;
|
|
va_start(Args, Format);
|
|
vsprintf(Dest, Format, Args);
|
|
va_end(Args);
|
|
}
|
|
|
|
int
|
|
StringLength(char *String)
|
|
{
|
|
int i = 0;
|
|
while(String[i])
|
|
{
|
|
++i;
|
|
}
|
|
return i;
|
|
}
|
|
|
|
void
|
|
CopyBuffer(buffer *Dest, buffer *Src)
|
|
{
|
|
Src->Ptr = Src->Location;
|
|
while(*Src->Ptr)
|
|
{
|
|
// TODO(matt)
|
|
{
|
|
if(Dest->Ptr - Dest->Location >= Dest->Size)
|
|
{
|
|
fprintf(stderr, "CopyBuffer: %s cannot accommodate %s\n", Dest->ID, Src->ID);
|
|
__asm__("int3");
|
|
}
|
|
}
|
|
*Dest->Ptr++ = *Src->Ptr++;
|
|
}
|
|
*Dest->Ptr = '\0';
|
|
}
|
|
|
|
int
|
|
CopyStringNoFormat(char *Dest, char *String)
|
|
{
|
|
int Length = 0;
|
|
while(*String)
|
|
{
|
|
*Dest++ = *String++;
|
|
++Length;
|
|
}
|
|
*Dest = '\0';
|
|
return Length;
|
|
}
|
|
|
|
// TODO(matt): Maybe do a version of this that takes a string as a Terminator
|
|
int
|
|
CopyStringNoFormatT(char *Dest, char *String, char Terminator)
|
|
{
|
|
int Length = 0;
|
|
while(*String != Terminator)
|
|
{
|
|
*Dest++ = *String++;
|
|
++Length;
|
|
}
|
|
*Dest = '\0';
|
|
return Length;
|
|
}
|
|
|
|
__attribute__ ((format (printf, 2, 3)))
|
|
void
|
|
CopyStringToBuffer(buffer *Dest, char *Format, ...)
|
|
{
|
|
va_list Args;
|
|
va_start(Args, Format);
|
|
int Length = vsnprintf(Dest->Ptr, Dest->Size - (Dest->Ptr - Dest->Location), Format, Args);
|
|
va_end(Args);
|
|
// TODO(matt):
|
|
{
|
|
if(Length + (Dest->Ptr - Dest->Location) >= Dest->Size)
|
|
{
|
|
fprintf(stderr, "CopyStringToBuffer: %s cannot accommodate %d-character string:\n"
|
|
"\n"
|
|
"%s\n", Dest->ID, Length, Format);
|
|
__asm__("int3");
|
|
}
|
|
}
|
|
Dest->Ptr += Length;
|
|
}
|
|
|
|
void
|
|
CopyStringToBufferHTMLSafe(buffer *Dest, char *String)
|
|
{
|
|
while(*String)
|
|
{
|
|
if(Dest->Ptr - Dest->Location >= Dest->Size)
|
|
{
|
|
fprintf(stderr, "CopyStringToBufferHTMLSafe: %s cannot accommodate %d-character string\n", Dest->ID, StringLength(String));
|
|
__asm__("int3");
|
|
}
|
|
switch(*String)
|
|
{
|
|
case '<':
|
|
CopyStringToBuffer(Dest, "<");
|
|
String++;
|
|
break;
|
|
case '>':
|
|
CopyStringToBuffer(Dest, ">");
|
|
String++;
|
|
break;
|
|
case '&':
|
|
CopyStringToBuffer(Dest, "&");
|
|
String++;
|
|
break;
|
|
case '\"':
|
|
CopyStringToBuffer(Dest, """);
|
|
String++;
|
|
break;
|
|
case '\'':
|
|
CopyStringToBuffer(Dest, "'");
|
|
String++;
|
|
break;
|
|
default:
|
|
*Dest->Ptr++ = *String++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
int
|
|
StringsDiffer(char *A, char *B) // NOTE(matt): Two null-terminated strings
|
|
{
|
|
while(*A && *B && *A == *B)
|
|
{
|
|
++A, ++B;
|
|
}
|
|
return *A - *B;
|
|
}
|
|
|
|
bool
|
|
StringsDifferT(char *A, // NOTE(matt): Null-terminated string
|
|
char *B, // NOTE(matt): Not null-terminated string (e.g. one mid-buffer)
|
|
char Terminator // NOTE(matt): Caller definable terminator. Pass 0 to only match on the extent of A
|
|
)
|
|
{
|
|
int ALength = StringLength(A);
|
|
int i = 0;
|
|
while(i < ALength && A[i] && A[i] == B[i])
|
|
{
|
|
++i;
|
|
}
|
|
if((!Terminator && !A[i] && ALength == i) ||
|
|
(!A[i] && ALength == i && (B[i] == Terminator)))
|
|
{
|
|
return FALSE;
|
|
}
|
|
else
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
int
|
|
MakeDir(char *Path)
|
|
{
|
|
// TODO(matt): Correctly check for permissions
|
|
int i = StringLength(Path);
|
|
int Ancestors = 0;
|
|
while(mkdir(Path, 00755) == -1)
|
|
{
|
|
if(errno == EACCES)
|
|
{
|
|
return RC_ERROR_DIRECTORY;
|
|
}
|
|
while(Path[i] != '/' && i > 0)
|
|
{
|
|
--i;
|
|
}
|
|
++Ancestors;
|
|
Path[i] = '\0';
|
|
if(i == 0) { return RC_ERROR_DIRECTORY; }
|
|
}
|
|
while(Ancestors > 0)
|
|
{
|
|
while(Path[i] != '\0')
|
|
{
|
|
++i;
|
|
}
|
|
Path[i] = '/';
|
|
--Ancestors;
|
|
if((mkdir(Path, 00755)) == -1)
|
|
{
|
|
return RC_ERROR_DIRECTORY;
|
|
}
|
|
}
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
void
|
|
LogUsage(buffer *Buffer)
|
|
{
|
|
#if DEBUG
|
|
char LogPath[255];
|
|
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[255];
|
|
CopyString(LogPath, "%s/%s", Config.CacheDir, "errors.log");
|
|
FILE *LogFile;
|
|
if(!(LogFile = fopen(LogPath, "a+")))
|
|
{
|
|
MakeDir(Config.CacheDir);
|
|
if(!(LogFile = fopen(LogPath, "a+")))
|
|
{
|
|
perror("LogUsage");
|
|
return;
|
|
}
|
|
}
|
|
|
|
va_list Args;
|
|
va_start(Args, Format);
|
|
vfprintf(LogFile, Format, Args);
|
|
va_end(Args);
|
|
// TODO(matt): Include the LogLevel "string" and the current wall time
|
|
fprintf(LogFile, "\n");
|
|
fclose(LogFile);
|
|
}
|
|
}
|
|
|
|
void
|
|
FreeBuffer(buffer *Buffer)
|
|
{
|
|
free(Buffer->Location);
|
|
#if 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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
CopyString((*Template)->Metadata.Filename, 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 = 26;
|
|
|
|
int i;
|
|
for(i = 0; String[i]; ++i)
|
|
{
|
|
Colour->Hue += CharToColour(String[i]).Hue;
|
|
Colour->Saturation += CharToColour(String[i]).Saturation;
|
|
}
|
|
|
|
Colour->Hue = Colour->Hue % 360;
|
|
Colour->Saturation = Colour->Saturation % 26 + 74;
|
|
return(Colour);
|
|
}
|
|
|
|
char *
|
|
SanitisePunctuation(char *String)
|
|
{
|
|
char *Ptr = String;
|
|
while(*Ptr)
|
|
{
|
|
if(*Ptr == ' ')
|
|
{
|
|
*Ptr = '_';
|
|
}
|
|
if((*Ptr < '0' || *Ptr > '9') &&
|
|
(*Ptr < 'a' || *Ptr > 'z') &&
|
|
(*Ptr < 'A' || *Ptr > 'Z'))
|
|
{
|
|
*Ptr = '-';
|
|
}
|
|
++Ptr;
|
|
}
|
|
return String;
|
|
}
|
|
|
|
enum
|
|
{
|
|
CreditsError_NoHost,
|
|
CreditsError_NoAnnotator,
|
|
CreditsError_NoCredentials
|
|
};
|
|
|
|
int
|
|
SearchCredentials(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)
|
|
{
|
|
if(Config.Edition == EDITION_PROJECT)
|
|
{
|
|
CopyStringToBuffer(CreditsMenu,
|
|
" <a class=\"support\" href=\"%s\" target=\"_blank\"><img src=\"../%s/%s\"></a>\n",
|
|
Credentials[CredentialIndex].SupportURL,
|
|
Config.ImagesDir,
|
|
Credentials[CredentialIndex].SupportIcon);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(CreditsMenu,
|
|
" <a class=\"support\" href=\"%s\" target=\"_blank\"><img src=\"%s/%s\"></a>\n",
|
|
Credentials[CredentialIndex].SupportURL,
|
|
Config.ImagesDir,
|
|
Credentials[CredentialIndex].SupportIcon);
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
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[255];
|
|
CopyString(QuoteCacheDir, "%s/quotes", Config.CacheDir);
|
|
char QuoteCachePath[255];
|
|
CopyString(QuoteCachePath, "%s/%s", QuoteCacheDir, Speaker);
|
|
FILE *QuoteCache;
|
|
char QuotesURL[256];
|
|
// TODO(matt): Make the URL configurable
|
|
CopyString(QuotesURL, "https://dev.abaines.me.uk/quotes/%s.raw", Speaker);
|
|
bool CacheAvailable = FALSE;
|
|
|
|
if(!(QuoteCache = fopen(QuoteCachePath, "a+")))
|
|
{
|
|
if(MakeDir(QuoteCacheDir) == RC_SUCCESS)
|
|
{
|
|
CacheAvailable = TRUE;
|
|
};
|
|
if(!(QuoteCache = fopen(QuoteCachePath, "a+")))
|
|
{
|
|
fprintf(stderr, "Unable to open quote cache %s: %s\n", QuoteCachePath, strerror(errno));
|
|
}
|
|
else
|
|
{
|
|
CacheAvailable = TRUE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CacheAvailable = TRUE;
|
|
}
|
|
|
|
buffer QuoteStaging;
|
|
QuoteStaging.ID = "QuoteStaging";
|
|
QuoteStaging.Size = Kilobytes(256);
|
|
if(!(QuoteStaging.Location = malloc(QuoteStaging.Size)))
|
|
{
|
|
fclose(QuoteCache);
|
|
return RC_ERROR_MEMORY;
|
|
}
|
|
|
|
#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";
|
|
|
|
// TODO(matt): Correctly locate cinera_topics.css once the config takes everything we need
|
|
//char TopicsPath[255];
|
|
CopyString(Topics.Path, "%s/cinera_topics.css", Config.CSSDir);
|
|
#if 0
|
|
printf("BaseDir: %s\n"
|
|
"CSSDir relative to BaseDir: %s\n", Config->BaseDir, Config->CSSDir);
|
|
#endif
|
|
|
|
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"
|
|
" -b <base output directory>\n"
|
|
" Override default base output directory (\"%s\"), relative to root\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"
|
|
" -p <project directory>\n"
|
|
" Override default project directory (\"%s\")\n"
|
|
"\n"
|
|
" -f\n"
|
|
" Force integration with an incomplete template\n"
|
|
" -I <project ID>\n"
|
|
" Set the project ID, corresponding to the \"project\" field in the HMML files\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"
|
|
" -h\n"
|
|
" display this help\n"
|
|
"\n"
|
|
"Environment Variables:\n"
|
|
" CINERA_MODE\n"
|
|
" =INTEGRATE\n"
|
|
" Enable integration\n"
|
|
"\n"
|
|
"Template:\n"
|
|
" A complete template shall contain exactly one each of the following tags:\n"
|
|
" <!-- __CINERA_INCLUDES__ -->\n"
|
|
" <!-- __CINERA_MENUS__ -->\n"
|
|
" <!-- __CINERA_PLAYER__ -->\n"
|
|
" <!-- __CINERA_SCRIPT__ --> (must come after <!-- __CINERA_PLAYER__ -->)\n"
|
|
" Other available tags include:\n"
|
|
" <!-- __CINERA_TITLE__ -->\n"
|
|
"\n"
|
|
"HMML Specification:\n"
|
|
" https://git.handmade.network/Annotation-Pushers/Annotation-System/wikis/hmmlspec\n",
|
|
BinaryLocation, DefaultConfig->RootDir, 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;
|
|
}
|
|
}
|
|
|
|
enum
|
|
{
|
|
PAGE_PLAYER = 1 << 0,
|
|
PAGE_INDEX = 1 << 1
|
|
} pages;
|
|
|
|
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_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;
|
|
}
|
|
}
|
|
|
|
//FreeBuffer(&(*Template)->Buffer);
|
|
|
|
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->Script);
|
|
RewindBuffer(&CollationBuffers->IncludesIndex);
|
|
|
|
char Filepath[255];
|
|
if(Config.Edition == EDITION_PROJECT)
|
|
{
|
|
CopyString(Filepath, "%s/%s", Config.ProjectDir, Filename);
|
|
}
|
|
else
|
|
{
|
|
CopyString(Filepath, "%s", Filename);
|
|
}
|
|
|
|
FILE *InFile;
|
|
if(!(InFile = fopen(Filepath, "r")))
|
|
{
|
|
LogError(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);
|
|
#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(16)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
|
|
if(ClaimBuffer(&ReferenceMenu, "ReferenceMenu", Kilobytes(16)) == 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=\"title %s\">\n"
|
|
" <span class=\"episode_name\">", 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=\"player_container\">\n"
|
|
" <div class=\"video_container\" data-videoId=\"%s\"></div>\n"
|
|
" <div class=\"markers_container %s\">\n", HMML.metadata.id, 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
|
|
for(int AnnotationIndex = 0; AnnotationIndex < HMML.annotation_count; ++AnnotationIndex)
|
|
{
|
|
#if DEBUG
|
|
printf("%d\n", AnnotationIndex);
|
|
#endif
|
|
HMML_Annotation *Anno = HMML.annotations + AnnotationIndex;
|
|
categories LocalTopics = { 0 };
|
|
categories LocalMedia = { 0 };
|
|
bool HasQuote = FALSE;
|
|
bool HasReference = FALSE;
|
|
|
|
quote_info QuoteInfo = { 0 };
|
|
|
|
// NOTE(matt): Tree structure of "annotation local" buffer dependencies
|
|
// Annotation
|
|
// AnnotationHeader
|
|
// AnnotationClass
|
|
// AnnotationData
|
|
// Text
|
|
// TopicDots
|
|
|
|
if(ClaimBuffer(&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 ▼</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, "<");
|
|
InPtr++;
|
|
break;
|
|
case '>':
|
|
CopyStringToBuffer(&Text, ">");
|
|
InPtr++;
|
|
break;
|
|
case '&':
|
|
CopyStringToBuffer(&Text, "&");
|
|
InPtr++;
|
|
break;
|
|
case '\"':
|
|
CopyStringToBuffer(&Text, """);
|
|
InPtr++;
|
|
break;
|
|
case '\'':
|
|
CopyStringToBuffer(&Text, "'");
|
|
InPtr++;
|
|
break;
|
|
default:
|
|
*Text.Ptr++ = *InPtr++;
|
|
*Text.Ptr = '\0';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(Anno->is_quote)
|
|
{
|
|
if(!HasQuoteMenu)
|
|
{
|
|
CopyStringToBuffer(&QuoteMenu,
|
|
" <div class=\"menu quotes\">\n"
|
|
" <span>Quotes ▼</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\">—%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, "“");
|
|
CopyStringToBufferHTMLSafe(&Text, QuoteInfo.Text);
|
|
CopyStringToBuffer(&Text, "”");
|
|
}
|
|
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=\"content\"><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=\"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(&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);
|
|
}
|
|
|
|
#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,
|
|
" var filterState = {\n");
|
|
for(int i = 0; i < Topics.Count; ++i)
|
|
{
|
|
CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": false },\n",
|
|
Topics.Category[i].Marker, "topic");
|
|
}
|
|
for(int i = 0; i < Media.Count; ++i)
|
|
{
|
|
CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": false },\n",
|
|
Media.Category[i].Marker, "medium");
|
|
}
|
|
CopyStringToBuffer(&FilterState,
|
|
" };\n");
|
|
|
|
if(Config.Edition == EDITION_PROJECT)
|
|
{
|
|
CopyStringToBuffer(&FilterMenu,
|
|
" <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", Config.ImagesDir);
|
|
}
|
|
else
|
|
{
|
|
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", Config.ImagesDir);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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
|
|
// TODO(matt): We may need to do some actual logic here to figure out
|
|
// where the style paths are in relation to us, rather than assuming them
|
|
// to be one directory up the tree
|
|
if(Config.Edition == EDITION_PROJECT)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->IncludesIndex,
|
|
"<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\">\n"
|
|
"\n"
|
|
" <meta charset=\"UTF-8\">\n"
|
|
" <meta name=\"generator\" content=\"Cinera\">\n",
|
|
Config.CSSDir,
|
|
Config.CSSDir,
|
|
HMML.metadata.project,
|
|
Config.CSSDir);
|
|
|
|
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
|
|
"<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\">\n",
|
|
Config.CSSDir,
|
|
Config.CSSDir,
|
|
HMML.metadata.project,
|
|
Config.CSSDir);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
|
|
"<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\">\n",
|
|
Config.CSSDir,
|
|
Config.CSSDir,
|
|
HMML.metadata.project,
|
|
Config.CSSDir);
|
|
}
|
|
|
|
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
|
|
"\n"
|
|
" <meta charset=\"UTF-8\">\n"
|
|
" <meta name=\"generator\" content=\"Cinera\">\n");
|
|
|
|
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");
|
|
}
|
|
|
|
if(Config.Edition == EDITION_PROJECT)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
|
|
" <script type=\"text/javascript\" src=\"../%s/cinera.js\"></script>\n",
|
|
Config.JSDir);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
|
|
" <script type=\"text/javascript\" src=\"%s/cinera.js\"></script>\n",
|
|
Config.JSDir);
|
|
}
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Script,
|
|
" <script type=\"text/javascript\">\n"
|
|
"var menuState = [];\n");
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Script,
|
|
"var quotesMenu = document.querySelector(\".quotes_container\");\n");
|
|
|
|
if(HasQuoteMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->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(&CollationBuffers->Script,
|
|
"var referencesMenu = document.querySelector(\".references_container\");\n");
|
|
|
|
if(HasReferenceMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->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(&CollationBuffers->Script,
|
|
"var filterMenu = document.querySelector(\".filter_container\");\n");
|
|
|
|
if(HasFilterMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->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(&CollationBuffers->Script,
|
|
"var creditsMenu = document.querySelector(\".credits_container\");\n");
|
|
|
|
if(HasCreditsMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->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(&CollationBuffers->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>");
|
|
|
|
// 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_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->Script);
|
|
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->Script);
|
|
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;
|
|
}
|
|
|
|
int
|
|
InsertIntoIndex(buffers *CollationBuffers, char *BaseFilename)
|
|
{
|
|
file_buffer Index;
|
|
Index.Buffer.ID = "Index";
|
|
CopyString(Index.Path, "%s/%s.index", Config.CacheDir, Config.ProjectID);
|
|
int FileReadCode = ReadFileIntoBuffer(&Index, 0);
|
|
switch(FileReadCode)
|
|
{
|
|
case RC_ERROR_MEMORY:
|
|
return RC_ERROR_MEMORY;
|
|
case RC_ERROR_FILE:
|
|
case RC_SUCCESS:
|
|
break;
|
|
}
|
|
|
|
int EntryInsertionOffset = -1;
|
|
int TitleInsertionOffset = -1;
|
|
int TitleInsertionEnd = -1;
|
|
int EntryCount = 0;
|
|
|
|
char InputFile[255];
|
|
CopyString(InputFile, "%s.hmml", BaseFilename);
|
|
HMMLToBuffers(CollationBuffers, InputFile);
|
|
|
|
if(FileReadCode == RC_SUCCESS)
|
|
{
|
|
EntryCount = *(int *)Index.Buffer.Ptr;
|
|
Index.Buffer.Ptr += sizeof(int);
|
|
|
|
while(Index.Buffer.Ptr - Index.Buffer.Location < Index.FileSize)
|
|
{
|
|
char IndexedFile[32];
|
|
char *Ptr = IndexedFile;
|
|
Index.Buffer.Ptr += CopyStringNoFormatT(Ptr, Index.Buffer.Ptr, ',') + 1;
|
|
if(!StringsDiffer(IndexedFile, BaseFilename))
|
|
{
|
|
if(!StringsDifferT(CollationBuffers->Title, Index.Buffer.Ptr, '\n'))
|
|
{
|
|
FreeBuffer(&Index.Buffer);
|
|
return RC_NOOP;
|
|
}
|
|
else
|
|
{
|
|
TitleInsertionOffset = Index.Buffer.Ptr - Index.Buffer.Location;
|
|
while(*Index.Buffer.Ptr != '\n' && Index.Buffer.Ptr - Index.Buffer.Location < Index.FileSize)
|
|
{
|
|
++Index.Buffer.Ptr;
|
|
}
|
|
++Index.Buffer.Ptr;
|
|
TitleInsertionEnd = Index.Buffer.Ptr - Index.Buffer.Location;
|
|
break;
|
|
}
|
|
}
|
|
else if(StringsDiffer(IndexedFile, BaseFilename) > 0)
|
|
{
|
|
while(*Index.Buffer.Ptr != '\n' && Index.Buffer.Ptr > Index.Buffer.Location + sizeof(int))
|
|
{
|
|
--Index.Buffer.Ptr;
|
|
}
|
|
if(Index.Buffer.Ptr > Index.Buffer.Location + sizeof(int))
|
|
{
|
|
++Index.Buffer.Ptr;
|
|
}
|
|
EntryInsertionOffset = Index.Buffer.Ptr - Index.Buffer.Location;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
while(*Index.Buffer.Ptr != '\n' && Index.Buffer.Ptr - Index.Buffer.Location < Index.FileSize)
|
|
{
|
|
++Index.Buffer.Ptr;
|
|
}
|
|
++Index.Buffer.Ptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!(Index.Handle = fopen(Index.Path, "w"))) { FreeBuffer(&Index.Buffer); return RC_ERROR_FILE; }
|
|
|
|
if(FileReadCode == RC_ERROR_FILE)
|
|
{
|
|
Index.Buffer.Size = sizeof(int);
|
|
if(!(Index.Buffer.Location = malloc(Index.Buffer.Size)))
|
|
{
|
|
fclose(Index.Handle);
|
|
return RC_ERROR_MEMORY;
|
|
}
|
|
}
|
|
|
|
if(TitleInsertionOffset < 0) { EntryCount++; }
|
|
|
|
Index.Buffer.Ptr = Index.Buffer.Location;
|
|
*(int *)Index.Buffer.Ptr = EntryCount;
|
|
fwrite(Index.Buffer.Ptr, sizeof(int), 1, Index.Handle);
|
|
Index.Buffer.Ptr += sizeof(int);
|
|
|
|
if(EntryInsertionOffset >= 0)
|
|
{
|
|
fwrite(Index.Buffer.Ptr, EntryInsertionOffset - sizeof(int), 1, Index.Handle);
|
|
fprintf(Index.Handle, "%s,%s\n", BaseFilename, CollationBuffers->Title);
|
|
fwrite(Index.Buffer.Ptr + EntryInsertionOffset - sizeof(int), Index.FileSize - EntryInsertionOffset, 1, Index.Handle);
|
|
LogError(LOG_NOTICE, "Inserted %s - %s", BaseFilename, CollationBuffers->Title);
|
|
fprintf(stderr, "Inserted %s - %s\n", BaseFilename, CollationBuffers->Title);
|
|
}
|
|
else if(TitleInsertionOffset >= 0)
|
|
{
|
|
fwrite(Index.Buffer.Ptr, TitleInsertionOffset - sizeof(int), 1, Index.Handle);
|
|
fprintf(Index.Handle, "%s\n", CollationBuffers->Title);
|
|
fwrite(Index.Buffer.Ptr + TitleInsertionEnd - sizeof(int), Index.FileSize - TitleInsertionEnd, 1, Index.Handle);
|
|
LogError(LOG_NOTICE, "Edited %s - %s", BaseFilename, CollationBuffers->Title);
|
|
fprintf(stderr, "Edited %s - %s\n", BaseFilename, CollationBuffers->Title);
|
|
}
|
|
else
|
|
{
|
|
if(FileReadCode == RC_SUCCESS)
|
|
{
|
|
fwrite(Index.Buffer.Ptr, Index.FileSize - sizeof(int), 1, Index.Handle); // Write existing stuff back out
|
|
}
|
|
fprintf(Index.Handle, "%s,%s\n", BaseFilename, CollationBuffers->Title);
|
|
LogError(LOG_NOTICE, "Inserted %s - %s", BaseFilename, CollationBuffers->Title);
|
|
fprintf(stderr, "Inserted %s - %s\n", BaseFilename, CollationBuffers->Title);
|
|
}
|
|
|
|
fclose(Index.Handle);
|
|
FreeBuffer(&Index.Buffer);
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
int
|
|
DeleteFromIndex(char *BaseFilename)
|
|
{
|
|
// TODO(matt): LogError()
|
|
file_buffer Index;
|
|
Index.Buffer.ID = "Index";
|
|
CopyString(Index.Path, "%s/%s.index", Config.CacheDir, Config.ProjectID);
|
|
|
|
switch(ReadFileIntoBuffer(&Index, 0))
|
|
{
|
|
case RC_ERROR_FILE:
|
|
return RC_ERROR_FILE;
|
|
break;
|
|
case RC_ERROR_MEMORY:
|
|
LogError(LOG_ERROR, "DeleteFromIndex(): %s", strerror(errno));
|
|
return RC_ERROR_MEMORY;
|
|
break;
|
|
case RC_SUCCESS:
|
|
break;
|
|
}
|
|
|
|
int EntryCount = *(int *)Index.Buffer.Ptr;
|
|
Index.Buffer.Ptr += sizeof(int);
|
|
|
|
bool Found = FALSE;
|
|
int DeleteFrom = -1;
|
|
int DeleteTo = -1;
|
|
|
|
while(Index.Buffer.Ptr - Index.Buffer.Location < Index.FileSize)
|
|
{
|
|
if(!StringsDifferT(BaseFilename, Index.Buffer.Ptr, ','))
|
|
{
|
|
Found = TRUE;
|
|
--EntryCount;
|
|
DeleteFrom = Index.Buffer.Ptr - Index.Buffer.Location;
|
|
while(*Index.Buffer.Ptr != '\n' && Index.Buffer.Ptr - Index.Buffer.Location < Index.FileSize)
|
|
{
|
|
++Index.Buffer.Ptr;
|
|
}
|
|
++Index.Buffer.Ptr;
|
|
DeleteTo = Index.Buffer.Ptr - Index.Buffer.Location;
|
|
break;
|
|
}
|
|
++Index.Buffer.Ptr;
|
|
}
|
|
|
|
if(Found)
|
|
{
|
|
if(EntryCount == 0)
|
|
{
|
|
remove(Index.Path);
|
|
}
|
|
else
|
|
{
|
|
if(!(Index.Handle = fopen(Index.Path, "w")))
|
|
{
|
|
free(Index.Buffer.Location);
|
|
return RC_ERROR_FILE;
|
|
}
|
|
Index.Buffer.Ptr = Index.Buffer.Location;
|
|
*(int *)Index.Buffer.Ptr = EntryCount;
|
|
fwrite(Index.Buffer.Ptr, DeleteFrom, 1, Index.Handle);
|
|
fwrite(Index.Buffer.Ptr + DeleteTo, Index.FileSize - DeleteTo, 1, Index.Handle);
|
|
fclose(Index.Handle);
|
|
}
|
|
}
|
|
|
|
free(Index.Buffer.Location);
|
|
return Found ? RC_SUCCESS : RC_NOOP;
|
|
}
|
|
|
|
int
|
|
IndexToBuffer(buffers *CollationBuffers)
|
|
{
|
|
RewindBuffer(&CollationBuffers->Index);
|
|
// 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
|
|
|
|
file_buffer Index;
|
|
Index.Buffer.ID = "Index";
|
|
CopyString(Index.Path, "%s/%s.index", Config.CacheDir, Config.ProjectID);
|
|
ReadFileIntoBuffer(&Index, 0);
|
|
|
|
//int EntryCount = *(int *)Index.Buffer.Ptr;
|
|
Index.Buffer.Ptr += sizeof(int);
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Index, "<dl>\n");
|
|
while(Index.Buffer.Ptr - Index.Buffer.Location < Index.FileSize)
|
|
{
|
|
char IndexedFile[32];
|
|
char *Ptr = IndexedFile;
|
|
Index.Buffer.Ptr += CopyStringNoFormatT(Ptr, Index.Buffer.Ptr, ',') + 1;
|
|
|
|
char Title[255];
|
|
Ptr = Title;
|
|
Index.Buffer.Ptr += CopyStringNoFormatT(Ptr, Index.Buffer.Ptr, '\n') + 1;
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Index,
|
|
" <dt>\n"
|
|
" <a href=\"%s\">%s</a>\n"
|
|
" </dt>\n",
|
|
IndexedFile, Title);
|
|
|
|
}
|
|
CopyStringToBuffer(&CollationBuffers->Index, " </dl>");
|
|
|
|
FreeBuffer(&Index.Buffer);
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
int
|
|
GeneratePlayerPage(buffers *CollationBuffers, template *PlayerTemplate, char *BaseFilename)
|
|
{
|
|
char OutputDir[255];
|
|
CopyString(OutputDir, "%s/%s", Config.BaseDir, BaseFilename);
|
|
char PlayerPagePath[255];
|
|
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[255];
|
|
CopyString(IndexPagePath, "%s/index.html", Config.BaseDir);
|
|
IndexToBuffer(CollationBuffers);
|
|
BuffersToHTML(CollationBuffers, IndexTemplate, IndexPagePath, PAGE_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[255];
|
|
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[255];
|
|
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 removed %s. Unable to remove directory %s: %s", BaseFilename, PlayerDirPath, strerror(errno));
|
|
fprintf(stderr, "Mostly removed %s. Unable to remove directory %s: %s", BaseFilename, PlayerDirPath, strerror(errno));
|
|
}
|
|
else
|
|
{
|
|
LogError(LOG_INFORMATIONAL, "Fully removed %s from the system", BaseFilename);
|
|
fprintf(stderr, "Fully removed %s from the system\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(4)) == RC_ARENA_FULL) { return RC_ARENA_FULL; };
|
|
|
|
struct inotify_event *Event;
|
|
int BytesRead;
|
|
|
|
while((BytesRead = read(inotifyInstance, Events.Location, Events.Size)) != -1 && errno == EAGAIN && BytesRead > 0)
|
|
{
|
|
for(Events.Ptr = Events.Location;
|
|
Events.Ptr < Events.Location + BytesRead && Events.Ptr - 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[255];
|
|
CopyString(BaseFilename, Event->name);
|
|
*Ptr = '.';
|
|
|
|
if(Event->mask & IN_DELETE || Event->mask & IN_MOVED_FROM)
|
|
{
|
|
// TODO(matt): Remove from Index and synchronise Table of Contents and player pages accordingly
|
|
DeleteEntry(BaseFilename);
|
|
GenerateIndexPage(CollationBuffers, IndexTemplate);
|
|
}
|
|
else
|
|
{
|
|
switch(InsertIntoIndex(CollationBuffers, BaseFilename))
|
|
{
|
|
case RC_SUCCESS:
|
|
GeneratePlayerPage(CollationBuffers, PlayerTemplate, BaseFilename);
|
|
GenerateIndexPage(CollationBuffers, IndexTemplate);
|
|
break;
|
|
case RC_NOOP:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
DeclaimBuffer(&Events);
|
|
return RC_NOOP;
|
|
}
|
|
|
|
typedef struct
|
|
{
|
|
bool Present;
|
|
char ID[32];
|
|
} index_entry;
|
|
|
|
int
|
|
DeleteDeadIndexEntries()
|
|
{
|
|
file_buffer Index;
|
|
Index.Buffer.ID = "Index";
|
|
CopyString(Index.Path, "%s/%s.index", Config.CacheDir, Config.ProjectID);
|
|
if(ReadFileIntoBuffer(&Index, 0) == RC_ERROR_FILE)
|
|
{
|
|
return RC_ERROR_FILE;
|
|
}
|
|
|
|
// TODO(matt): Stuff entries into an array, through which we can iterate, marking off input files that are still present
|
|
int EntryCount = *(int *)Index.Buffer.Ptr;
|
|
Index.Buffer.Ptr += sizeof(int);
|
|
|
|
index_entry Entries[EntryCount];
|
|
|
|
int i = 0;
|
|
while(Index.Buffer.Ptr - Index.Buffer.Location < Index.FileSize)
|
|
{
|
|
Index.Buffer.Ptr += CopyStringNoFormatT(Entries[i].ID, Index.Buffer.Ptr, ',');
|
|
Entries[i].Present = FALSE;
|
|
while(*Index.Buffer.Ptr != '\n')
|
|
{
|
|
++Index.Buffer.Ptr;
|
|
}
|
|
++Index.Buffer.Ptr, ++i;
|
|
}
|
|
|
|
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 < 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 < EntryCount; ++i)
|
|
{
|
|
if(Entries[i].Present == FALSE)
|
|
{
|
|
Deleted = TRUE;
|
|
DeleteEntry(Entries[i].ID);
|
|
}
|
|
}
|
|
|
|
FreeBuffer(&Index.Buffer);
|
|
return Deleted ? RC_SUCCESS : RC_NOOP;
|
|
}
|
|
|
|
int
|
|
SyncIndexWithInput(buffers *CollationBuffers, template *IndexTemplate, template *PlayerTemplate)
|
|
{
|
|
DeleteDeadIndexEntries();
|
|
|
|
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';
|
|
switch(InsertIntoIndex(CollationBuffers, ProjectFiles->d_name))
|
|
{
|
|
case RC_SUCCESS:
|
|
GeneratePlayerPage(CollationBuffers, PlayerTemplate, ProjectFiles->d_name);
|
|
Inserted = TRUE;
|
|
break;
|
|
case RC_NOOP:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
closedir(ProjectDirHandle);
|
|
|
|
if(Inserted)
|
|
{
|
|
GenerateIndexPage(CollationBuffers, IndexTemplate);
|
|
}
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
int
|
|
main(int ArgC, char **Args)
|
|
{
|
|
// TODO(matt): Read all defaults from the config
|
|
config DefaultConfig = {
|
|
.BaseDir = ".",
|
|
.CSSDir = ".",
|
|
.Edition = EDITION_SINGLE,
|
|
.ImagesDir = ".",
|
|
.JSDir = ".",
|
|
.LogLevel = LOG_EMERGENCY,
|
|
.DefaultMedium = "programming",
|
|
.Mode = getenv("CINERA_MODE") ? MODE_INTEGRATE : MODE_BARE,
|
|
.OutLocation = "out.html",
|
|
.OutIntegratedLocation = "out_integrated.html",
|
|
.ForceIntegration = FALSE,
|
|
.ProjectDir = ".",
|
|
.ProjectID = "",
|
|
.RootDir = ".",
|
|
.TemplatePlayerLocation = "template_player.html",
|
|
.TemplateIndexLocation = "template_index.html",
|
|
.UpdateInterval = 4
|
|
};
|
|
|
|
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:fhi:I:j:l:m:o:p:r:t:u:x:")) != -1)
|
|
{
|
|
switch(CommandLineArg)
|
|
{
|
|
case 'b':
|
|
Config.BaseDir = optarg;
|
|
break;
|
|
case 'c':
|
|
Config.CSSDir = optarg;
|
|
break;
|
|
case 'f':
|
|
Config.ForceIntegration = TRUE;
|
|
break;
|
|
case 'i':
|
|
Config.ImagesDir = optarg;
|
|
break;
|
|
case 'I':
|
|
Config.ProjectID = optarg;
|
|
break;
|
|
case 'j':
|
|
Config.JSDir = optarg;
|
|
break;
|
|
case 'l':
|
|
// TODO(matt): Make this actually take a string, rather than requiring the LogLevel number
|
|
Config.LogLevel = StringToInt(optarg);
|
|
break;
|
|
case 'm':
|
|
Config.DefaultMedium = optarg;
|
|
break;
|
|
case 'o':
|
|
Config.OutLocation = optarg;
|
|
Config.OutIntegratedLocation = optarg;
|
|
break;
|
|
case 'p':
|
|
Config.ProjectDir = optarg;
|
|
break;
|
|
case 'r':
|
|
Config.RootDir = optarg;
|
|
break;
|
|
case 't':
|
|
Config.TemplatePlayerLocation = optarg;
|
|
Config.Mode = MODE_INTEGRATE;
|
|
break;
|
|
case 'u':
|
|
Config.UpdateInterval = StringToInt(optarg);
|
|
break;
|
|
case 'x':
|
|
Config.TemplateIndexLocation = optarg;
|
|
Config.Mode = MODE_INTEGRATE;
|
|
break;
|
|
case 'h':
|
|
default:
|
|
PrintUsage(Args[0], &DefaultConfig);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// TODO(matt): Sanitise this entry into Project Mode, probably once we switch to the config system
|
|
if(StringsDiffer(Config.BaseDir, ".") || StringsDiffer(Config.ProjectDir, "."))
|
|
{
|
|
if(StringsDiffer(Config.BaseDir, ".") && StringsDiffer(Config.ProjectDir, "."))
|
|
{
|
|
if(StringsDiffer(Config.ProjectID, ""))
|
|
{
|
|
Config.Edition = EDITION_PROJECT;
|
|
}
|
|
else
|
|
{
|
|
fprintf(stderr, "%s: Project ID must be set using the -I flag in order for us to enter Project Mode\n", Args[0]);
|
|
return 1;
|
|
|
|
}
|
|
}
|
|
else
|
|
{
|
|
fprintf(stderr, "%s: Both the Base Output Directory and the Project Directory must be set in order for us to enter Project Mode\n", Args[0]);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if(Config.CSSDir[StringLength(Config.CSSDir) - 1] == '/')
|
|
{
|
|
Config.CSSDir[StringLength(Config.CSSDir) - 1] = '\0';
|
|
}
|
|
|
|
bool ValidDefaultMedium = FALSE;
|
|
for(int i = 0; i < ArrayCount(CategoryMedium); ++i)
|
|
{
|
|
if(!StringsDiffer(Config.DefaultMedium, CategoryMedium[i].Medium))
|
|
{
|
|
ValidDefaultMedium = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
if(!ValidDefaultMedium)
|
|
{
|
|
fprintf(stderr, "Specified default medium \"%s\" not available. Valid media are:\n", Config.DefaultMedium);
|
|
for(int i = 0; i < ArrayCount(CategoryMedium); ++i)
|
|
{
|
|
fprintf(stderr, " %s\n", CategoryMedium[i].Medium);
|
|
}
|
|
fprintf(stderr, "To have \"%s\" added to the list, contact matt@handmadedev.org\n", Config.DefaultMedium);
|
|
return 1;
|
|
}
|
|
|
|
if(Config.ImagesDir[StringLength(Config.ImagesDir) - 1] == '/')
|
|
{
|
|
Config.ImagesDir[StringLength(Config.ImagesDir) - 1] = '\0';
|
|
}
|
|
if(Config.JSDir[StringLength(Config.JSDir) - 1] == '/')
|
|
{
|
|
Config.JSDir[StringLength(Config.JSDir) - 1] = '\0';
|
|
}
|
|
|
|
// NOTE(matt): Templating
|
|
//
|
|
// Config will contain paths of multiple templates
|
|
// App is running all the time, and picking up changes to the config as we go
|
|
// If we find a new template, we first of all validate it
|
|
// In our case here, we just want to straight up validate a template if Config.Mode == MODE_INTEGRATE
|
|
// And, in that same state, we gotta keep a Template buffer around
|
|
|
|
// NOTE(matt): Init MemoryArena (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
|
|
// Script
|
|
//
|
|
// IncludesIndex
|
|
// Index
|
|
|
|
buffers CollationBuffers;
|
|
if(ClaimBuffer(&CollationBuffers.IncludesPlayer, "IncludesPlayer", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; };
|
|
if(ClaimBuffer(&CollationBuffers.Menus, "Menus", Kilobytes(24)) == RC_ARENA_FULL) { goto RIP; };
|
|
if(ClaimBuffer(&CollationBuffers.Player, "Player", Kilobytes(256)) == RC_ARENA_FULL) { goto RIP; };
|
|
if(ClaimBuffer(&CollationBuffers.Script, "Script", Kilobytes(8)) == RC_ARENA_FULL) { goto RIP; };
|
|
|
|
if(ClaimBuffer(&CollationBuffers.IncludesIndex, "IncludesIndex", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; };
|
|
if(ClaimBuffer(&CollationBuffers.Index, "Index", Kilobytes(8)) == RC_ARENA_FULL) { goto RIP; };
|
|
*CollationBuffers.Title = '\0';
|
|
|
|
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
|
|
|
|
printf("[Cinera %s]\n"
|
|
"\n"
|
|
"Project ID: %s\n"
|
|
"Project Directory: %s\n"
|
|
"\n"
|
|
"Synchronising with annotation files in Project Directory\n", CINERA_VERSION, Config.ProjectID, Config.ProjectDir);
|
|
|
|
SyncIndexWithInput(&CollationBuffers, IndexTemplate, PlayerTemplate);
|
|
|
|
printf("\nMonitoring Project Directory for new, edited or deleted .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.Index);
|
|
DeclaimBuffer(&CollationBuffers.IncludesIndex);
|
|
DeclaimBuffer(&CollationBuffers.Script);
|
|
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
|
|
|
|
}
|