Annotation-System/cinera/cinera.c

6833 lines
270 KiB
C

#if 0
ctime -begin ${0%.*}.ctm
#gcc -g -fsanitize=address -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl
gcc -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl
#clang -fsanitize=address -g -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl
#clang -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl
ctime -end ${0%.*}.ctm
exit
#endif
typedef struct
{
unsigned int Major, Minor, Patch;
} version;
version CINERA_APP_VERSION = {
.Major = 0,
.Minor = 5,
.Patch = 66
};
// TODO(matt): Copy in the DB 3 stuff from cinera_working.c
#define CINERA_DB_VERSION 3
#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()
typedef unsigned int bool;
#define TRUE 1
#define FALSE 0
#define DEBUG 0
#define DEBUG_MEM 0
bool PROFILING = 0;
clock_t TIMING_START;
#define START_TIMING_BLOCK(...) if(PROFILING) { printf(__VA_ARGS__); TIMING_START = clock(); }
#define END_TIMING_BLOCK() if(PROFILING) { printf("\e[1;34m%ld\e[0m\n", clock() - TIMING_START);}
#define Kilobytes(Bytes) Bytes << 10
#define Megabytes(Bytes) Bytes << 20
#define MAX_PROJECT_ID_LENGTH 31
#define MAX_PROJECT_NAME_LENGTH 63
#define MAX_BASE_URL_LENGTH 127
#define MAX_RELATIVE_PAGE_LOCATION_LENGTH 31
#define MAX_PLAYER_URL_PREFIX_LENGTH 15
#define MAX_BASE_FILENAME_LENGTH 31
#define MAX_TITLE_LENGTH 128 - (MAX_BASE_FILENAME_LENGTH + 1) - (int)sizeof(link_insertion_offsets) - (int)sizeof(unsigned short int) - 1 // NOTE(matt): We size this such that index_metadata is 128 bytes total
#define MAX_CUSTOM_SNIPPET_SHORT_LENGTH 255
#define MAX_CUSTOM_SNIPPET_LONG_LENGTH 1023
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_FORCEINTEGRATION = 1 << 0,
MODE_ONESHOT = 1 << 1,
MODE_EXAMINE = 1 << 2,
MODE_NOCACHE = 1 << 3,
MODE_NOPRIVACY = 1 << 4,
MODE_SINGLETAB = 1 << 5
} modes;
enum
{
RC_ARENA_FULL,
RC_ERROR_DIRECTORY,
RC_ERROR_FATAL,
RC_ERROR_FILE,
RC_ERROR_HMML,
RC_ERROR_MAX_REFS,
RC_ERROR_MEMORY,
RC_ERROR_PROJECT,
RC_ERROR_QUOTE,
RC_ERROR_SEEK,
RC_FOUND,
RC_UNFOUND,
RC_INVALID_REFERENCE,
RC_INVALID_TEMPLATE,
RC_PRIVATE_VIDEO,
RC_NOOP,
RC_RIP,
RC_SUCCESS
} returns;
typedef struct
{
void *Location;
void *Ptr;
char *ID;
int Size;
} arena;
typedef struct
{
// Universal
char CacheDir[256];
int Edition;
int LogLevel;
int Mode;
int UpdateInterval;
// Advisedly universal, although could be per-project
char *RootDir; // Absolute
char *RootURL;
char *CSSDir; // Relative to Root{Dir,URL}
char *ImagesDir; // Relative to Root{Dir,URL}
char *JSDir; // Relative to Root{Dir,URL}
// Per Project
char *ProjectID;
char *Theme;
char *DefaultMedium;
// Per Project - Input
char *ProjectDir; // Absolute
char *TemplatesDir; // Absolute
char *TemplateIndexLocation; // Relative to TemplatesDir ???
char *TemplatePlayerLocation; // Relative to TemplatesDir ???
// Per Project - Output
char *BaseDir; // Absolute
char *BaseURL;
char *IndexLocation; // Relative to Base{Dir,URL}
char *PlayerLocation; // Relative to Base{Dir,URL}
char *PlayerURLPrefix; /* NOTE(matt): This will become a full blown customisable output URL.
For now it simply replaces the ProjectID */
// Single Edition - Input
char SingleHMMLFilePath[256];
// Single Edition - Output
char *OutLocation;
char *OutIntegratedLocation;
} config;
// NOTE(matt): Globals
config Config = {};
arena MemoryArena;
time_t LastPrivacyCheck;
time_t LastQuoteFetch;
//
typedef struct
{
char *Location;
char *Ptr;
char *ID;
int Size;
} buffer;
typedef struct
{
buffer Buffer;
FILE *Handle;
char Path[256]; // NOTE(matt): Could this just be a char *?
int FileSize;
} file_buffer;
// TODO(matt): Increment CINERA_DB_VERSION!
typedef struct
{
// NOTE(matt): Consider augmenting this to contain such stuff as: "hex signature"
unsigned int CurrentDBVersion; // NOTE(matt): Put this first to aid reliability
version CurrentAppVersion;
version CurrentHMMLVersion;
unsigned int InitialDBVersion;
version InitialAppVersion;
version InitialHMMLVersion;
unsigned short int EntryCount;
char ProjectID[MAX_PROJECT_ID_LENGTH + 1];
char ProjectName[MAX_PROJECT_NAME_LENGTH + 1];
char BaseURL[MAX_BASE_URL_LENGTH + 1];
char IndexLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1];
char PlayerLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1];
char PlayerURLPrefix[MAX_PLAYER_URL_PREFIX_LENGTH + 1]; // TODO(matt): Replace this with the OutputPath, when we add that
} index_header;
typedef struct
{
unsigned int PrevStart, NextStart;
unsigned short int PrevEnd, NextEnd;
} link_insertion_offsets; // NOTE(matt): Relative
typedef struct
{
link_insertion_offsets LinkOffsets;
unsigned short int Size;
char BaseFilename[MAX_BASE_FILENAME_LENGTH + 1];
char Title[MAX_TITLE_LENGTH + 1];
} index_metadata;
typedef struct
{
file_buffer File;
file_buffer Metadata;
index_header Header;
index_metadata Entry;
} index;
// TODO(matt): Increment CINERA_DB_VERSION!
typedef struct
{
buffer IncludesIndex;
buffer Search;
buffer Index; // NOTE(matt): This buffer is malloc'd separately, rather than claimed from the memory_arena
buffer IncludesPlayer;
buffer Menus;
buffer Player;
buffer ScriptPlayer;
char Custom0[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
char Custom1[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
char Custom2[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
char Custom3[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
char Custom4[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
char Custom5[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
char Custom6[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
char Custom7[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
char Custom8[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
char Custom9[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
char Custom10[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
char Custom11[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + 1];
char Custom12[MAX_CUSTOM_SNIPPET_LONG_LENGTH + 1];
char Custom13[MAX_CUSTOM_SNIPPET_LONG_LENGTH + 1];
char Custom14[MAX_CUSTOM_SNIPPET_LONG_LENGTH + 1];
char Custom15[MAX_CUSTOM_SNIPPET_LONG_LENGTH + 1];
char ProjectID[MAX_PROJECT_ID_LENGTH + 1];
char ProjectName[MAX_PROJECT_NAME_LENGTH + 1];
char Theme[MAX_PROJECT_NAME_LENGTH + 1];
char Title[MAX_TITLE_LENGTH + 1];
char URLIndex[MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1];
char URLPlayer[MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1];
char VideoID[16];
} buffers;
enum
{
// Contents and Player Pages Mandatory
TAG_INCLUDES,
// Contents Page Mandatory
TAG_INDEX,
// Player Page Mandatory
TAG_MENUS,
TAG_PLAYER,
TAG_SCRIPT,
// Player Page Optional
TAG_CUSTOM0,
TAG_CUSTOM1,
TAG_CUSTOM2,
TAG_CUSTOM3,
TAG_CUSTOM4,
TAG_CUSTOM5,
TAG_CUSTOM6,
TAG_CUSTOM7,
TAG_CUSTOM8,
TAG_CUSTOM9,
TAG_CUSTOM10,
TAG_CUSTOM11,
TAG_CUSTOM12,
TAG_CUSTOM13,
TAG_CUSTOM14,
TAG_CUSTOM15,
TAG_TITLE,
TAG_VIDEO_ID,
// Anywhere Optional
TAG_PROJECT,
TAG_PROJECT_ID,
TAG_THEME,
TAG_URL,
} template_tags;
typedef struct
{
int Code; // template_tags
char *Tag;
} tag;
tag Tags[] = {
{ TAG_INCLUDES, "__CINERA_INCLUDES__" },
{ TAG_INDEX, "__CINERA_INDEX__" },
{ TAG_MENUS, "__CINERA_MENUS__" },
{ TAG_PLAYER, "__CINERA_PLAYER__" },
{ TAG_SCRIPT, "__CINERA_SCRIPT__" },
{ TAG_CUSTOM0, "__CINERA_CUSTOM0__" },
{ TAG_CUSTOM1, "__CINERA_CUSTOM1__" },
{ TAG_CUSTOM2, "__CINERA_CUSTOM2__" },
{ TAG_CUSTOM3, "__CINERA_CUSTOM3__" },
{ TAG_CUSTOM4, "__CINERA_CUSTOM4__" },
{ TAG_CUSTOM5, "__CINERA_CUSTOM5__" },
{ TAG_CUSTOM6, "__CINERA_CUSTOM6__" },
{ TAG_CUSTOM7, "__CINERA_CUSTOM7__" },
{ TAG_CUSTOM8, "__CINERA_CUSTOM8__" },
{ TAG_CUSTOM9, "__CINERA_CUSTOM9__" },
{ TAG_CUSTOM10, "__CINERA_CUSTOM10__" },
{ TAG_CUSTOM11, "__CINERA_CUSTOM11__" },
{ TAG_CUSTOM12, "__CINERA_CUSTOM12__" },
{ TAG_CUSTOM13, "__CINERA_CUSTOM13__" },
{ TAG_CUSTOM14, "__CINERA_CUSTOM14__" },
{ TAG_CUSTOM15, "__CINERA_CUSTOM15__" },
{ TAG_TITLE, "__CINERA_TITLE__" },
{ TAG_VIDEO_ID, "__CINERA_VIDEO_ID__" },
{ TAG_PROJECT, "__CINERA_PROJECT__" },
{ TAG_PROJECT_ID, "__CINERA_PROJECT_ID__" },
{ TAG_THEME, "__CINERA_THEME__" },
{ TAG_URL, "__CINERA_URL__" },
};
typedef struct
{
int Offset;
int TagCode;
} tag_offset;
typedef struct
{
char Filename[256];
tag_offset Tag[16];
int Validity; // NOTE(matt): Bitmask describing which page the template is valid for, i.e. contents and / or player page
int TagCount;
} template_metadata;
typedef struct
{
template_metadata Metadata;
buffer Buffer;
} template;
// TODO(matt): Consider putting the ref_info and quote_info into linked lists on the heap, just to avoid all the hardcoded sizes
typedef struct
{
char Date[32];
char Text[512];
} quote_info;
typedef struct
{
char Timecode[8];
int Identifier;
} identifier;
#define REF_MAX_IDENTIFIER 64
typedef struct
{
char RefTitle[620];
char ID[512];
char URL[512];
char Source[256];
identifier Identifier[REF_MAX_IDENTIFIER];
int IdentifierCount;
} ref_info;
typedef struct
{
char Marker[32];
char WrittenText[32];
} category_info;
typedef struct
{
category_info Category[64];
int Count;
} categories;
// TODO(matt): Parse this stuff out of a config file
typedef struct
{
char *Username;
char *CreditedName;
char *HomepageURL;
char *SupportIcon;
char *SupportURL;
} credential_info;
credential_info Credentials[] =
{
{ "a_waterman", "Andrew Waterman", "https://www.linkedin.com/in/andrew-waterman-76805788", "", ""},
{ "y_lee", "Yunsup Lee", "https://www.linkedin.com/in/yunsup-lee-385b692b/", "", ""},
{ "AndrewJDR", "Andrew Johnson", "", "", ""},
{ "AsafGartner", "Asaf Gartner", "", "", ""},
{ "BretHudson", "Bret Hudson", "http://www.brethudson.com/", "cinera_sprite_patreon.png", "https://www.patreon.com/indieFunction"},
{ "ChronalDragon", "Andrew Chronister", "http://chronal.net/", "", ""},
{ "Kelimion", "Jeroen van Rijn", "https://handmade.network/home", "", ""},
{ "Mannilie", "Emmanuel Vaccaro", "http://emmanuelvaccaro.com/", "", ""},
{ "Miblo", "Matt Mascarenhas", "https://miblodelcarpio.co.uk/", "cinera_sprite_sendowl.png", "https://miblodelcarpio.co.uk/cinera#pledge"},
{ "Mr4thDimention", "Allen Webster", "http://www.4coder.net/", "", ""},
{ "Pseudonym73", "Andrew Bromage", "https://twitter.com/deguerre", "", ""},
{ "Quel_Solaar", "Eskil Steenberg", "http://quelsolaar.com/", "", ""},
{ "ZedZull", "Jay Waggle", "", "", ""},
{ "abnercoimbre", "Abner Coimbre", "https://handmade.network/m/abnercoimbre", "", ""},
{ "brianwill", "Brian Will", "http://brianwill.net/blog/", "", ""},
{ "cbloom", "Charles Bloom", "http://cbloomrants.blogspot.co.uk/", "", ""},
{ "cmuratori", "Casey Muratori", "https://handmadehero.org", "cinera_sprite_sendowl.png", "https://handmadehero.org/fund"},
{ "csnover", "Colin Snover", "https://zetafleet.com/", "", ""},
{ "debiatan", "Miguel Lechón", "http://blog.debiatan.net/", "", ""},
{ "dspecht", "Dustin Specht", "", "", ""},
{ "effect0r", "Cory Henderlite", "", "", ""},
{ "ffsjs", "ffsjs", "", "", ""},
{ "fierydrake", "Mike Tunnicliffe", "", "", ""},
{ "garlandobloom", "Matthew VanDevander", "https://lowtideproductions.com/", "cinera_sprite_patreon.png", "https://www.patreon.com/mv"},
{ "ikerms", "Iker Murga", "", "", ""},
{ "insofaras", "Alex Baines", "https://abaines.me.uk/", "", ""},
{ "jacebennett", "Jace Bennett", "", "", ""},
{ "jon", "Jonathan Blow", "http://the-witness.net/news/", "", ""},
{ "jpike", "Jacob Pike", "", "", ""},
{ "martincohen", "Martin Cohen", "http://blog.coh.io/", "", ""},
{ "miotatsu", "Mio Iwakura", "http://riscy.tv/", "cinera_sprite_patreon.png", "https://patreon.com/miotatsu"},
{ "nothings", "Sean Barrett", "https://nothings.org/", "", ""},
{ "pervognsen", "Per Vognsen", "https://github.com/pervognsen/bitwise/", "", ""},
{ "philipbuuck", "Philip Buuck", "http://philipbuuck.com/", "", ""},
{ "powerc9000", "Clay Murray", "http://claymurray.website/", "", ""},
{ "rygorous", "Fabian Giesen", "https://fgiesen.wordpress.com/", "", ""},
{ "schme", "Kasper Sauramo", "", "", ""},
{ "sssmcgrath", "Shawn McGrath", "http://www.dyadgame.com/", "", ""},
{ "thehappiecat", "Anne", "https://www.youtube.com/c/TheHappieCat", "cinera_sprite_patreon.png", "https://www.patreon.com/thehappiecat"},
{ "theinternetftw", "Ben Craddock", "", "", ""},
{ "wheatdog", "Tim Liou", "http://stringbulbs.com/", "", ""},
{ "williamchyr", "William Chyr", "http://williamchyr.com/", "", ""},
{ "wonchun", "Won Chun", "https://twitter.com/won3d", "", ""},
};
typedef struct
{
char *Medium;
char *Icon;
char *WrittenName;
} category_medium;
category_medium CategoryMedium[] =
{
// medium icon written name
{ "admin", "&#128505;", "Administrivia"},
{ "afk", "&#8230;" , "Away from Keyboard"},
{ "authored", "&#128490;", "Chat Comment"}, // TODO(matt): Conditionally handle Chat vs Guest Comments
{ "blackboard", "&#128396;", "Blackboard"},
{ "drawing", "&#127912;", "Drawing"},
{ "experience", "&#127863;", "Experience"},
{ "hat", "&#127913;", "Hat"},
{ "multimedia", "&#127916;", "Media Clip"},
{ "owl", "&#129417;", "Owl of Shame"},
{ "programming", "&#128430;", "Programming"}, // TODO(matt): Potentially make this configurable per project
{ "rant", "&#128162;", "Rant"},
{ "research", "&#128214;", "Research"},
{ "run", "&#127939;", "In-Game"}, // TODO(matt): Potentially make this configurable per project
{ "speech", "&#128489;", "Speech"},
{ "trivia", "&#127922;", "Trivia"},
};
enum
{
NS_CALENDRICAL,
NS_LINEAR,
NS_SEASONAL,
} numbering_schemes;
typedef struct
{
char *ProjectID;
char *FullName;
char *Unit; // e.g. Day, Episode, Session
int NumberingScheme; // numbering_schemes
char *Medium;
char *AltURLPrefix; // NOTE(matt): This currently just straight up replaces the ProjectID in the player pages' output directories
} project_info;
project_info ProjectInfo[] =
{
{ "bitwise", "Bitwise", "Day", NS_LINEAR, "programming", "" },
{ "book", "Book Club", "Day", NS_LINEAR, "research", "" },
{ "coad", "Computer Organization and Design", "", NS_LINEAR, "research", "" },
{ "reader", "RISC-V Reader", "", NS_LINEAR, "research", "" },
{ "riscy", "RISCY BUSINESS", "Day", NS_LINEAR, "programming", "" },
{ "risc", "RISCellaneous", "", NS_CALENDRICAL, "speech", "" },
{ "chat", "Handmade Chat", "Day", NS_LINEAR, "speech", "" },
{ "code", "Handmade Hero", "Day", NS_LINEAR, "programming", "day" },
{ "intro-to-c", "Intro to C on Windows", "Day", NS_LINEAR, "programming", "day" },
{ "misc", "Handmade Miscellany", "", NS_LINEAR, "admin", "" },
{ "ray", "Handmade Ray", "Day", NS_LINEAR, "programming", "" },
{ "hmdshow", "HandmadeDev Show", "", NS_SEASONAL, "speech", "ep" },
{ "lecture", "Abner Talks", "", NS_SEASONAL, "speech", "" },
{ "stream", "Abner Programs", "", NS_SEASONAL, "programming", "" },
{ "special", "Abner Show Special", "", NS_SEASONAL, "programming", "" },
{ "obbg", "Open Block Building Game", "Episode", NS_LINEAR, "programming", "" },
{ "sysadmin", "SysAdmin", "Session", NS_LINEAR, "admin", "" },
};
#define ArrayCount(A) sizeof(A)/sizeof(*(A))
void
Clear(char *String, int Size)
{
for(int i = 0; i < Size; ++i)
{
String[i] = 0;
}
}
int
StringLength(char *String)
{
int i = 0;
while(String[i])
{
++i;
}
return i;
}
#define CopyString(Dest, DestSize, Format, ...) CopyString_(__LINE__, (Dest), (DestSize), (Format), ##__VA_ARGS__)
__attribute__ ((format (printf, 4, 5)))
int
CopyString_(int LineNumber, char Dest[], int DestSize, char *Format, ...)
{
int Length = 0;
va_list Args;
va_start(Args, Format);
Length = vsnprintf(Dest, DestSize, Format, Args);
if(Length >= DestSize)
{
printf("CopyString() call on line %d has been passed a buffer too small (%d bytes) to contain null-terminated %d(+1)-character string\n", LineNumber, DestSize, Length);
__asm__("int3");
}
va_end(Args);
return Length;
}
#define CopyStringNoFormat(Dest, DestSize, String) CopyStringNoFormat_(__LINE__, Dest, DestSize, String)
int
CopyStringNoFormat_(int LineNumber, char *Dest, int DestSize, char *String)
{
int Length = 0;
char *Start = String;
while(*String)
{
*Dest++ = *String++;
++Length;
}
if(Length >= DestSize)
{
printf("CopyStringNoFormat() call on line %d has been passed a buffer too small (%d bytes) to contain null-terminated %d(+1)-character string:\n"
"%s\n", LineNumber, DestSize, Length, Start);
__asm__("int3");
}
*Dest = '\0';
return Length;
}
#define ClearCopyStringNoFormat(Dest, DestSize, String) ClearCopyStringNoFormat_(__LINE__, Dest, DestSize, String)
int
ClearCopyStringNoFormat_(int LineNumber, char *Dest, int DestSize, char *String)
{
Clear(Dest, DestSize);
int Length = 0;
char *Start = String;
while(*String)
{
*Dest++ = *String++;
++Length;
}
if(Length >= DestSize)
{
printf("ClearCopyStringNoFormat() call on line %d has been passed a buffer too small (%d bytes) to contain null-terminated %d(+1)-character string:\n"
"%s\n", LineNumber, DestSize, Length, Start);
__asm__("int3");
}
*Dest = '\0';
return Length;
}
// TODO(matt): Maybe do a version of this that takes a string as a Terminator
#define CopyStringNoFormatT(Dest, DestSize, String, Terminator) CopyStringNoFormatT_(__LINE__, Dest, DestSize, String, Terminator)
int
CopyStringNoFormatT_(int LineNumber, char *Dest, int DestSize, char *String, char Terminator)
{
int Length = 0;
char *Start = String;
while(*String != Terminator)
{
*Dest++ = *String++;
++Length;
}
if(Length >= DestSize)
{
printf("CopyStringNoFormatT() call on line %d has been passed a buffer too small (%d bytes) to contain %c-terminated %d(+1)-character string:\n"
"%.*s\n", LineNumber, DestSize, Terminator == 0 ? '0' : Terminator, Length, Length, Start);
__asm__("int3");
}
*Dest = '\0';
return Length;
}
#define CopyStringToBuffer(Dest, Format, ...) CopyStringToBuffer_(__LINE__, Dest, Format, ##__VA_ARGS__)
__attribute__ ((format (printf, 3, 4)))
void
CopyStringToBuffer_(int LineNumber, 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);
if(Length + (Dest->Ptr - Dest->Location) >= Dest->Size)
{
fprintf(stderr, "CopyStringToBuffer(%s) call on line %d cannot accommodate null-terminated %d(+1)-character string:\n"
"%s\n", Dest->ID, LineNumber, Length, Format);
__asm__("int3");
}
Dest->Ptr += Length;
}
#define CopyStringToBufferNoFormat(Dest, String) CopyStringToBufferNoFormat_(__LINE__, Dest, String)
void
CopyStringToBufferNoFormat_(int LineNumber, buffer *Dest, char *String)
{
char *Start = String;
while(*String)
{
*Dest->Ptr++ = *String++;
}
if(Dest->Ptr - Dest->Location >= Dest->Size)
{
fprintf(stderr, "CopyStringToBufferNoFormat(%s) call on line %d cannot accommodate %d-character string:\n"
"%s\n", Dest->ID, LineNumber, StringLength(Start), Start);
__asm__("int3");
}
*Dest->Ptr = '\0';
}
#define CopyStringToBufferHTMLSafe(Dest, String) CopyStringToBufferHTMLSafe_(__LINE__, Dest, String)
void
CopyStringToBufferHTMLSafe_(int LineNumber, buffer *Dest, char *String)
{
char *Start = String;
int Length = StringLength(String);
while(*String)
{
switch(*String)
{
case '<': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'l'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 3; break;
case '>': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'g'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 3; break;
case '&': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'a'; *Dest->Ptr++ = 'm'; *Dest->Ptr++ = 'p'; *Dest->Ptr++ = ';'; Length += 4; break;
case '\"': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'q'; *Dest->Ptr++ = 'u'; *Dest->Ptr++ = 'o'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 5; break;
case '\'': *Dest->Ptr++ = '&'; *Dest->Ptr++ = '#'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = '9'; *Dest->Ptr++ = ';'; Length += 4; break;
default: *Dest->Ptr++ = *String; break;
}
++String;
}
if(Dest->Ptr - Dest->Location >= Dest->Size)
{
fprintf(stderr, "CopyStringToBufferHTMLSafe(%s) call on line %d cannot accommodate %d(+1)-character HTML-sanitised string:\n"
"%s\n", Dest->ID, LineNumber, Length, Start);
__asm__("int3");
}
*Dest->Ptr = '\0';
}
#define CopyStringToBufferHTMLSafeBreakingOnSlash(Dest, String) CopyStringToBufferHTMLSafeBreakingOnSlash_(__LINE__, Dest, String)
void
CopyStringToBufferHTMLSafeBreakingOnSlash_(int LineNumber, buffer *Dest, char *String)
{
char *Start = String;
int Length = StringLength(String);
while(*String)
{
switch(*String)
{
case '<': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'l'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 3; break;
case '>': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'g'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 3; break;
case '&': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'a'; *Dest->Ptr++ = 'm'; *Dest->Ptr++ = 'p'; *Dest->Ptr++ = ';'; Length += 4; break;
case '\'': *Dest->Ptr++ = '&'; *Dest->Ptr++ = '#'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = '9'; *Dest->Ptr++ = ';'; Length += 4; break;
case '\"': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'q'; *Dest->Ptr++ = 'u'; *Dest->Ptr++ = 'o'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 5; break;
case '/': *Dest->Ptr++ = '/'; *Dest->Ptr++ = '&'; *Dest->Ptr++ = '#'; *Dest->Ptr++ = '8'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '0'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = ';'; Length += 7; break;
default: *Dest->Ptr++ = *String; break;
}
++String;
}
if(Dest->Ptr - Dest->Location >= Dest->Size)
{
fprintf(stderr, "CopyStringToBufferHTMLSafeBreakingOnSlash_(%s) call on line %d cannot accommodate %d(+1)-character HTML-sanitised string:\n"
"%s\n", Dest->ID, LineNumber, Length, Start);
__asm__("int3");
}
}
#define CopyBuffer(Dest, Src) CopyBuffer_(__LINE__, Dest, Src)
void
CopyBuffer_(int LineNumber, buffer *Dest, buffer *Src)
{
Src->Ptr = Src->Location;
while(*Src->Ptr)
{
*Dest->Ptr++ = *Src->Ptr++;
}
if(Dest->Ptr - Dest->Location >= Dest->Size)
{
fprintf(stderr, "CopyBuffer(%s) call on line %d cannot accommodate %d(+1)-character %s\n", Dest->ID, LineNumber, StringLength(Src->Location), Src->ID);
__asm__("int3");
}
*Dest->Ptr = '\0';
}
#define CopyBufferSized(Dest, Src, Size) CopyBufferSized_(__LINE__, Dest, Src, Size)
void
CopyBufferSized_(int LineNumber, buffer *Dest, buffer *Src, int Size)
{
Src->Ptr = Src->Location;
while(Src->Ptr - Src->Location < Size)
{
*Dest->Ptr++ = *Src->Ptr++;
}
if(Dest->Ptr - Dest->Location >= Dest->Size)
{
fprintf(stderr, "CopyBufferNoNull(%s) call on line %d cannot accommodate %d(+1)-character %s\n", Dest->ID, LineNumber, StringLength(Src->Location), Src->ID);
__asm__("int3");
}
}
int
StringsDiffer(char *A, char *B) // NOTE(matt): Two null-terminated strings
{
while(*A && *B && *A == *B)
{
++A, ++B;
}
return *A - *B;
}
int
StringsDifferCaseInsensitive(char *A, char *B) // NOTE(matt): Two null-terminated strings
{
while(*A && *B &&
((*A >= 'A' && *A <= 'Z') ? *A + ('a' - 'A') : *A) ==
((*B >= 'A' && *B <= 'Z') ? *B + ('a' - '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
)
{
// TODO(matt): Make sure this can't crash upon reaching the end of B's buffer
int ALength = StringLength(A);
int i = 0;
while(i < ALength && A[i] && A[i] == B[i])
{
++i;
}
if((!Terminator && !A[i] && ALength == i) ||
(!A[i] && ALength == i && (B[i] == Terminator)))
{
return FALSE;
}
else
{
return TRUE;
}
}
int
MakeDir(char *Path)
{
// TODO(matt): Correctly check for permissions
int i = StringLength(Path);
int Ancestors = 0;
while(mkdir(Path, 00755) == -1)
{
if(errno == EACCES)
{
return RC_ERROR_DIRECTORY;
}
while(Path[i] != '/' && i > 0)
{
--i;
}
++Ancestors;
Path[i] = '\0';
if(i == 0) { return RC_ERROR_DIRECTORY; }
}
while(Ancestors > 0)
{
while(Path[i] != '\0')
{
++i;
}
Path[i] = '/';
--Ancestors;
if((mkdir(Path, 00755)) == -1)
{
return RC_ERROR_DIRECTORY;
}
}
return RC_SUCCESS;
}
void
LogUsage(buffer *Buffer)
{
#if DEBUG
char LogPath[256];
CopyString(LogPath, "%s/%s", Config.CacheDir, "buffers.log");
FILE *LogFile;
if(!(LogFile = fopen(LogPath, "a+")))
{
MakeDir(Config.CacheDir);
if(!(LogFile = fopen(LogPath, "a+")))
{
perror("LogUsage");
return;
}
}
fprintf(LogFile, "%s,%ld,%d\n",
Buffer->ID,
Buffer->Ptr - Buffer->Location,
Buffer->Size);
fclose(LogFile);
#endif
}
__attribute__ ((format (printf, 2, 3)))
void
LogError(int LogLevel, char *Format, ...)
{
if(Config.LogLevel >= LogLevel)
{
char LogPath[256];
CopyString(LogPath, sizeof(LogPath), "%s/%s", Config.CacheDir, "errors.log");
FILE *LogFile;
if(!(LogFile = fopen(LogPath, "a+")))
{
MakeDir(Config.CacheDir);
if(!(LogFile = fopen(LogPath, "a+")))
{
perror("LogUsage");
return;
}
}
va_list Args;
va_start(Args, Format);
vfprintf(LogFile, Format, Args);
va_end(Args);
// TODO(matt): Include the LogLevel "string" and the current wall time
fprintf(LogFile, "\n");
fclose(LogFile);
}
}
void
FreeBuffer(buffer *Buffer)
{
free(Buffer->Location);
Buffer->Location = 0;
#if DEBUG_MEM
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, " Freed %s\n", Buffer->ID);
fclose(MemLog);
printf(" Freed %s\n", Buffer->ID);
#endif
}
#if 0
#define ClaimBuffer(MemoryArena, Buffer, ID, Size) if(__ClaimBuffer(MemoryArena, Buffer, ID, Size))\
{\
fprintf(stderr, "%s:%d: MemoryArena cannot contain %s of size %d\n", __FILE__, __LINE__, ID, Size);\
hmml_free(&HMML);\
FreeBuffer(MemoryArena);\
return 1;\
};
#endif
int
ClaimBuffer(buffer *Buffer, char *ID, int Size)
{
if(MemoryArena.Ptr - MemoryArena.Location + Size > MemoryArena.Size)
{
return RC_ARENA_FULL;
}
Buffer->Location = (char *)MemoryArena.Ptr;
Buffer->Size = Size;
Buffer->ID = ID;
MemoryArena.Ptr += Buffer->Size;
*Buffer->Location = '\0';
Buffer->Ptr = Buffer->Location;
#if DEBUG
float PercentageUsed = (float)(MemoryArena.Ptr - MemoryArena.Location) / MemoryArena.Size * 100;
printf(" ClaimBuffer(%s): %d\n"
" Total ClaimedMemory: %ld (%.2f%%, leaving %ld free)\n\n", Buffer->ID, Buffer->Size, MemoryArena.Ptr - MemoryArena.Location, PercentageUsed, MemoryArena.Size - (MemoryArena.Ptr - MemoryArena.Location));
#endif
return RC_SUCCESS;
}
void
DeclaimBuffer(buffer *Buffer)
{
*Buffer->Location = '\0';
MemoryArena.Ptr -= Buffer->Size;
float PercentageUsed = (float)(Buffer->Ptr - Buffer->Location) / Buffer->Size * 100;
#if DEBUG
printf("DeclaimBuffer(%s)\n"
" Used: %ld / %d (%.2f%%)\n"
"\n"
" Total ClaimedMemory: %ld\n\n",
Buffer->ID,
Buffer->Ptr - Buffer->Location,
Buffer->Size,
PercentageUsed,
MemoryArena.Ptr - MemoryArena.Location);
#endif
LogUsage(Buffer);
if(PercentageUsed >= 95.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, "\e[1;31mWarning\e[0m: %s used %.2f%% of its allotted memory\n", Buffer->ID, PercentageUsed);
}
else 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, "\e[0;33mWarning\e[0m: %s used %.2f%% of its allotted memory\n", Buffer->ID, PercentageUsed);
}
Buffer->Size = 0;
}
void
RewindBuffer(buffer *Buffer)
{
#if DEBUG
float PercentageUsed = (float)(Buffer->Ptr - Buffer->Location) / Buffer->Size * 100;
printf("Rewinding %s\n"
" Used: %ld / %d (%.2f%%)\n\n",
Buffer->ID,
Buffer->Ptr - Buffer->Location,
Buffer->Size,
PercentageUsed);
#endif
Buffer->Ptr = Buffer->Location;
}
enum
{
TEMPLATE_INDEX,
TEMPLATE_PLAYER,
TEMPLATE_BESPOKE
} templates;
char *
GetDirectoryPath(char *Filepath)
{
char *Ptr = Filepath + StringLength(Filepath) - 1;
while(Ptr > Filepath && *Ptr != '/')
{
--Ptr;
}
if(Ptr == Filepath)
{
*Ptr++ = '.';
}
*Ptr = '\0';
return Filepath;
}
char *
GetBaseFilename(char *Filepath,
char *Extension // Including the "."
// Pass 0 to retain the whole file path, only without its parent directories
)
{
char *BaseFilename = Filepath + StringLength(Filepath) - 1;
while(BaseFilename > Filepath && *BaseFilename != '/')
{
--BaseFilename;
}
if(*BaseFilename == '/')
{
++BaseFilename;
}
BaseFilename[StringLength(BaseFilename) - StringLength(Extension)] = '\0';
return BaseFilename;
}
void
ConstructTemplatePath(template *Template, int TemplateType)
{
// NOTE(matt): Bespoke template paths are set relative to:
// in Project Edition: ProjectDir
// in Single Edition: Parent directory of .hmml file
if(Template->Metadata.Filename[0] != '/')
{
char Temp[256];
CopyString(Temp, sizeof(Temp), "%s", Template->Metadata.Filename);
char *Ptr = Template->Metadata.Filename;
char *End = Template->Metadata.Filename + sizeof(Template->Metadata.Filename);
if(TemplateType == TEMPLATE_BESPOKE)
{
if(Config.Edition == EDITION_SINGLE)
{
Ptr += CopyString(Ptr, End - Ptr, "%s/", GetDirectoryPath(Config.SingleHMMLFilePath));
}
else
{
Ptr += CopyString(Ptr, End - Ptr, "%s/", Config.ProjectDir);
}
}
else
{
Ptr += CopyString(Ptr, End - Ptr, "%s/", Config.TemplatesDir);
}
CopyString(Ptr, End - Ptr, "%s", Temp);
}
}
int
InitTemplate(template **Template)
{
if(MemoryArena.Ptr - MemoryArena.Location + sizeof(template) > MemoryArena.Size)
{
return RC_ARENA_FULL;
}
*Template = (template *)MemoryArena.Ptr;
Clear((*Template)->Metadata.Filename, 256); // NOTE(matt): template_metadata specifies Filename[256]
(*Template)->Metadata.Validity = 0;
(*Template)->Metadata.TagCount = 0;
for(int i = 0; i < 16; ++i) // NOTE(matt): template_metadata specifies Tag[16]
{
(*Template)->Metadata.Tag[i].Offset = 0;
(*Template)->Metadata.Tag[i].TagCode = 0;
}
MemoryArena.Ptr += sizeof(template);
return RC_SUCCESS;
}
int
ClaimTemplate(template **Template, char *Location, int TemplateType)
{
CopyString((*Template)->Metadata.Filename, sizeof((*Template)->Metadata.Filename), "%s", Location);
ConstructTemplatePath((*Template), TemplateType);
if(TemplateType == TEMPLATE_BESPOKE)
{
fprintf(stderr, "\e[0;35mPacking\e[0m template: %s\n", (*Template)->Metadata.Filename);
}
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));
Clear((*Template)->Metadata.Filename, 256); // NOTE(matt): template_metadata specifies Filename[256]
return RC_ERROR_FILE;
}
fseek(File, 0, SEEK_END);
(*Template)->Buffer.Size = ftell(File);
if(MemoryArena.Ptr - MemoryArena.Location + (*Template)->Buffer.Size > MemoryArena.Size)
{
Clear((*Template)->Metadata.Filename, 256); // NOTE(matt): template_metadata specifies Filename[256]
return RC_ARENA_FULL;
}
(*Template)->Buffer.Location = MemoryArena.Ptr;
(*Template)->Buffer.Ptr = (*Template)->Buffer.Location;
(*Template)->Buffer.ID = (*Template)->Metadata.Filename;
fseek(File, 0, SEEK_SET);
fread((*Template)->Buffer.Location, (*Template)->Buffer.Size, 1, File);
fclose(File);
MemoryArena.Ptr += (*Template)->Buffer.Size;
#if DEBUG
printf(" ClaimTemplate(%s): %d\n"
" Total ClaimedMemory: %ld\n\n", (*Template)->Metadata.Filename, (*Template)->Buffer.Size, MemoryArena.Ptr - MemoryArena.Location);
#endif
return RC_SUCCESS;
}
int
DeclaimTemplate(template *Template)
{
Clear(Template->Metadata.Filename, 256);
Template->Metadata.Validity = 0;
for(int i = 0; i < Template->Metadata.TagCount; ++i)
{
Template->Metadata.Tag[i].Offset = 0;
Template->Metadata.Tag[i].TagCode = 0;
}
Template->Metadata.TagCount = 0;
MemoryArena.Ptr -= (*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;
}
void
StringToColourHash(hsl_colour *Colour, char *String)
{
Colour->Hue = 0;
Colour->Saturation = 0;
Colour->Lightness = 74;
int i;
for(i = 0; String[i]; ++i)
{
Colour->Hue += CharToColour(String[i]).Hue;
Colour->Saturation += CharToColour(String[i]).Saturation;
}
Colour->Hue = Colour->Hue % 360;
Colour->Saturation = Colour->Saturation % 26 + 74;
}
char *
SanitisePunctuation(char *String)
{
char *Ptr = String;
while(*Ptr)
{
if(*Ptr == ' ')
{
*Ptr = '_';
}
if((*Ptr < '0' || *Ptr > '9') &&
(*Ptr < 'a' || *Ptr > 'z') &&
(*Ptr < 'A' || *Ptr > 'Z'))
{
*Ptr = '-';
}
++Ptr;
}
return String;
}
enum
{
INCLUDE_CSS,
INCLUDE_Images,
INCLUDE_JS,
} include_types;
enum
{
PAGE_PLAYER = 1 << 0,
PAGE_INDEX = 1 << 1
} pages;
void
ConstructURLPrefix(buffer *URLPrefix, int IncludeType, int PageType)
{
RewindBuffer(URLPrefix);
if(StringsDiffer(Config.RootURL, ""))
{
CopyStringToBuffer(URLPrefix, "%s/", Config.RootURL);
}
else
{
if(Config.Edition == EDITION_PROJECT)
{
if(PageType == PAGE_PLAYER)
{
CopyStringToBuffer(URLPrefix, "../");
}
CopyStringToBuffer(URLPrefix, "../");
}
}
switch(IncludeType)
{
case INCLUDE_CSS:
if(StringsDiffer(Config.CSSDir, ""))
{
CopyStringToBuffer(URLPrefix, "%s/", Config.CSSDir);
}
break;
case INCLUDE_Images:
if(StringsDiffer(Config.ImagesDir, ""))
{
CopyStringToBuffer(URLPrefix, "%s/", Config.ImagesDir);
}
break;
case INCLUDE_JS:
if(StringsDiffer(Config.JSDir, ""))
{
CopyStringToBuffer(URLPrefix, "%s/", Config.JSDir);
}
break;
}
}
typedef struct
{
char Abbreviation[32];
hsl_colour Colour;
credential_info *Credential;
bool Seen;
} speaker;
typedef struct
{
speaker Speaker[16];
int Count;
} speakers;
enum
{
CreditsError_NoHost,
CreditsError_NoAnnotator,
CreditsError_NoCredentials
} credits_errors;
int
SearchCredentials(buffer *CreditsMenu, bool *HasCreditsMenu, char *Person, char *Role, speakers *Speakers)
{
bool Found = FALSE;
for(int CredentialIndex = 0; CredentialIndex < ArrayCount(Credentials); ++CredentialIndex)
{
if(!StringsDiffer(Person, Credentials[CredentialIndex].Username))
{
if(Speakers)
{
Speakers->Speaker[Speakers->Count].Credential = &Credentials[CredentialIndex];
++Speakers->Count;
}
Found = TRUE;
if(*HasCreditsMenu == FALSE)
{
CopyStringToBuffer(CreditsMenu,
" <div class=\"menu credits\">\n"
" <span>Credits</span>\n"
" <div class=\"credits_container\">\n");
*HasCreditsMenu = TRUE;
}
CopyStringToBuffer(CreditsMenu,
" <span class=\"credit\">\n");
if(*Credentials[CredentialIndex].HomepageURL)
{
CopyStringToBuffer(CreditsMenu,
" <a class=\"person\" href=\"%s\" target=\"_blank\">\n"
" <div class=\"role\">%s</div>\n"
" <div class=\"name\">%s</div>\n"
" </a>\n",
Credentials[CredentialIndex].HomepageURL,
Role,
Credentials[CredentialIndex].CreditedName);
}
else
{
CopyStringToBuffer(CreditsMenu,
" <div class=\"person\">\n"
" <div class=\"role\">%s</div>\n"
" <div class=\"name\">%s</div>\n"
" </div>\n",
Role,
Credentials[CredentialIndex].CreditedName);
}
if(*Credentials[CredentialIndex].SupportIcon && *Credentials[CredentialIndex].SupportURL)
{
buffer URLPrefix;
ClaimBuffer(&URLPrefix, "URLPrefix", 1024);
ConstructURLPrefix(&URLPrefix, INCLUDE_Images, PAGE_PLAYER);
CopyStringToBuffer(CreditsMenu,
" <a class=\"support\" href=\"%s\" target=\"_blank\"><div class=\"support_icon\" data-sprite=\"%s%s\"></div></a>\n",
Credentials[CredentialIndex].SupportURL,
URLPrefix.Location,
Credentials[CredentialIndex].SupportIcon);
DeclaimBuffer(&URLPrefix);
}
CopyStringToBuffer(CreditsMenu,
" </span>\n");
}
}
return Found ? RC_SUCCESS : CreditsError_NoCredentials;
}
void
ClearNullTerminatedString(char *String)
{
while(*String)
{
*String++ = '\0';
}
}
void
InitialString(char *Dest, char *Src)
{
ClearNullTerminatedString(Dest);
*Dest++ = *Src++;
while(*Src++)
{
if(*Src == ' ')
{
++Src;
if(*Src)
{
*Dest++ = *Src;
}
}
}
}
void
GetFirstSubstring(char *Dest, char *Src)
{
ClearNullTerminatedString(Dest);
while(*Src && *Src != ' ')
{
*Dest++ = *Src++;
}
}
void
InitialAndGetFinalString(char *Dest, int DestSize, char *Src)
{
ClearNullTerminatedString(Dest);
int SrcLength = StringLength(Src);
char *SrcPtr = Src + SrcLength - 1;
while(SrcPtr > Src && *SrcPtr != ' ')
{
--SrcPtr;
}
if(*SrcPtr == ' ' && SrcPtr - Src < SrcLength - 1)
{
++SrcPtr;
}
if(Src < SrcPtr)
{
*Dest++ = *Src++;
*Dest++ = '.';
*Dest++ = ' ';
while(Src < SrcPtr - 1)
{
if(*Src == ' ')
{
++Src;
if(*Src)
{
*Dest++ = *Src;
*Dest++ = '.';
*Dest++ = ' ';
}
}
++Src;
}
}
CopyString(Dest, DestSize, "%s", SrcPtr);
}
bool
AbbreviationsClash(speakers *Speakers)
{
for(int i = 0; i < Speakers->Count; ++i)
{
for(int j = i + 1; j < Speakers->Count; ++j)
{
if(!StringsDiffer(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[j].Abbreviation))
{
return TRUE;
}
}
}
return FALSE;
}
void
SortAndAbbreviateSpeakers(speakers *Speakers)
{
for(int i = 0; i < Speakers->Count; ++i)
{
for(int j = i + 1; j < Speakers->Count; ++j)
{
if(StringsDiffer(Speakers->Speaker[i].Credential->Username, Speakers->Speaker[j].Credential->Username) > 0)
{
credential_info *Temp = Speakers->Speaker[j].Credential;
Speakers->Speaker[j].Credential = Speakers->Speaker[i].Credential;
Speakers->Speaker[i].Credential = Temp;
break;
}
}
}
for(int i = 0; i < Speakers->Count; ++i)
{
StringToColourHash(&Speakers->Speaker[i].Colour, Speakers->Speaker[i].Credential->Username);
InitialString(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Credential->CreditedName);
}
int Attempt = 0;
while(AbbreviationsClash(Speakers))
{
for(int i = 0; i < Speakers->Count; ++i)
{
switch(Attempt)
{
case 0: GetFirstSubstring(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Credential->CreditedName); break;
case 1: InitialAndGetFinalString(Speakers->Speaker[i].Abbreviation, sizeof(Speakers->Speaker[i].Abbreviation), Speakers->Speaker[i].Credential->CreditedName); break;
case 2: ClearCopyStringNoFormat(Speakers->Speaker[i].Abbreviation, sizeof(Speakers->Speaker[i].Abbreviation), Speakers->Speaker[i].Credential->Username); break;
}
}
++Attempt;
}
}
int
BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Metadata, speakers *Speakers)
// TODO(matt): Make this take the Credentials, once we are parsing them from a config
{
if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->member, "Host", Speakers) == CreditsError_NoCredentials)
{
printf("No credentials for member %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);
return CreditsError_NoCredentials;
}
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", Speakers) == CreditsError_NoCredentials)
{
printf("No credentials for co-host %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]);
return CreditsError_NoCredentials;
}
}
}
if(Metadata->guest_count > 0)
{
for(int i = 0; i < Metadata->guest_count; ++i)
{
if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->guests[i], "Guest", Speakers) == CreditsError_NoCredentials)
{
printf("No credentials for guest %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]);
return CreditsError_NoCredentials;
}
}
}
if(Speakers->Count > 1)
{
SortAndAbbreviateSpeakers(Speakers);
}
if(Metadata->annotator_count > 0)
{
for(int i = 0; i < Metadata->annotator_count; ++i)
{
if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->annotators[i], "Annotator", 0) == CreditsError_NoCredentials)
{
printf("No credentials for annotator %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]);
return CreditsError_NoCredentials;
}
}
}
else
{
if(*HasCreditsMenu == TRUE)
{
CopyStringToBuffer(CreditsMenu,
" </div>\n"
" </div>\n");
}
fprintf(stderr, "Missing \"annotator\" in the [video] node\n");
return CreditsError_NoAnnotator;
}
if(*HasCreditsMenu == TRUE)
{
CopyStringToBuffer(CreditsMenu,
" </div>\n"
" </div>\n");
}
return RC_SUCCESS;
}
enum
{
REF_SITE = 1 << 0,
REF_PAGE = 1 << 1,
REF_URL = 1 << 2,
REF_TITLE = 1 << 3,
REF_ARTICLE = 1 << 4,
REF_AUTHOR = 1 << 5,
REF_EDITOR = 1 << 6,
REF_PUBLISHER = 1 << 7,
REF_ISBN = 1 << 8,
} reference_fields;
int
BuildReference(ref_info *ReferencesArray, int RefIdentifier, int UniqueRefs, HMML_Reference *Ref, HMML_Annotation *Anno)
{
if(Ref->isbn)
{
CopyString(ReferencesArray[UniqueRefs].ID, sizeof(ReferencesArray[UniqueRefs].ID), "%s", Ref->isbn);
if(!Ref->url) { CopyString(ReferencesArray[UniqueRefs].URL, sizeof(ReferencesArray[UniqueRefs].URL), "https://isbndb.com/book/%s", Ref->isbn); }
else { CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, sizeof(ReferencesArray[UniqueRefs].URL), Ref->url); }
}
else if(Ref->url)
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, sizeof(ReferencesArray[UniqueRefs].ID), Ref->url);
CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, sizeof(ReferencesArray[UniqueRefs].URL), Ref->url);
}
else { return RC_INVALID_REFERENCE; }
int Mask = 0;
if(Ref->site) { Mask |= REF_SITE; }
if(Ref->page) { Mask |= REF_PAGE; }
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; }
// TODO(matt): Consider handling the various combinations more flexibly, unless we defer this stuff until we have the
// reference store, in which we could optionally customise the display of each reference entry
switch(Mask)
{
case (REF_TITLE | REF_AUTHOR | REF_PUBLISHER):
{
CopyString(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), "%s (%s)", Ref->author, Ref->publisher);
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->title);
} break;
case (REF_AUTHOR | REF_SITE | REF_PAGE):
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->site);
CopyString(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), "%s: \"%s\"", Ref->author, Ref->page);
} break;
case (REF_PAGE | REF_TITLE):
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->title);
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->page);
} break;
case (REF_SITE | REF_PAGE):
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->site);
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->page);
} break;
case (REF_SITE | REF_TITLE):
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->site);
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->title);
} break;
case (REF_TITLE | REF_AUTHOR):
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->author);
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->title);
} break;
case (REF_ARTICLE | REF_AUTHOR):
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->author);
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->article);
} break;
case (REF_TITLE | REF_PUBLISHER):
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Ref->publisher);
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->title);
} break;
case REF_TITLE:
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->title);
} break;
case REF_SITE:
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Ref->site);
} break;
default: return RC_INVALID_REFERENCE; break;
}
CopyString(ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Timecode, sizeof(ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Timecode), "%s", Anno->time);
ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Identifier = RefIdentifier;
return RC_SUCCESS;
}
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;
}
}
if(IsMedium)
{
int MediumIndex;
for(MediumIndex = 0; MediumIndex < LocalMedia->Count; ++MediumIndex)
{
if(!StringsDiffer(CategoryMedium[CategoryMediumIndex].Medium, LocalMedia->Category[MediumIndex].Marker))
{
return;
}
if((StringsDiffer(CategoryMedium[CategoryMediumIndex].WrittenName, LocalMedia->Category[MediumIndex].WrittenText)) < 0)
{
int CategoryCount;
for(CategoryCount = LocalMedia->Count; CategoryCount > MediumIndex; --CategoryCount)
{
CopyString(LocalMedia->Category[CategoryCount].Marker, sizeof(LocalMedia->Category[CategoryCount].Marker), "%s", LocalMedia->Category[CategoryCount-1].Marker);
CopyString(LocalMedia->Category[CategoryCount].WrittenText, sizeof(LocalMedia->Category[CategoryCount].WrittenText), "%s", LocalMedia->Category[CategoryCount-1].WrittenText);
}
CopyString(LocalMedia->Category[CategoryCount].Marker, sizeof(LocalMedia->Category[CategoryCount].Marker), "%s", CategoryMedium[CategoryMediumIndex].Medium);
CopyString(LocalMedia->Category[CategoryCount].WrittenText, sizeof(LocalMedia->Category[CategoryCount].WrittenText), "%s", CategoryMedium[CategoryMediumIndex].WrittenName);
break;
}
}
if(MediumIndex == LocalMedia->Count)
{
CopyString(LocalMedia->Category[MediumIndex].Marker, sizeof(LocalMedia->Category[MediumIndex].Marker), "%s", CategoryMedium[CategoryMediumIndex].Medium);
CopyString(LocalMedia->Category[MediumIndex].WrittenText, sizeof(LocalMedia->Category[MediumIndex].WrittenText), "%s", CategoryMedium[CategoryMediumIndex].WrittenName);
}
++LocalMedia->Count;
for(MediumIndex = 0; MediumIndex < GlobalMedia->Count; ++MediumIndex)
{
if(!StringsDiffer(CategoryMedium[CategoryMediumIndex].Medium, GlobalMedia->Category[MediumIndex].Marker))
{
return;
}
if((StringsDiffer(CategoryMedium[CategoryMediumIndex].WrittenName, GlobalMedia->Category[MediumIndex].WrittenText)) < 0)
{
int CategoryCount;
for(CategoryCount = GlobalMedia->Count; CategoryCount > MediumIndex; --CategoryCount)
{
CopyString(GlobalMedia->Category[CategoryCount].Marker, sizeof(GlobalMedia->Category[CategoryCount].Marker), "%s", GlobalMedia->Category[CategoryCount-1].Marker);
CopyString(GlobalMedia->Category[CategoryCount].WrittenText, sizeof(GlobalMedia->Category[CategoryCount].WrittenText), "%s", GlobalMedia->Category[CategoryCount-1].WrittenText);
}
CopyString(GlobalMedia->Category[CategoryCount].Marker, sizeof(GlobalMedia->Category[CategoryCount].Marker), "%s", CategoryMedium[CategoryMediumIndex].Medium);
CopyString(GlobalMedia->Category[CategoryCount].WrittenText, sizeof(GlobalMedia->Category[CategoryCount].WrittenText), "%s", CategoryMedium[CategoryMediumIndex].WrittenName);
break;
}
}
if(MediumIndex == GlobalMedia->Count)
{
CopyString(GlobalMedia->Category[MediumIndex].Marker, sizeof(GlobalMedia->Category[MediumIndex].Marker), "%s", CategoryMedium[CategoryMediumIndex].Medium);
CopyString(GlobalMedia->Category[MediumIndex].WrittenText, sizeof(GlobalMedia->Category[MediumIndex].WrittenText), "%s", CategoryMedium[CategoryMediumIndex].WrittenName);
}
++GlobalMedia->Count;
}
else
{
int TopicIndex;
for(TopicIndex = 0; TopicIndex < LocalTopics->Count; ++TopicIndex)
{
if(!StringsDiffer(Marker, LocalTopics->Category[TopicIndex].Marker))
{
return;
}
if((StringsDiffer(Marker, LocalTopics->Category[TopicIndex].Marker)) < 0)
{
int CategoryCount;
for(CategoryCount = LocalTopics->Count; CategoryCount > TopicIndex; --CategoryCount)
{
CopyString(LocalTopics->Category[CategoryCount].Marker, sizeof(LocalTopics->Category[CategoryCount].Marker), "%s", LocalTopics->Category[CategoryCount-1].Marker);
}
CopyString(LocalTopics->Category[CategoryCount].Marker, sizeof(LocalTopics->Category[CategoryCount].Marker), "%s", Marker);
break;
}
}
if(TopicIndex == LocalTopics->Count)
{
CopyString(LocalTopics->Category[TopicIndex].Marker, sizeof(LocalTopics->Category[TopicIndex].Marker), "%s", Marker);
}
++LocalTopics->Count;
for(TopicIndex = 0; TopicIndex < GlobalTopics->Count; ++TopicIndex)
{
if(!StringsDiffer(Marker, GlobalTopics->Category[TopicIndex].Marker))
{
return;
}
// NOTE(matt): This successfully sorts "nullTopic" at the end, but maybe figure out a more general way to force the
// order of stuff, perhaps blocks of dudes that should sort to the start / end
if(((StringsDiffer(Marker, GlobalTopics->Category[TopicIndex].Marker)) < 0 || !StringsDiffer(GlobalTopics->Category[TopicIndex].Marker, "nullTopic")))
{
if(StringsDiffer(Marker, "nullTopic")) // NOTE(matt): This test (with the above || condition) forces nullTopic never to be inserted, only appended
{
int CategoryCount;
for(CategoryCount = GlobalTopics->Count; CategoryCount > TopicIndex; --CategoryCount)
{
CopyString(GlobalTopics->Category[CategoryCount].Marker, sizeof(GlobalTopics->Category[CategoryCount].Marker), "%s", GlobalTopics->Category[CategoryCount-1].Marker);
}
CopyString(GlobalTopics->Category[CategoryCount].Marker, sizeof(GlobalTopics->Category[CategoryCount].Marker), "%s", Marker);
break;
}
}
}
if(TopicIndex == GlobalTopics->Count)
{
CopyString(GlobalTopics->Category[TopicIndex].Marker, sizeof(GlobalTopics->Category[TopicIndex].Marker), "%s", Marker);
}
++GlobalTopics->Count;
}
}
void
BuildCategories(buffer *AnnotationClass, buffer *CategoryIcons, categories *LocalTopics, categories *LocalMedia, int *MarkerIndex, char *DefaultMedium)
{
bool CategoriesSpan = FALSE;
if(!(LocalTopics->Count == 1 && !StringsDiffer(LocalTopics->Category[0].Marker, "nullTopic")
&& LocalMedia->Count == 1 && !StringsDiffer(LocalMedia->Category[0].Marker, DefaultMedium)))
{
CategoriesSpan = TRUE;
CopyStringToBuffer(CategoryIcons, "<span class=\"cineraCategories\">");
}
if(LocalTopics->Count == 1 && !StringsDiffer(LocalTopics->Category[0].Marker, "nullTopic"))
{
char SanitisedMarker[StringLength(LocalTopics->Category[0].Marker) + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalTopics->Category[0].Marker);
SanitisePunctuation(SanitisedMarker);
CopyStringToBuffer(AnnotationClass, " cat_%s", SanitisedMarker);
}
else
{
for(int i = 0; i < LocalTopics->Count; ++i)
{
char SanitisedMarker[StringLength(LocalTopics->Category[i].Marker) + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalTopics->Category[i].Marker);
SanitisePunctuation(SanitisedMarker);
CopyStringToBuffer(CategoryIcons, "<div title=\"%s\" class=\"category %s\"></div>",
LocalTopics->Category[i].Marker,
SanitisedMarker);
CopyStringToBuffer(AnnotationClass, " cat_%s",
SanitisedMarker);
}
}
if(LocalMedia->Count == 1 && !StringsDiffer(LocalMedia->Category[0].Marker, DefaultMedium))
{
char SanitisedMarker[StringLength(LocalMedia->Category[0].Marker) + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalMedia->Category[0].Marker);
SanitisePunctuation(SanitisedMarker);
CopyStringToBuffer(AnnotationClass, " %s", SanitisedMarker);
}
else
{
for(int i = 0; i < LocalMedia->Count; ++i)
{
char SanitisedMarker[StringLength(LocalMedia->Category[i].Marker) + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalMedia->Category[i].Marker);
SanitisePunctuation(SanitisedMarker);
if(!StringsDiffer(LocalMedia->Category[i].Marker, "afk")) // TODO(matt): Initially hidden config
{
CopyStringToBuffer(AnnotationClass, " off_%s skip", SanitisedMarker); // TODO(matt): Bulletproof this?
}
else
{
for(int j = 0; j < ArrayCount(CategoryMedium); ++j)
{
if(!StringsDiffer(LocalMedia->Category[i].Marker, CategoryMedium[j].Medium))
{
CopyStringToBuffer(CategoryIcons, "<div title=\"%s\" class=\"categoryMedium %s\">%s</div>",
LocalMedia->Category[i].WrittenText,
LocalMedia->Category[i].Marker,
CategoryMedium[j].Icon);
CopyStringToBuffer(AnnotationClass, " %s", SanitisedMarker);
}
}
}
}
}
if(CategoriesSpan)
{
CopyStringToBuffer(CategoryIcons, "</span>");
}
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)
{
fprintf(stderr, "\e[0;35mFetching\e[0m quotes: %s\n", QuotesURL);
CURL *curl = curl_easy_init();
if(curl) {
CURLcode CurlReturnCode;
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &QuoteStaging->Ptr);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlIntoBuffer);
curl_easy_setopt(curl, CURLOPT_URL, QuotesURL);
if((CurlReturnCode = curl_easy_perform(curl)))
{
fprintf(stderr, "%s\n", curl_easy_strerror(CurlReturnCode));
}
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[8] = { 0 };
char InTime[16] = { 0 };
QuoteStaging->Ptr += CopyStringNoFormatT(InID, sizeof(InID), QuoteStaging->Ptr, ',') + 1; // Skip past the ','
if(StringToInt(InID) == ID)
{
QuoteStaging->Ptr += CopyStringNoFormatT(InTime, sizeof(InTime), QuoteStaging->Ptr, ',') + 1; // Skip past the ','
long int Time = StringToInt(InTime);
char DayString[3] = { 0 };
strftime(DayString, 3, "%d", gmtime(&Time));
int Day = StringToInt(DayString);
char DaySuffix[3]; if(DayString[1] == '1' && Day != 11) { CopyString(DaySuffix, sizeof(DaySuffix), "st"); }
else if(DayString[1] == '2' && Day != 12) { CopyString(DaySuffix, sizeof(DaySuffix), "nd"); }
else if(DayString[1] == '3' && Day != 13) { CopyString(DaySuffix, sizeof(DaySuffix), "rd"); }
else { CopyString(DaySuffix, sizeof(DaySuffix), "th"); }
char MonthYear[32];
strftime(MonthYear, 32, "%B, %Y", gmtime(&Time));
CopyString(Info->Date, sizeof(Info->Date), "%d%s %s", Day, DaySuffix, MonthYear);
CopyStringNoFormatT(Info->Text, sizeof(Info->Text), 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, bool ShouldFetchQuotes)
{
char QuoteCacheDir[256];
CopyString(QuoteCacheDir, sizeof(QuoteCacheDir), "%s/quotes", Config.CacheDir);
char QuoteCachePath[256];
CopyString(QuoteCachePath, sizeof(QuoteCachePath), "%s/%s", QuoteCacheDir, Speaker);
FILE *QuoteCache;
char QuotesURL[256];
// TODO(matt): Make the URL configurable and also handle the case in which the .raw isn't available
CopyString(QuotesURL, sizeof(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(ShouldFetchQuotes || 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) == RC_UNFOUND)
{
FreeBuffer(&QuoteStaging);
return RC_UNFOUND;
}
}
}
else
{
CurlQuotes(&QuoteStaging, QuotesURL);
int CacheSize = QuoteStaging.Ptr - QuoteStaging.Location;
QuoteStaging.Ptr = QuoteStaging.Location;
if(SearchQuotes(&QuoteStaging, CacheSize, Info, ID) == RC_UNFOUND)
{
FreeBuffer(&QuoteStaging);
return RC_UNFOUND;
}
}
return RC_SUCCESS;
}
int
GenerateTopicColours(char *Topic)
{
char SanitisedTopic[StringLength(Topic) + 1];
CopyString(SanitisedTopic, sizeof(SanitisedTopic), "%s", Topic);
SanitisePunctuation(SanitisedTopic);
for(int i = 0; i < ArrayCount(CategoryMedium); ++i)
{
if(!StringsDiffer(Topic, CategoryMedium[i].Medium))
{
return RC_NOOP;
}
}
file_buffer Topics;
Topics.Buffer.ID = "Topics";
if(StringsDiffer(Config.CSSDir, ""))
{
CopyString(Topics.Path, sizeof(Topics.Path), "%s/%s/cinera_topics.css", Config.RootDir, Config.CSSDir);
}
else
{
CopyString(Topics.Path, sizeof(Topics.Path), "%s/cinera_topics.css", Config.RootDir);
}
char *Ptr = Topics.Path + StringLength(Topics.Path) - 1;
while(*Ptr != '/')
{
--Ptr;
}
*Ptr = '\0';
DIR *CSSDirHandle; // TODO(matt): open()
if(!(CSSDirHandle = opendir(Topics.Path)))
{
if(MakeDir(Topics.Path) == RC_ERROR_DIRECTORY)
{
LogError(LOG_ERROR, "Unable to create directory %s: %s", Topics.Path, strerror(errno));
fprintf(stderr, "Unable to create directory %s: %s\n", Topics.Path, strerror(errno));
return RC_ERROR_DIRECTORY;
};
}
closedir(CSSDirHandle);
*Ptr = '/';
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.Buffer.Size);
fclose(MemLog);
printf(" Allocated Topics (%d)\n", Topics.Buffer.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(SanitisedTopic, 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;
}
if(!StringsDiffer(Topic, "nullTopic"))
{
fprintf(Topics.Handle, ".category.%s { border: 1px solid transparent; background: transparent; }\n",
SanitisedTopic);
}
else
{
hsl_colour Colour;
StringToColourHash(&Colour, Topic);
fprintf(Topics.Handle, ".category.%s { border: 1px solid hsl(%d, %d%%, %d%%); background: hsl(%d, %d%%, %d%%); }\n",
SanitisedTopic, Colour.Hue, Colour.Saturation, Colour.Lightness, Colour.Hue, Colour.Saturation, Colour.Lightness);
}
fclose(Topics.Handle);
FreeBuffer(&Topics.Buffer);
return RC_SUCCESS;
}
else
{
// NOTE(matt): Maybe it shouldn't be possible to hit this case now that we MakeDir the actually dir...
perror(Topics.Path);
return RC_ERROR_FILE;
}
}
void
PrintUsage(char *BinaryLocation, config *DefaultConfig)
{
fprintf(stderr,
"Usage: %s [option(s)] filename(s)\n"
"\n"
"Options:\n"
" Paths: \e[1;30m(advisedly universal, but may be set per-(sub)project as required)\e[0m\n"
" -r <root directory>\n"
" Override default root directory (\"%s\")\n"
" -R <root URL>\n"
" Override default root URL (\"%s\")\n"
" \e[1;31mIMPORTANT\e[0m: -r and -R must correspond to the same location\n"
" \e[1;30mUNSUPPORTED: If you move files from RootDir, the RootURL should\n"
" correspond to the resulting location\e[0m\n"
"\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"
"\n"
" Project Settings:\n"
" -p <project ID>\n"
" Set the project ID, equal to the \"project\" field in the HMML files\n"
" NOTE: Setting the project ID triggers PROJECT EDITION\n"
" -m <default medium>\n"
" Override default default medium (\"%s\")\n"
" \e[1;30mKnown project defaults:\n",
BinaryLocation,
DefaultConfig->RootDir,
DefaultConfig->RootURL,
DefaultConfig->CSSDir,
DefaultConfig->ImagesDir,
DefaultConfig->JSDir,
DefaultConfig->DefaultMedium);
for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex)
{
fprintf(stderr, " %s:",
ProjectInfo[ProjectIndex].ProjectID);
// NOTE(matt): This kind of thing really needs to loop over the dudes once to find the longest one
for(int i = 11; i > StringLength(ProjectInfo[ProjectIndex].ProjectID); i -= 4)
{
fprintf(stderr, "\t");
}
fprintf(stderr, "%s\n",
ProjectInfo[ProjectIndex].Medium);
}
fprintf(stderr,
"\e[0m -s <style>\n"
" Set the style / theme, corresponding to a cinera__*.css file\n"
" This is equal to the \"project\" field in the HMML files by default\n"
"\n"
" Project Input Paths\n"
" -d <annotations directory>\n"
" Override default annotations directory (\"%s\")\n"
" -t <templates directory>\n"
" Override default templates directory (\"%s\")\n"
"\n"
" -x <index template location>\n"
" Set index template file path, either absolute or relative to\n"
" template directory, and enable integration\n"
" -y <player template location>\n"
" Set player template file path, either absolute or relative\n"
" to template directory, and enable integration\n"
"\n"
" Project Output Paths\n"
" -b <base output directory>\n"
" Override project's default base output directory (\"%s\")\n"
" -B <base URL>\n"
" Override default base URL (\"%s\")\n"
" NOTE: This must be set, if -n or -a are to be used\n"
"\n"
" -n <index location>\n"
" Override default index location (\"%s\"), relative to base\n"
" -a <player location>\n"
" Override default player location (\"%s\"), relative to base\n"
" NOTE: The PlayerURLPrefix is currently hardcoded in cinera.c but\n"
" will be configurable in the full configuration system\n"
"\n"
" Single Edition Output Path\n"
" -o <output location>\n"
" Override default output player location (\"%s\")\n"
"\n"
" -1\n"
" Open search result links in the same browser tab\n"
" NOTE: Ideal for a guide embedded in an iframe\n"
" -f\n"
" Force integration with an incomplete template\n"
" -g\n"
" Ignore video privacy status\n"
" NOTE: For use with projects whose videos are known to all be public,\n"
" to save us having to check their privacy status\n"
" -q\n"
" Quit after syncing with annotation files in project input directory\n"
" \e[1;30mUNSUPPORTED: This is likely to be removed in the future\e[0m\n"
" -w\n"
" Force quote cache rebuild \e[1;30m(memory aid: \"wget\")\e[0m\n"
"\n"
" -l <n>\n"
" Override default log level (%d), where n is from 0 (terse) to 7 (verbose)\n"
" -u <seconds>\n"
" Override default update interval (%d)\n"
//" -c config location\n"
"\n"
" -e\n"
" Display (examine) index file and exit\n"
" -v\n"
" Display version and exit\n"
" -h\n"
" Display this help\n"
"\n"
"Template:\n"
" A complete Index Template shall contain exactly one each of the following tags:\n"
" <!-- __CINERA_INCLUDES__ -->\n"
" to put inside your own <head></head>\n"
" <!-- __CINERA_INDEX__ -->\n"
"\n"
" A complete Player Template shall contain exactly one each of the following tags:\n"
" <!-- __CINERA_INCLUDES__ -->\n"
" to put inside your own <head></head>\n"
" <!-- __CINERA_MENUS__ -->\n"
" <!-- __CINERA_PLAYER__ -->\n"
" <!-- __CINERA_SCRIPT__ -->\n"
" must come after <!-- __CINERA_MENUS__ --> and <!-- __CINERA_PLAYER__ -->\n"
"\n"
" Optional tags available for use in your Player Template:\n"
" <!-- __CINERA_TITLE__ -->\n"
" <!-- __CINERA_VIDEO_ID__ -->\n"
"\n"
" Other available tags:\n"
" <!-- __CINERA_PROJECT__ -->\n"
" <!-- __CINERA_PROJECT_ID__ -->\n"
" <!-- __CINERA_THEME__ -->\n"
" <!-- __CINERA_URL__ -->\n"
" Only really usable if BaseURL is set \e[1;30m(-B)\e[0m\n"
" <!-- __CINERA_CUSTOM0__ -->\n"
" <!-- __CINERA_CUSTOM1__ -->\n"
" <!-- __CINERA_CUSTOM2__ -->\n"
"\n"
" <!-- __CINERA_CUSTOM15__ -->\n"
" Freeform buffers for small snippets of localised information, e.g. a\n"
" single <a> element or perhaps a <!-- comment -->\n"
" They correspond to the custom0 to custom15 attributes in the [video]\n"
" node in your .hmml files\n"
" 0 to 11 may hold up to 255 characters\n"
" 12 to 15 may hold up to 1023 characters\n"
"\n"
"HMML Specification:\n"
" https://git.handmade.network/Annotation-Pushers/Annotation-System/wikis/hmmlspec\n",
DefaultConfig->ProjectDir,
DefaultConfig->TemplatesDir,
DefaultConfig->BaseDir,
DefaultConfig->BaseURL,
DefaultConfig->IndexLocation,
DefaultConfig->PlayerLocation,
DefaultConfig->OutLocation,
DefaultConfig->LogLevel,
DefaultConfig->UpdateInterval);
}
void
DepartComment(buffer *Template)
{
while(Template->Ptr - Template->Location < Template->Size)
{
if(!StringsDifferT("-->", Template->Ptr, 0))
{
Template->Ptr += StringLength("-->");
break;
}
++Template->Ptr;
}
}
int
ValidateTemplate(template **Template, char *Location, int TemplateType)
{
// TODO(matt): Record line numbers and contextual information:
// <? ?>
// <!-- -->
// < >
// <script </script>
#if 1
int Return = ClaimTemplate(Template, Location, TemplateType);
switch(Return)
{
case RC_ARENA_FULL:
case RC_ERROR_FILE:
return Return;
case RC_SUCCESS:
break;
}
#endif
buffer Errors;
if(ClaimBuffer(&Errors, "Errors", Kilobytes(1)) == RC_ARENA_FULL) { DeclaimTemplate(*Template); return RC_ARENA_FULL; };
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)
{
NextTagSearch:
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
*
*/
int ThisTagCode = Tags[i].Code;
char *ThisTagName = Tags[i].Tag;
switch(ThisTagCode)
{
case TAG_INDEX:
FoundIndex = TRUE;
goto RecordTag;
case TAG_INCLUDES:
if(!(Config.Mode & MODE_FORCEINTEGRATION) && FoundIncludes == TRUE)
{
CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", ThisTagName);
HaveErrors = TRUE;
}
FoundIncludes = TRUE;
goto RecordTag;
case TAG_MENUS:
if(!(Config.Mode & MODE_FORCEINTEGRATION) && FoundMenus == TRUE)
{
CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", Tags[i].Tag);
HaveErrors = TRUE;
}
FoundMenus = TRUE;
goto RecordTag;
case TAG_PLAYER:
if(!(Config.Mode & MODE_FORCEINTEGRATION) && FoundPlayer == TRUE)
{
CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", Tags[i].Tag);
HaveErrors = TRUE;
}
FoundPlayer = TRUE;
goto RecordTag;
case TAG_SCRIPT:
if(!(Config.Mode & MODE_FORCEINTEGRATION) && (FoundMenus == FALSE || FoundPlayer == FALSE))
{
CopyStringToBuffer(&Errors, "<!-- %s --> must come after <!-- __CINERA_MENUS__ --> and <!-- __CINERA_PLAYER__ -->\n", Tags[i].Tag);
HaveErrors = TRUE;
}
if(!(Config.Mode & MODE_FORCEINTEGRATION) && FoundScript == TRUE)
{
CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", Tags[i].Tag);
HaveErrors = TRUE;
}
FoundScript = TRUE;
goto RecordTag;
default: // NOTE(matt): All freely usable tags should hit this case
RecordTag:
(*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].Offset = CommentStart - Previous;
(*Template)->Metadata.Tag[(*Template)->Metadata.TagCount].TagCode = ThisTagCode;
(*Template)->Metadata.TagCount++;
DepartComment(&(*Template)->Buffer);
Previous = (*Template)->Buffer.Ptr;
goto NextTagSearch;
};
}
}
++(*Template)->Buffer.Ptr;
}
}
else
{
++(*Template)->Buffer.Ptr;
}
}
if(FoundIndex)
{
(*Template)->Metadata.Validity |= PAGE_INDEX;
}
if(!HaveErrors && FoundIncludes && FoundMenus && FoundPlayer && FoundScript)
{
(*Template)->Metadata.Validity |= PAGE_PLAYER;
}
if(!(Config.Mode & MODE_FORCEINTEGRATION))
{
if(TemplateType == TEMPLATE_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);
DeclaimBuffer(&Errors);
DeclaimTemplate(*Template);
return RC_INVALID_TEMPLATE;
}
else if((TemplateType == TEMPLATE_PLAYER || TemplateType == TEMPLATE_BESPOKE) && !((*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);
DeclaimBuffer(&Errors);
DeclaimTemplate(*Template);
return RC_INVALID_TEMPLATE;
}
}
DeclaimBuffer(&Errors);
return RC_SUCCESS;
}
void
ConstructIndexURL(buffer *IndexURL)
{
RewindBuffer(IndexURL);
if(StringsDiffer(Config.BaseURL, ""))
{
CopyStringToBuffer(IndexURL, "%s/", Config.BaseURL);
if(StringsDiffer(Config.IndexLocation, ""))
{
CopyStringToBuffer(IndexURL, "%s/", Config.IndexLocation);
}
}
}
void
ConstructPlayerURL(buffer *PlayerURL, char *BaseFilename)
{
RewindBuffer(PlayerURL);
if(StringsDiffer(Config.BaseURL, ""))
{
CopyStringToBuffer(PlayerURL, "%s/", Config.BaseURL);
if(StringsDiffer(Config.PlayerLocation, ""))
{
CopyStringToBuffer(PlayerURL, "%s/", Config.PlayerLocation);
}
}
if(StringsDiffer(BaseFilename, ""))
{
if(StringsDiffer(Config.PlayerURLPrefix, ""))
{
char *Ptr = BaseFilename + StringLength(Config.ProjectID);
CopyStringToBuffer(PlayerURL, "%s%s/", Config.PlayerURLPrefix, Ptr);
}
else
{
CopyStringToBuffer(PlayerURL, "%s/", BaseFilename);
}
}
}
bool
MediumExists(char *Medium)
{
for(int i = 0; i < ArrayCount(CategoryMedium); ++i)
{
if(!StringsDiffer(Medium, CategoryMedium[i].Medium))
{
return TRUE;
}
}
fprintf(stderr, "Specified default medium \"%s\" not available. Valid media are:\n", Medium);
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", Medium);
return FALSE;
}
int
ReadFileIntoBuffer(file_buffer *File, int BufferPadding)
{
if(!(File->Handle = fopen(File->Path, "r"))) // TODO(matt): Fuller error handling
{
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);
File->Buffer.ID = File->Path;
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;
}
typedef struct
{
char *BaseFilename;
char *Title;
} neighbour;
typedef struct
{
neighbour Prev;
neighbour Next;
} neighbours;
int
ExamineIndex(index *Index)
{
int IndexMetadataFileReadCode = ReadFileIntoBuffer(&Index->Metadata, 0);
switch(IndexMetadataFileReadCode)
{
case RC_ERROR_MEMORY:
return RC_ERROR_MEMORY;
case RC_ERROR_FILE:
fprintf(stderr, "Unable to open index file %s: %s\n", Index->Metadata.Path, strerror(errno));
return RC_ERROR_FILE;
case RC_SUCCESS:
break;
}
Index->Header = *(index_header *)Index->Metadata.Buffer.Ptr;
// TODO(matt): Check that we're the current version, and maybe print out stuff accordingly? Or do we just straight up
// enforce that examining an index requires upgrading to the current version?
printf("Current:\n"
"\tDBVersion: %d\n"
"\tAppVersion: %d.%d.%d\n"
"\tHMMLVersion: %d.%d.%d\n"
"\n"
"Initial:\n"
"\tDBVersion: %d\n"
"\tAppVersion: %d.%d.%d\n"
"\tHMMLVersion: %d.%d.%d\n"
"\n"
"Project ID: %s\n"
"Project Full Name: %s\n"
"\n"
"Base URL: %s\n"
"Relative Index Page Location: %s\n"
"Relative Player Page Location: %s\n"
"Player Page URL Prefix: %s\n"
"\n"
"Entries: %d\n",
Index->Header.CurrentDBVersion,
Index->Header.CurrentAppVersion.Major, Index->Header.CurrentAppVersion.Minor, Index->Header.CurrentAppVersion.Patch,
Index->Header.CurrentHMMLVersion.Major, Index->Header.CurrentHMMLVersion.Minor, Index->Header.CurrentHMMLVersion.Patch,
Index->Header.InitialDBVersion,
Index->Header.InitialAppVersion.Major, Index->Header.InitialAppVersion.Minor, Index->Header.InitialAppVersion.Patch,
Index->Header.InitialHMMLVersion.Major, Index->Header.InitialHMMLVersion.Minor, Index->Header.InitialHMMLVersion.Patch,
Index->Header.ProjectID,
Index->Header.ProjectName,
Index->Header.BaseURL,
StringsDiffer(Index->Header.IndexLocation, "") ? Index->Header.IndexLocation : "(same as Base URL)",
StringsDiffer(Index->Header.PlayerLocation, "") ? Index->Header.PlayerLocation : "(directly descended from Base URL)",
StringsDiffer(Index->Header.PlayerURLPrefix, "") ? Index->Header.PlayerURLPrefix : "(no special prefix, the player page URLs equal their entry's base filename)",
Index->Header.EntryCount);
Index->Metadata.Buffer.Ptr += sizeof(index_header);
for(int EntryIndex = 0; EntryIndex < Index->Header.EntryCount; ++EntryIndex)
{
Index->Entry = *(index_metadata *)Index->Metadata.Buffer.Ptr;
printf(" %3d\t%s%sSize: %4d\t%d\t%d\t%d\t%d\n"
"\t %s\n",
EntryIndex + 1, Index->Entry.BaseFilename,
StringLength(Index->Entry.BaseFilename) > 8 ? "\t" : "\t\t", // NOTE(matt): Janktasm
Index->Entry.Size,
Index->Entry.LinkOffsets.PrevStart,
Index->Entry.LinkOffsets.PrevEnd,
Index->Entry.LinkOffsets.NextStart,
Index->Entry.LinkOffsets.NextEnd,
Index->Entry.Title);
Index->Metadata.Buffer.Ptr += sizeof(index_metadata);
}
FreeBuffer(&Index->Metadata.Buffer);
return RC_SUCCESS;
}
enum
{
C_SEEK_FORWARDS,
C_SEEK_BACKWARDS
} seek_directions;
enum
{
C_SEEK_START, // First character of string
C_SEEK_BEFORE, // Character before first character
C_SEEK_END, // Last character of string
C_SEEK_AFTER // Character after last character
} seek_positions;
int
SeekBufferForString(buffer *Buffer, char *String,
int Direction, /* seek_directions */
int Position /* seek_positions */)
{
// TODO(matt): Optimise? Some means of analysing the String to increment
// the pointer in bigger strides
// Perhaps count up runs of consecutive chars and seek for the char with
// the longest run, in strides of that run-length
char *InitialLocation = Buffer->Ptr;
if(Direction == C_SEEK_FORWARDS)
{
while(Buffer->Ptr - Buffer->Location < Buffer->Size - StringLength(String)
&& StringsDifferT(String, Buffer->Ptr, 0))
{
++Buffer->Ptr;
}
}
else
{
while(Buffer->Ptr > Buffer->Location
&& StringsDifferT(String, Buffer->Ptr, 0))
{
--Buffer->Ptr;
}
}
if(StringsDifferT(String, Buffer->Ptr, 0))
{
Buffer->Ptr = InitialLocation;
return RC_UNFOUND;
}
switch(Position)
{
case C_SEEK_START:
break;
case C_SEEK_BEFORE:
if(Buffer->Ptr > Buffer->Location)
{
--Buffer->Ptr;
break;
}
else
{
return RC_ERROR_SEEK; // Ptr remains at string start
}
case C_SEEK_END:
Buffer->Ptr += StringLength(String) - 1;
break;
case C_SEEK_AFTER:
if(Buffer->Size >= Buffer->Ptr - Buffer->Location + StringLength(String))
{
Buffer->Ptr += StringLength(String);
break;
}
else
{
return RC_ERROR_SEEK; // Ptr remains at string start
// NOTE(matt): Should it, however, be left at the end of the string?
}
}
return RC_SUCCESS;
}
#define HMMLCleanup() \
DeclaimBuffer(&CreditsMenu); \
DeclaimBuffer(&FilterMedia); \
DeclaimBuffer(&FilterTopics); \
DeclaimBuffer(&FilterMenu); \
DeclaimBuffer(&ReferenceMenu); \
DeclaimBuffer(&QuoteMenu); \
hmml_free(&HMML);
void
ClearTerminalRow(int Length)
{
fprintf(stderr, "\r");
for(int i = 0; i < Length; ++i)
{
fprintf(stderr, " ");
}
fprintf(stderr, "\r");
}
bool
VideoIsPrivate(char *VideoID)
{
// NOTE(matt): Currently only supports YouTube
char Message[128];
CopyString(Message, sizeof(Message), "\e[0;35mChecking\e[0m privacy status of: https://youtube.com/watch?v=%s", VideoID);
fprintf(stderr, "%s", Message);
int MessageLength = StringLength(Message);
buffer VideoAPIResponse;
ClaimBuffer(&VideoAPIResponse, "VideoAPIResponse", Kilobytes(1));
CURL *curl = curl_easy_init();
if(curl) {
LastPrivacyCheck = time(0);
#define APIKey "AIzaSyAdV2U8ivPk8PHMaPMId0gynksw_gdzr9k"
char URL[1024] = {0};
CopyString(URL, sizeof(URL), "https://www.googleapis.com/youtube/v3/videos?key=%s&part=status&id=%s", APIKey, VideoID);
CURLcode CurlReturnCode;
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &VideoAPIResponse.Ptr);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlIntoBuffer);
curl_easy_setopt(curl, CURLOPT_URL, URL);
if((CurlReturnCode = curl_easy_perform(curl)))
{
fprintf(stderr, "%s\n", curl_easy_strerror(CurlReturnCode));
}
curl_easy_cleanup(curl);
VideoAPIResponse.Ptr = VideoAPIResponse.Location;
SeekBufferForString(&VideoAPIResponse, "{", C_SEEK_FORWARDS, C_SEEK_AFTER);
SeekBufferForString(&VideoAPIResponse, "\"totalResults\": ", C_SEEK_FORWARDS, C_SEEK_AFTER);
if(*VideoAPIResponse.Ptr == '0')
{
DeclaimBuffer(&VideoAPIResponse);
// printf("Private video: https://youtube.com/watch?v=%s\n", VideoID);
ClearTerminalRow(MessageLength);
return TRUE;
}
SeekBufferForString(&VideoAPIResponse, "{", C_SEEK_FORWARDS, C_SEEK_AFTER);
SeekBufferForString(&VideoAPIResponse, "\"privacyStatus\": \"", C_SEEK_FORWARDS, C_SEEK_AFTER);
char Status[16];
CopyStringNoFormatT(Status, sizeof(Status), VideoAPIResponse.Ptr, '\"');
if(!StringsDiffer(Status, "public"))
{
DeclaimBuffer(&VideoAPIResponse);
ClearTerminalRow(MessageLength);
return FALSE;
}
}
DeclaimBuffer(&VideoAPIResponse);
// printf("Unlisted video: https://youtube.com/watch?v=%s\n", VideoID);
ClearTerminalRow(MessageLength);
return TRUE;
}
typedef struct
{
index_metadata Prev, This, Next;
bool PrevIsFirst, NextIsFinal;
short int PrevIndex, ThisIndex, NextIndex;
} neighbourhood;
int
LinearSearchForSpeaker(speakers Speakers, char *Username)
{
for(int i = 0; i < Speakers.Count; ++i)
{
if(!StringsDifferCaseInsensitive(Speakers.Speaker[i].Credential->Username, Username))
{
return i;
}
}
return -1;
}
bool
IsCategorisedAFK(HMML_Annotation Anno)
{
for(int i = 0; i < Anno.marker_count; ++i)
{
if(!StringsDiffer(Anno.markers[i].marker, "afk"))
{
return TRUE;
}
}
return FALSE;
}
int
HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filename, neighbourhood *N)
{
RewindBuffer(&CollationBuffers->IncludesPlayer);
RewindBuffer(&CollationBuffers->Menus);
RewindBuffer(&CollationBuffers->Player);
RewindBuffer(&CollationBuffers->ScriptPlayer);
RewindBuffer(&CollationBuffers->IncludesIndex);
RewindBuffer(&CollationBuffers->Search);
*CollationBuffers->Custom0 = '\0';
*CollationBuffers->Custom1 = '\0';
*CollationBuffers->Custom2 = '\0';
*CollationBuffers->Custom3 = '\0';
*CollationBuffers->Custom4 = '\0';
*CollationBuffers->Custom5 = '\0';
*CollationBuffers->Custom6 = '\0';
*CollationBuffers->Custom7 = '\0';
*CollationBuffers->Custom8 = '\0';
*CollationBuffers->Custom9 = '\0';
*CollationBuffers->Custom10 = '\0';
*CollationBuffers->Custom11 = '\0';
*CollationBuffers->Custom12 = '\0';
*CollationBuffers->Custom13 = '\0';
*CollationBuffers->Custom14 = '\0';
*CollationBuffers->Custom15 = '\0';
*CollationBuffers->Title = '\0';
char Filepath[256];
if(Config.Edition == EDITION_PROJECT)
{
CopyString(Filepath, sizeof(Filepath), "%s/%s", Config.ProjectDir, Filename);
}
else
{
CopyString(Filepath, sizeof(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)
{
bool HaveErrors = FALSE;
char *BaseFilename = GetBaseFilename(Filename, ".hmml");
if(StringLength(BaseFilename) > MAX_BASE_FILENAME_LENGTH)
{
fprintf(stderr, "\e[1;31mBase filename \"%s\" is too long (%d/%d characters)\e[0m\n", BaseFilename, StringLength(BaseFilename), MAX_BASE_FILENAME_LENGTH);
HaveErrors = TRUE;
}
if(!HMML.metadata.title)
{
fprintf(stderr, "Please set the title attribute in the [video] node of your .hmml file\n");
HaveErrors = TRUE;
}
else if(StringLength(HMML.metadata.title) > MAX_TITLE_LENGTH)
{
fprintf(stderr, "\e[1;31mVideo title \"%s\" is too long (%d/%d characters)\e[0m\n", HMML.metadata.title, StringLength(HMML.metadata.title), MAX_TITLE_LENGTH);
HaveErrors = TRUE;
}
else
{
CopyString(CollationBuffers->Title, sizeof(CollationBuffers->Title), "%s", HMML.metadata.title);
}
if(!HMML.metadata.member)
{
fprintf(stderr, "Please set the member attribute in the [video] node of your .hmml file\n");
HaveErrors = TRUE;
}
if(!StringsDiffer(CollationBuffers->Theme, ""))
{
if(HMML.metadata.project)
{
CopyStringNoFormat(CollationBuffers->Theme, sizeof(CollationBuffers->Theme), HMML.metadata.project);
}
else
{
fprintf(stderr, "Unable to determine which theme to apply to the HTML\n"
"Please set at least one of:\n"
"\t1. project attribute in the [video] node of your .hmml file\n"
"\t2. ProjectID on the command line with -p\n"
"\t3. Style on the command line with -s\n"
);
HaveErrors = TRUE;
}
}
if(!HMML.metadata.id)
{
fprintf(stderr, "Please set the id attribute in the [video] node of your .hmml file\n");
HaveErrors = TRUE;
}
CopyString(CollationBuffers->VideoID, sizeof(CollationBuffers->VideoID), "%s", HMML.metadata.id);
buffer URLPlayer;
ClaimBuffer(&URLPlayer, "URLPlayer", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH);
ConstructPlayerURL(&URLPlayer, BaseFilename);
CopyString(CollationBuffers->URLPlayer, sizeof(CollationBuffers->URLPlayer), "%s", URLPlayer.Location);
DeclaimBuffer(&URLPlayer);
if(HMML.metadata.project && (!StringsDiffer(CollationBuffers->ProjectID, "") || !StringsDiffer(CollationBuffers->ProjectName, "")))
{
for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex)
{
if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID,
HMML.metadata.project))
{
CopyStringNoFormat(CollationBuffers->ProjectID, sizeof(CollationBuffers->ProjectID), HMML.metadata.project);
CopyStringNoFormat(CollationBuffers->ProjectName, sizeof(CollationBuffers->ProjectName), ProjectInfo[ProjectIndex].FullName);
break;
}
}
}
char *DefaultMedium = Config.DefaultMedium;
if(HMML.metadata.medium)
{
if(MediumExists(HMML.metadata.medium))
{
DefaultMedium = HMML.metadata.medium;
}
else { HaveErrors = TRUE; }
}
// TODO(matt): Consider simply making these as buffers and claiming the necessary amount for them
// The nice thing about doing it this way, though, is that it encourages bespoke template use, which should
// usually be the more convenient way for people to write greater amounts of localised information
for(int CustomIndex = 0; CustomIndex < HMML_CUSTOM_ATTR_COUNT; ++CustomIndex)
{
if(HMML.metadata.custom[CustomIndex])
{
int LengthOfString = StringLength(HMML.metadata.custom[CustomIndex]);
if(LengthOfString > (CustomIndex < 12 ? MAX_CUSTOM_SNIPPET_SHORT_LENGTH : MAX_CUSTOM_SNIPPET_LONG_LENGTH))
{
fprintf(stderr, "\e[1;31mCustom string %d \"\e[0m%s\e[1;31m\" is too long (%d/%d characters)\e[0m\n", CustomIndex, HMML.metadata.custom[CustomIndex], LengthOfString, CustomIndex < 12 ? MAX_CUSTOM_SNIPPET_SHORT_LENGTH : MAX_CUSTOM_SNIPPET_LONG_LENGTH);
if(LengthOfString < MAX_CUSTOM_SNIPPET_LONG_LENGTH)
{
fprintf(stderr, "Consider using custom12 to custom15, which can hold %d characters\n", MAX_CUSTOM_SNIPPET_LONG_LENGTH);
}
else
{
fprintf(stderr, "Consider using a bespoke template for longer amounts of localised information\n");
}
HaveErrors = TRUE;
}
else
{
switch(CustomIndex)
{
case 0: CopyStringNoFormat(CollationBuffers->Custom0, sizeof(CollationBuffers->Custom0), HMML.metadata.custom[CustomIndex]); break;
case 1: CopyStringNoFormat(CollationBuffers->Custom1, sizeof(CollationBuffers->Custom1), HMML.metadata.custom[CustomIndex]); break;
case 2: CopyStringNoFormat(CollationBuffers->Custom2, sizeof(CollationBuffers->Custom2), HMML.metadata.custom[CustomIndex]); break;
case 3: CopyStringNoFormat(CollationBuffers->Custom3, sizeof(CollationBuffers->Custom3), HMML.metadata.custom[CustomIndex]); break;
case 4: CopyStringNoFormat(CollationBuffers->Custom4, sizeof(CollationBuffers->Custom4), HMML.metadata.custom[CustomIndex]); break;
case 5: CopyStringNoFormat(CollationBuffers->Custom5, sizeof(CollationBuffers->Custom5), HMML.metadata.custom[CustomIndex]); break;
case 6: CopyStringNoFormat(CollationBuffers->Custom6, sizeof(CollationBuffers->Custom6), HMML.metadata.custom[CustomIndex]); break;
case 7: CopyStringNoFormat(CollationBuffers->Custom7, sizeof(CollationBuffers->Custom7), HMML.metadata.custom[CustomIndex]); break;
case 8: CopyStringNoFormat(CollationBuffers->Custom8, sizeof(CollationBuffers->Custom8), HMML.metadata.custom[CustomIndex]); break;
case 9: CopyStringNoFormat(CollationBuffers->Custom9, sizeof(CollationBuffers->Custom9), HMML.metadata.custom[CustomIndex]); break;
case 10: CopyStringNoFormat(CollationBuffers->Custom10, sizeof(CollationBuffers->Custom10), HMML.metadata.custom[CustomIndex]); break;
case 11: CopyStringNoFormat(CollationBuffers->Custom11, sizeof(CollationBuffers->Custom11), HMML.metadata.custom[CustomIndex]); break;
case 12: CopyStringNoFormat(CollationBuffers->Custom12, sizeof(CollationBuffers->Custom12), HMML.metadata.custom[CustomIndex]); break;
case 13: CopyStringNoFormat(CollationBuffers->Custom13, sizeof(CollationBuffers->Custom13), HMML.metadata.custom[CustomIndex]); break;
case 14: CopyStringNoFormat(CollationBuffers->Custom14, sizeof(CollationBuffers->Custom14), HMML.metadata.custom[CustomIndex]); break;
case 15: CopyStringNoFormat(CollationBuffers->Custom15, sizeof(CollationBuffers->Custom15), HMML.metadata.custom[CustomIndex]); break;
}
}
}
}
if(!HaveErrors)
{
if(HMML.metadata.template)
{
switch(ValidateTemplate(BespokeTemplate, HMML.metadata.template, TEMPLATE_BESPOKE))
{
case RC_ARENA_FULL:
case RC_INVALID_TEMPLATE: // Invalid template
case RC_ERROR_FILE: // Could not load template
case RC_ERROR_MEMORY: // Could not allocate memory for template
HaveErrors = TRUE;
case RC_SUCCESS:
break;
}
}
}
if(HaveErrors)
{
fprintf(stderr, "\e[1;31mSkipping\e[0m %s", BaseFilename);
if(HMML.metadata.title) { fprintf(stderr, " - %s", HMML.metadata.title); }
fprintf(stderr, "\n");
hmml_free(&HMML);
return RC_ERROR_HMML;
}
if(N)
{
N->This.LinkOffsets.PrevStart = 0;
N->This.LinkOffsets.PrevEnd = 0;
N->This.LinkOffsets.NextStart = 0;
N->This.LinkOffsets.NextEnd = 0;
}
#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
buffer QuoteMenu;
buffer ReferenceMenu;
buffer FilterMenu;
buffer FilterTopics;
buffer FilterMedia;
buffer CreditsMenu;
buffer Annotation;
buffer AnnotationHeader;
buffer AnnotationClass;
buffer AnnotationData;
buffer Text;
buffer CategoryIcons;
if(ClaimBuffer(&QuoteMenu, "QuoteMenu", Kilobytes(32)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
if(ClaimBuffer(&ReferenceMenu, "ReferenceMenu", Kilobytes(32)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
if(ClaimBuffer(&FilterMenu, "FilterMenu", Kilobytes(16)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
if(ClaimBuffer(&FilterTopics, "FilterTopics", Kilobytes(8)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
if(ClaimBuffer(&FilterMedia, "FilterMedia", Kilobytes(8)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
if(ClaimBuffer(&CreditsMenu, "CreditsMenu", Kilobytes(8)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
ref_info ReferencesArray[200] = { };
categories Topics = { };
categories Media = { };
bool HasQuoteMenu = FALSE;
bool HasReferenceMenu = FALSE;
bool HasFilterMenu = FALSE;
bool HasCreditsMenu = FALSE;
int QuoteIdentifier = 0x3b1;
int RefIdentifier = 1;
int UniqueRefs = 0;
CopyStringToBuffer(&CollationBuffers->Menus,
"<div class=\"cineraMenus %s\">\n"
" <span class=\"episode_name\">", StringsDiffer(Config.Theme, "") ? Config.Theme : HMML.metadata.project);
CopyStringToBufferHTMLSafe(&CollationBuffers->Menus, HMML.metadata.title);
CopyStringToBuffer(&CollationBuffers->Menus, "</span>\n");
CopyStringToBuffer(&CollationBuffers->Player,
"<div class=\"cineraPlayerContainer\">\n"
" <div class=\"video_container\" data-videoId=\"%s\"></div>\n"
" <div class=\"markers_container %s\">\n", HMML.metadata.id, StringsDiffer(Config.Theme, "") ? Config.Theme : HMML.metadata.project);
if(N)
{
N->This.LinkOffsets.PrevStart = (CollationBuffers->Player.Ptr - CollationBuffers->Player.Location);
if(N->Prev.Size || N->Next.Size)
{
if(N->Prev.Size)
{
// TODO(matt): Once we have a more rigorous notion of "Day Numbers", perhaps also use them here
buffer PreviousPlayerURL;
ClaimBuffer(&PreviousPlayerURL, "PreviousPlayerURL", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH);
ConstructPlayerURL(&PreviousPlayerURL, N->Prev.BaseFilename);
CopyStringToBuffer(&CollationBuffers->Player,
" <a class=\"episodeMarker prev\" href=\"%s\"><div>&#9195;</div><div>Previous: '%s'</div><div>&#9195;</div></a>\n",
PreviousPlayerURL.Location,
N->Prev.Title);
DeclaimBuffer(&PreviousPlayerURL);
}
else
{
CopyStringToBuffer(&CollationBuffers->Player,
" <div class=\"episodeMarker first\"><div>&#8226;</div><div>Welcome to <cite>%s</cite></div><div>&#8226;</div></div>\n", CollationBuffers->ProjectName);
}
}
N->This.LinkOffsets.PrevEnd = (CollationBuffers->Player.Ptr - CollationBuffers->Player.Location - N->This.LinkOffsets.PrevStart);
}
CopyStringToBuffer(&CollationBuffers->Player,
" <div class=\"markers\">\n");
speakers Speakers = { };
switch(BuildCredits(&CreditsMenu, &HasCreditsMenu, &HMML.metadata, &Speakers))
{
case CreditsError_NoHost:
case CreditsError_NoAnnotator:
case CreditsError_NoCredentials:
fprintf(stderr, "\e[1;31mSkipping\e[0m %s - %s\n", BaseFilename, HMML.metadata.title);
HMMLCleanup();
return RC_ERROR_HMML;
}
bool PrivateVideo = FALSE;
if(Config.Edition != EDITION_SINGLE && N->This.Size == 0 && !(Config.Mode & MODE_NOPRIVACY))
{
if(VideoIsPrivate(HMML.metadata.id))
{
// TODO(matt): Actually generate these guys, just putting them in a secret location
N->This.LinkOffsets.PrevStart = 0;
N->This.LinkOffsets.PrevEnd = 0;
PrivateVideo = TRUE;
HMMLCleanup();
return RC_PRIVATE_VIDEO;
}
}
if(Config.Edition != EDITION_SINGLE && !PrivateVideo)
{
CopyStringToBuffer(&CollationBuffers->Search, "name: \"");
if(StringsDiffer(Config.PlayerURLPrefix, ""))
{
char *Ptr = BaseFilename + StringLength(Config.ProjectID);
CopyStringToBuffer(&CollationBuffers->Search, "%s%s", Config.PlayerURLPrefix, Ptr);
}
else
{
CopyStringToBuffer(&CollationBuffers->Search, "%s", BaseFilename);
}
CopyStringToBuffer(&CollationBuffers->Search, "\"\n"
"title: \"");
CopyStringToBufferNoFormat(&CollationBuffers->Search, HMML.metadata.title);
CopyStringToBuffer(&CollationBuffers->Search, "\"\n"
"markers:\n");
}
#if DEBUG
printf("\n\n --- Entering Annotations Loop ---\n\n\n\n");
#endif
int PreviousTimecode = 0;
for(int AnnotationIndex = 0; AnnotationIndex < HMML.annotation_count; ++AnnotationIndex)
{
#if DEBUG
printf("%d\n", AnnotationIndex);
#endif
HMML_Annotation *Anno = HMML.annotations + AnnotationIndex;
if(TimecodeToSeconds(Anno->time) < PreviousTimecode)
{
fprintf(stderr, "%s:%d: Timecode %s is chronologically before previous timecode\n"
"\e[1;31mSkipping\e[0m %s - %s\n",
Filename, Anno->line, Anno->time,
BaseFilename, HMML.metadata.title);
HMMLCleanup();
return RC_ERROR_HMML;
}
PreviousTimecode = TimecodeToSeconds(Anno->time);
categories LocalTopics = { };
categories LocalMedia = { };
bool HasQuote = FALSE;
bool HasReference = FALSE;
quote_info QuoteInfo = { };
// NOTE(matt): Tree structure of "annotation local" buffer dependencies
// Annotation
// AnnotationHeader
// AnnotationClass
// AnnotationData
// Text
// CategoryIcons
// TODO(matt): Shouldn't all of these guys DeclaimBuffer() before returning?
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(&CategoryIcons, "CategoryIcons", Kilobytes(1)) == 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 || Speakers.Count > 1) && !IsCategorisedAFK(*Anno))
{
int SpeakerIndex;
if(Anno->author && (SpeakerIndex = LinearSearchForSpeaker(Speakers, Anno->author)) == -1)
{
if(!HasFilterMenu)
{
HasFilterMenu = TRUE;
}
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, "authored");
hsl_colour AuthorColour;
StringToColourHash(&AuthorColour, Anno->author);
// TODO(matt): That EDITION_NETWORK site database API-polling stuff
CopyStringToBuffer(&Text,
"<span class=\"author\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</span> ",
AuthorColour.Hue, AuthorColour.Saturation,
Anno->author);
}
else
{
if(!Anno->author)
{
SpeakerIndex = LinearSearchForSpeaker(Speakers, HMML.metadata.member);
}
CopyStringToBuffer(&Text,
"<span class=\"author\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</span>: ",
Speakers.Speaker[SpeakerIndex].Colour.Hue,
Speakers.Speaker[SpeakerIndex].Colour.Saturation,
Speakers.Speaker[SpeakerIndex].Seen == FALSE ? Speakers.Speaker[SpeakerIndex].Credential->CreditedName : Speakers.Speaker[SpeakerIndex].Abbreviation);
Speakers.Speaker[SpeakerIndex].Seen = TRUE;
}
}
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;
switch(Anno->markers[MarkerIndex].type)
{
case HMML_MEMBER:
case HMML_PROJECT:
{
// TODO(matt): That EDITION_NETWORK site database API-polling stuff
hsl_colour Colour;
StringToColourHash(&Colour, Anno->markers[MarkerIndex].marker);
CopyStringToBuffer(&Text,
"<span class=\"%s\" data-hue=\"%d\" data-saturation=\"%d%%\">%.*s</span>",
Anno->markers[MarkerIndex].type == HMML_MEMBER ? "member" : "project",
Colour.Hue, Colour.Saturation,
StringLength(Readable), InPtr);
} break;
case HMML_CATEGORY:
{
switch(GenerateTopicColours(Anno->markers[MarkerIndex].marker))
{
case RC_SUCCESS:
case RC_NOOP:
break;
case RC_ERROR_FILE:
case RC_ERROR_MEMORY:
hmml_free(&HMML);
return RC_ERROR_FATAL;
};
if(!HasFilterMenu)
{
HasFilterMenu = TRUE;
}
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, Anno->markers[MarkerIndex].marker);
CopyStringToBuffer(&Text, "%.*s", StringLength(Readable), InPtr);
} break;
case HMML_MARKER_COUNT: break;
}
InPtr += StringLength(Readable);
++MarkerIndex;
}
while(RefIndex < Anno->reference_count &&
InPtr - Anno->text == Anno->references[RefIndex].offset)
{
HMML_Reference *CurrentRef = Anno->references + RefIndex;
if(!HasReferenceMenu)
{
CopyStringToBuffer(&ReferenceMenu,
" <div class=\"menu references\">\n"
" <span>References &#9660;</span>\n"
" <div class=\"refs references_container\">\n");
if(BuildReference(ReferencesArray, RefIdentifier, UniqueRefs, CurrentRef, Anno) == RC_INVALID_REFERENCE)
{
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, sizeof(ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Timecode), "%s", 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, sizeof(ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Timecode), "%s", 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) == RC_INVALID_REFERENCE)
{
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;
}
}
CopyStringToBuffer(&Text, "<sup>%s%d</sup>",
Anno->references[RefIndex].offset == Anno->references[RefIndex-1].offset ? "," : "",
RefIdentifier);
++RefIndex;
++RefIdentifier;
}
if(*InPtr)
{
switch(*InPtr)
{
case '<':
CopyStringToBuffer(&Text, "&lt;");
break;
case '>':
CopyStringToBuffer(&Text, "&gt;");
break;
case '&':
CopyStringToBuffer(&Text, "&amp;");
break;
case '\"':
CopyStringToBuffer(&Text, "&quot;");
break;
case '\'':
CopyStringToBuffer(&Text, "&#39;");
break;
default:
*Text.Ptr++ = *InPtr;
*Text.Ptr = '\0';
break;
}
++InPtr;
}
}
if(Anno->is_quote)
{
if(!HasQuoteMenu)
{
CopyStringToBuffer(&QuoteMenu,
" <div class=\"menu quotes\">\n"
" <span>Quotes &#9660;</span>\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;
bool ShouldFetchQuotes = FALSE;
if(Config.Mode & MODE_NOCACHE || (Config.Edition != EDITION_SINGLE && time(0) - LastQuoteFetch > 60*60))
{
ShouldFetchQuotes = TRUE;
LastQuoteFetch = time(0);
}
if(BuildQuote(&QuoteInfo,
Speaker,
Anno->quote.id, ShouldFetchQuotes) == RC_UNFOUND)
{
LogError(LOG_ERROR, "Quote #%s %d not found: %s:%d", Speaker, Anno->quote.id, Filename, Anno->line);
Filename[StringLength(Filename) - StringLength(".hmml")] = '\0';
fprintf(stderr, "Quote #%s %d not found\n"
"\e[1;31mSkipping\e[0m %s - %s\n",
Speaker,
Anno->quote.id,
BaseFilename, HMML.metadata.title);
hmml_free(&HMML);
return RC_ERROR_QUOTE;
}
CopyStringToBuffer(&QuoteMenu,
" <a target=\"_blank\" class=\"ref\" href=\"https://dev.abaines.me.uk/quotes/%s/%d\">\n"
" <span data-id=\"&#%d;\">\n"
" <span class=\"ref_content\">\n"
" <div class=\"source\">Quote %d</div>\n"
" <div class=\"ref_title\">",
Speaker,
Anno->quote.id,
QuoteIdentifier,
Anno->quote.id);
CopyStringToBufferHTMLSafe(&QuoteMenu, QuoteInfo.Text);
CopyStringToBuffer(&QuoteMenu, "</div>\n"
" <div class=\"quote_byline\">&mdash;%s, %s</div>\n"
" </span>\n"
" <div class=\"ref_indices\">\n"
" <span data-timestamp=\"%d\" class=\"timecode\"><span class=\"ref_index\">[&#%d;]</span><span class=\"time\">%s</span></span>\n"
" </div>\n"
" </span>\n"
" </a>\n",
Speaker,
QuoteInfo.Date,
TimecodeToSeconds(Anno->time),
QuoteIdentifier,
Anno->time);
if(!Anno->text[0])
{
CopyStringToBuffer(&Text, "&#8220;");
CopyStringToBufferHTMLSafe(&Text, QuoteInfo.Text);
CopyStringToBuffer(&Text, "&#8221;");
}
CopyStringToBuffer(&Text, "<sup>&#%d;</sup>", QuoteIdentifier);
++QuoteIdentifier;
}
if(Config.Edition != EDITION_SINGLE && !PrivateVideo)
{
CopyStringToBuffer(&CollationBuffers->Search, "\"%d\": \"", TimecodeToSeconds(Anno->time));
if(Anno->is_quote && !Anno->text[0])
{
CopyStringToBuffer(&CollationBuffers->Search, "\u201C");
CopyStringToBufferNoFormat(&CollationBuffers->Search, QuoteInfo.Text);
CopyStringToBuffer(&CollationBuffers->Search, "\u201D");
}
else
{
CopyStringToBufferNoFormat(&CollationBuffers->Search, Anno->text);
}
CopyStringToBuffer(&CollationBuffers->Search, "\"\n");
}
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:
hmml_free(&HMML);
return RC_ERROR_FATAL;
}
if(!HasFilterMenu)
{
HasFilterMenu = TRUE;
}
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, Anno->markers[MarkerIndex].marker);
++MarkerIndex;
}
if(LocalTopics.Count == 0)
{
switch(GenerateTopicColours("nullTopic"))
{
case RC_SUCCESS:
case RC_NOOP:
break;
case RC_ERROR_FILE:
case RC_ERROR_MEMORY:
hmml_free(&HMML);
return RC_ERROR_FATAL;
};
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, "nullTopic");
}
if(LocalMedia.Count == 0)
{
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, DefaultMedium);
}
BuildCategories(&AnnotationClass, &CategoryIcons, &LocalTopics, &LocalMedia, &MarkerIndex, DefaultMedium);
CopyBuffer(&AnnotationHeader, &AnnotationClass);
if(HasQuote || HasReference)
{
CopyStringToBuffer(&AnnotationData, "\"");
CopyBuffer(&AnnotationHeader, &AnnotationData);
}
CopyStringToBuffer(&AnnotationHeader, ">\n");
CopyBuffer(&Annotation, &AnnotationHeader);
CopyStringToBuffer(&Annotation,
" <div class=\"cineraContent\"><span class=\"timecode\">%s</span>",
Anno->time);
CopyBuffer(&Annotation, &Text);
if(LocalTopics.Count > 0)
{
CopyBuffer(&Annotation, &CategoryIcons);
}
CopyStringToBuffer(&Annotation, "</div>\n"
" <div class=\"progress faded\">\n"
" <div class=\"cineraContent\"><span class=\"timecode\">%s</span>",
Anno->time);
CopyBuffer(&Annotation, &Text);
if(LocalTopics.Count > 0)
{
CopyBuffer(&Annotation, &CategoryIcons);
}
CopyStringToBuffer(&Annotation, "</div>\n"
" </div>\n"
" <div class=\"progress main\">\n"
" <div class=\"cineraContent\"><span class=\"timecode\">%s</span>",
Anno->time);
CopyBuffer(&Annotation, &Text);
if(LocalTopics.Count > 0)
{
CopyBuffer(&Annotation, &CategoryIcons);
}
CopyStringToBuffer(&Annotation, "</div>\n"
" </div>\n"
" </div>\n");
CopyBuffer(&CollationBuffers->Player, &Annotation);
// NOTE(matt): Tree structure of "annotation local" buffer dependencies
// CategoryIcons
// Text
// AnnotationData
// AnnotationClass
// AnnotationHeader
// Annotation
DeclaimBuffer(&CategoryIcons);
DeclaimBuffer(&Text);
DeclaimBuffer(&AnnotationData);
DeclaimBuffer(&AnnotationClass);
DeclaimBuffer(&AnnotationHeader);
DeclaimBuffer(&Annotation);
}
if(Config.Edition != EDITION_SINGLE && !PrivateVideo)
{
CopyStringToBuffer(&CollationBuffers->Search, "---\n");
N->This.Size = CollationBuffers->Search.Ptr - CollationBuffers->Search.Location;
}
#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\">");
CopyStringToBufferHTMLSafeBreakingOnSlash(&ReferenceMenu, ReferencesArray[i].Source);
CopyStringToBuffer(&ReferenceMenu, "</div>\n"
" <div class=\"ref_title\">");
CopyStringToBufferHTMLSafeBreakingOnSlash(&ReferenceMenu, ReferencesArray[i].RefTitle);
CopyStringToBuffer(&ReferenceMenu, "</div>\n");
}
else
{
CopyStringToBuffer(&ReferenceMenu,
" <div class=\"ref_title\">");
CopyStringToBufferHTMLSafeBreakingOnSlash(&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)
{
buffer URLPrefix;
ClaimBuffer(&URLPrefix, "URLPrefix", 1024);
ConstructURLPrefix(&URLPrefix, INCLUDE_Images, PAGE_PLAYER);
CopyStringToBuffer(&FilterMenu,
" <div class=\"menu filter\">\n"
" <span><img src=\"%scinera_icon_filter.png\"></span>\n"
" <div class=\"filter_container\">\n"
" <div class=\"filter_mode exclusive\">Filter mode: </div>\n"
" <div class=\"filters\">\n",
URLPrefix.Location);
DeclaimBuffer(&URLPrefix);
if(Topics.Count > 0)
{
CopyStringToBuffer(&FilterMenu,
" <div class=\"filter_topics\">\n"
" <div class=\"filter_title\">Topics</div>\n");
for(int i = 0; i < Topics.Count; ++i)
{
char SanitisedMarker[StringLength(Topics.Category[i].Marker) + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", Topics.Category[i].Marker);
SanitisePunctuation(SanitisedMarker);
bool NullTopic = !StringsDiffer(Topics.Category[i].Marker, "nullTopic");
CopyStringToBuffer(&FilterTopics,
" <div %sclass=\"filter_content %s\">\n"
" <span class=\"icon category %s\"></span><span class=\"cineraText\">%s</span>\n"
" </div>\n",
NullTopic ? "title=\"Annotations that don't fit into the above topic(s) may be filtered using this pseudo-topic\" " : "",
SanitisedMarker,
SanitisedMarker,
NullTopic ? "(null topic)" : 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)
{
char SanitisedMarker[StringLength(Media.Category[i].Marker) + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", Media.Category[i].Marker);
SanitisePunctuation(SanitisedMarker);
int j;
for(j = 0; j < ArrayCount(CategoryMedium); ++j)
{
if(!StringsDiffer(Media.Category[i].Marker, CategoryMedium[j].Medium))
{
break;
}
}
if(!StringsDiffer(Media.Category[i].Marker, "afk")) // TODO(matt): Initially hidden config
// When we do this for real, we'll probably need to loop
// over the configured media to see who should be hidden
{
CopyStringToBuffer(&FilterMedia,
" <div class=\"filter_content %s off\">\n"
" <span class=\"icon\">%s</span><span class=\"cineraText\">%s</span>\n"
" </div>\n",
SanitisedMarker,
CategoryMedium[j].Icon,
CategoryMedium[j].WrittenName);
}
else
{
CopyStringToBuffer(&FilterMedia,
" <div class=\"filter_content %s\">\n"
" <span class=\"icon\">%s</span><span class=\"cineraText\">%s%s</span>\n"
" </div>\n",
SanitisedMarker,
CategoryMedium[j].Icon,
CategoryMedium[j].WrittenName,
!StringsDiffer(Media.Category[i].Marker, DefaultMedium) ? "</span><span class=\"cineraDefaultMediumIndicator\" title=\"Default medium\n"
"Annotations lacking a media icon are in this medium\">&#128969;" : "");
}
}
CopyStringToBuffer(&FilterMedia,
" </div>\n");
CopyBuffer(&FilterMenu, &FilterMedia);
}
CopyStringToBuffer(&FilterMenu,
" </div>\n"
" </div>\n"
" </div>\n");
CopyBuffer(&CollationBuffers->Menus, &FilterMenu);
}
CopyStringToBuffer(&CollationBuffers->Menus,
" <div class=\"menu views\">\n"
" <div class=\"view\" data-id=\"theatre\" title=\"Theatre mode\">&#127917;</div>\n"
" <div class=\"views_container\">\n"
" <div class=\"view\" data-id=\"super\" title=\"SUPERtheatre mode\">&#127967;</div>\n"
" </div>\n"
" </div>\n"
" <div class=\"menu link\">\n"
" <span>&#128279;</span>\n"
" <div class=\"link_container\">\n"
" <div id=\"cineraLinkMode\">Link to current annotation</div>\n"
" <textarea title=\"Click to copy to clipboard\" id=\"cineraLink\" readonly spellcheck=\"false\"></textarea>\n"
" </div>\n"
" </div>\n");
if(HasCreditsMenu)
{
CopyBuffer(&CollationBuffers->Menus, &CreditsMenu);
}
// TODO(matt): Maybe figure out a more succinct way to code this Help text
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\">[</span>, <span class=\"help_key\">&lt;</span> / <span class=\"help_key\">]</span>, <span class=\"help_key\">&gt;</span> <span class=\"help_text\">Jump to previous / next episode</span><br>\n"
" <span class=\"help_key\">W</span>, <span class=\"help_key\">K</span>, <span class=\"help_key\">P</span> / <span class=\"help_key\">S</span>, <span class=\"help_key\">J</span>, <span class=\"help_key\">N</span> <span class=\"help_text\">Jump to previous / next marker</span><br>\n"
" <span class=\"help_key\">t</span> / <span class=\"help_key\">T</span> <span class=\"help_text\">Toggle theatre / SUPERtheatre mode</span><br>\n"
);
if(HasFilterMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <span class=\"help_key\">V</span> <span class=\"help_text\">Revert filter to original state</span> <span class=\"help_key\">Y</span> <span class=\"help_text\">Select link (requires manual Ctrl-c)</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");
}
CopyStringToBuffer(&CollationBuffers->Menus,
" <span class=\"help_key\">y</span> <span class=\"help_text\">Link</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>In-Menu Movement</h2>\n"
" <div class=\"help_paragraph\">\n"
" <div class=\"key_block\">\n"
" <div>\n"
" <span class=\"help_key\">a</span>\n"
" </div>\n"
" <div>\n"
" <span class=\"help_key\">w</span><br>\n"
" <span class=\"help_key\">s</span>\n"
" </div>\n"
" <div>\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>\n"
" <span class=\"help_key\">←</span>\n"
" </div>\n"
" <div>\n"
" <span class=\"help_key\">↑</span><br>\n"
" <span class=\"help_key\">↓</span>\n"
" </div>\n"
" <div>\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 class=\"help_key word\">Enter</span> <span class=\"help_text\">Jump to timecode</span><br>\n");
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <span class=\"help_key word 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, "and</span> 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 class=\"help_key word\">Space</span> <span class=\"help_text\">Toggle category and focus next</span><br>\n"
" <span class=\"help_key\">X</span>, <span class=\"help_key word modifier\">Shift</span><span class=\"help_key word\">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"
"\n"
" <h2>Filter and Link Menus</h2>\n"
" <span class=\"help_key\">z</span> <span class=\"help_text\">Toggle filter / linking mode</span>\n");
}
else
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <h2><span class=\"unavailable\">Filter Menu</span></h2>\n"
" <span class=\"help_key unavailable\">x</span>, <span class=\"help_key word unavailable\">Space</span> <span class=\"help_text unavailable\">Toggle category and focus next</span><br>\n"
" <span class=\"help_key unavailable\">X</span>, <span class=\"help_key word modifier unavailable\">Shift</span><span class=\"help_key word 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"
"\n"
" <h2><span class=\"unavailable\">Filter</span> and Link Menus</h2>\n"
" <span class=\"help_key\">z</span> <span class=\"help_text\">Toggle <span class=\"unavailable\">filter /</span> linking mode</span>\n");
}
CopyStringToBuffer(&CollationBuffers->Menus, "\n");
if(HasCreditsMenu)
{
CopyStringToBuffer(&CollationBuffers->Menus,
" <h2>Credits Menu</h2>\n"
" <span class=\"help_key word\">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 class=\"help_key word 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");
if(N)
{
N->This.LinkOffsets.NextStart = (CollationBuffers->Player.Ptr - CollationBuffers->Player.Location - (N->This.LinkOffsets.PrevStart + N->This.LinkOffsets.PrevEnd));
if(N->Prev.Size || N->Next.Size)
{
if(N->Next.Size > 0)
{
// TODO(matt): Once we have a more rigorous notion of "Day Numbers", perhaps also use them here
buffer NextPlayerURL;
ClaimBuffer(&NextPlayerURL, "NextPlayerURL", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH);
ConstructPlayerURL(&NextPlayerURL, N->Next.BaseFilename);
CopyStringToBuffer(&CollationBuffers->Player,
" <a class=\"episodeMarker next\" href=\"%s\"><div>&#9196;</div><div>Next: '%s'</div><div>&#9196;</div></a>\n",
NextPlayerURL.Location,
N->Next.Title);
DeclaimBuffer(&NextPlayerURL);
}
else
{
CopyStringToBuffer(&CollationBuffers->Player,
" <div class=\"episodeMarker last\"><div>&#8226;</div><div>You have arrived at the (current) end of <cite>%s</cite></div><div>&#8226;</div></div>\n", CollationBuffers->ProjectName);
}
}
N->This.LinkOffsets.NextEnd = (CollationBuffers->Player.Ptr - CollationBuffers->Player.Location - (N->This.LinkOffsets.PrevStart + N->This.LinkOffsets.PrevEnd + N->This.LinkOffsets.NextStart));
}
CopyStringToBuffer(&CollationBuffers->Player,
" </div>\n"
" </div>");
// TODO(matt): Maybe do something about indentation levels
buffer URLIndex;
ClaimBuffer(&URLIndex, "URLIndex", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1);
ConstructIndexURL(&URLIndex);
CopyString(CollationBuffers->URLIndex, sizeof(CollationBuffers->URLIndex), "%s", URLIndex.Location);
DeclaimBuffer(&URLIndex);
buffer URLPrefix;
ClaimBuffer(&URLPrefix, "URLPrefix", 1024);
ConstructURLPrefix(&URLPrefix, INCLUDE_CSS, PAGE_INDEX);
CopyStringToBuffer(&CollationBuffers->IncludesIndex,
"<meta charset=\"UTF-8\">\n"
" <meta name=\"generator\" content=\"Cinera %d.%d.%d\">\n"
"\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera.css\">\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera__%s.css\">\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera_topics.css\">\n",
CINERA_APP_VERSION.Major,
CINERA_APP_VERSION.Minor,
CINERA_APP_VERSION.Patch,
URLPrefix.Location,
URLPrefix.Location, StringsDiffer(Config.Theme, "") ? Config.Theme : HMML.metadata.project,
URLPrefix.Location);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"<meta charset=\"UTF-8\">\n"
" <meta name=\"generator\" content=\"Cinera %d.%d.%d\">\n",
CINERA_APP_VERSION.Major,
CINERA_APP_VERSION.Minor,
CINERA_APP_VERSION.Patch);
if(Topics.Count || Media.Count)
{
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
" <meta name=\"keywords\" content=\"");
if(Topics.Count > 0)
{
for(int i = 0; i < Topics.Count; ++i)
{
if(StringsDiffer(Topics.Category[i].Marker, "nullTopic"))
{
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");
}
ConstructURLPrefix(&URLPrefix, INCLUDE_CSS, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera.css\">\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera__%s.css\">\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera_topics.css\">\n",
URLPrefix.Location,
URLPrefix.Location, StringsDiffer(Config.Theme, "") ? Config.Theme : HMML.metadata.project,
URLPrefix.Location);
ConstructURLPrefix(&URLPrefix, INCLUDE_JS, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
" <script type=\"text/javascript\" src=\"%scinera_player_pre.js\"></script>",
URLPrefix.Location);
CopyStringToBuffer(&CollationBuffers->ScriptPlayer,
"<script type=\"text/javascript\" src=\"%scinera_player_post.js\"></script>",
URLPrefix.Location);
DeclaimBuffer(&URLPrefix);
// NOTE(matt): Tree structure of "global" buffer dependencies
// CreditsMenu
// FilterMedia
// FilterTopics
// FilterMenu
// ReferenceMenu
// QuoteMenu
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, "\e[1;31mSkipping\e[0m %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, unsigned int *PlayerOffset)
{
#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(StringsDiffer(Template->Metadata.Filename, ""))
{
if((StringsDiffer(Template->Metadata.Filename, ""))
&& ((Template->Metadata.Validity & PageType) || Config.Mode & MODE_FORCEINTEGRATION))
{
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;
bool NeedPlayerOffset = PlayerOffset ? TRUE : FALSE;
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;
}
// TODO(matt): Make this whole template stuff context-aware, so it can determine whether or not to HTML-encode
// or sanitise punctuation for CSS-safety
switch(Template->Metadata.Tag[i].TagCode)
{
case TAG_PROJECT_ID:
if(CollationBuffers->ProjectID[0] == '\0')
{
fprintf(stderr, "Template contains a <!-- __CINERA_PROJECT_ID__ --> tag\n"
"Skipping just this tag, because no project ID is set\n");
}
else
{
CopyStringToBufferNoFormat(&Output, CollationBuffers->ProjectID); // NOTE(matt): Not HTML-safe
}
break;
case TAG_PROJECT:
if(CollationBuffers->ProjectName[0] == '\0')
{
fprintf(stderr, "Template contains a <!-- __CINERA_PROJECT__ --> tag\n"
"Skipping just this tag, because we do not know the project's full name\n");
}
else
{
CopyStringToBufferHTMLSafe(&Output, CollationBuffers->ProjectName);
}
break;
case TAG_THEME:
CopyStringToBufferNoFormat(&Output, CollationBuffers->Theme); // NOTE(matt): Not HTML-safe
break;
case TAG_TITLE:
CopyStringToBufferHTMLSafe(&Output, CollationBuffers->Title);
break;
case TAG_URL:
if(PageType == PAGE_PLAYER)
{
CopyStringToBufferNoFormat(&Output, CollationBuffers->URLPlayer);
}
else
{
CopyStringToBufferNoFormat(&Output, CollationBuffers->URLIndex);
}
break;
case TAG_VIDEO_ID:
CopyStringToBufferNoFormat(&Output, CollationBuffers->VideoID);
break;
case TAG_INDEX:
if(Config.Edition == EDITION_SINGLE)
{
fprintf(stderr, "Template contains a <!-- __CINERA_INDEX__ --> tag\n"
"Skipping just this tag, because an index cannot be generated for inclusion in a\n"
"bespoke template in Single Edition\n");
}
else
{
CopyBuffer(&Output, &CollationBuffers->Index);
}
break;
case TAG_INCLUDES:
if(PageType == PAGE_PLAYER)
{
CopyBuffer(&Output, &CollationBuffers->IncludesPlayer);
}
else
{
CopyBuffer(&Output, &CollationBuffers->IncludesIndex);
}
break;
case TAG_MENUS:
CopyBuffer(&Output, &CollationBuffers->Menus);
break;
case TAG_PLAYER:
if(NeedPlayerOffset) { *PlayerOffset += (Output.Ptr - Output.Location); NeedPlayerOffset = !NeedPlayerOffset; }
CopyBuffer(&Output, &CollationBuffers->Player);
break;
case TAG_SCRIPT:
CopyBuffer(&Output, &CollationBuffers->ScriptPlayer);
break;
case TAG_CUSTOM0: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom0); break;
case TAG_CUSTOM1: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom1); break;
case TAG_CUSTOM2: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom2); break;
case TAG_CUSTOM3: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom3); break;
case TAG_CUSTOM4: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom4); break;
case TAG_CUSTOM5: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom5); break;
case TAG_CUSTOM6: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom6); break;
case TAG_CUSTOM7: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom7); break;
case TAG_CUSTOM8: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom8); break;
case TAG_CUSTOM9: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom9); break;
case TAG_CUSTOM10: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom10); break;
case TAG_CUSTOM11: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom11); break;
case TAG_CUSTOM12: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom12); break;
case TAG_CUSTOM13: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom13); break;
case TAG_CUSTOM14: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom14); break;
case TAG_CUSTOM15: CopyStringToBufferNoFormat(&Output, CollationBuffers->Custom15); 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)
{
CopyStringToBuffer(&Master, "<div>\n"
" ");
CopyBuffer(&Master, &CollationBuffers->Menus);
CopyStringToBuffer(&Master, "\n"
" ");
if(PlayerOffset) { *PlayerOffset += Master.Ptr - Master.Location; }
CopyBuffer(&Master, &CollationBuffers->Player);
CopyStringToBuffer(&Master, "\n"
" ");
CopyStringToBuffer(&Master, "</div>\n"
" ");
CopyBuffer(&Master, &CollationBuffers->ScriptPlayer);
CopyStringToBuffer(&Master, "\n");
}
else
{
CopyBuffer(&Master, &CollationBuffers->Index);
}
CopyStringToBuffer(&Master,
" </body>\n"
"</html>\n");
FILE *OutFile;
if(!(OutFile = fopen(Config.Edition == EDITION_PROJECT ? OutputPath : Config.OutLocation, "w")))
{
LogError(LOG_ERROR, "Unable to open output file %s: %s", Config.Edition == EDITION_PROJECT ? OutputPath : Config.OutLocation, strerror(errno));
DeclaimBuffer(&Master);
return RC_ERROR_FILE;
}
fwrite(Master.Location, Master.Ptr - Master.Location, 1, OutFile);
fclose(OutFile);
DeclaimBuffer(&Master);
return RC_SUCCESS;
}
}
int
BinarySearchForMetadataEntry(index *Index, index_metadata **Entry, char *SearchTerm)
{
int Lower = 0;
index_metadata *LowerEntry = (index_metadata*)(Index->Metadata.Buffer.Ptr + sizeof(index_metadata) * Lower);
if(StringsDiffer(SearchTerm, LowerEntry->BaseFilename) < 0 ) { return -1; }
int Upper = Index->Header.EntryCount - 1;
int Pivot = Upper - ((Upper - Lower) >> 1);
index_metadata *UpperEntry;
index_metadata *PivotEntry;
do {
LowerEntry = (index_metadata*)(Index->Metadata.Buffer.Ptr + sizeof(index_metadata) * Lower);
PivotEntry = (index_metadata*)(Index->Metadata.Buffer.Ptr + sizeof(index_metadata) * Pivot);
UpperEntry = (index_metadata*)(Index->Metadata.Buffer.Ptr + sizeof(index_metadata) * Upper);
if(!StringsDiffer(SearchTerm, LowerEntry->BaseFilename)) { *Entry = LowerEntry; return Lower; }
if(!StringsDiffer(SearchTerm, PivotEntry->BaseFilename)) { *Entry = PivotEntry; return Pivot; }
if(!StringsDiffer(SearchTerm, UpperEntry->BaseFilename)) { *Entry = UpperEntry; return Upper; }
if((StringsDiffer(SearchTerm, PivotEntry->BaseFilename) < 0)) { Upper = Pivot; }
else { Lower = Pivot; }
Pivot = Upper - ((Upper - Lower) >> 1);
} while(Upper > Pivot);
return Upper;
}
int
AccumulateIndexEntryInsertionOffset(index *Index, int EntryIndex)
{
int Result = 0;
index_metadata *AccEntry = { 0 };
if(EntryIndex < Index->Header.EntryCount >> 1)
{
//printf("\u200B"); // NOTE(matt): Don't ask...
for(; EntryIndex > 0; --EntryIndex)
{
AccEntry = (index_metadata*)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * (EntryIndex - 1));
Result += AccEntry->Size;
}
Result += StringLength("---\n");
}
else
{
for(; EntryIndex < Index->Header.EntryCount; ++EntryIndex)
{
AccEntry = (index_metadata*)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * EntryIndex);
Result += AccEntry->Size;
}
Result = Index->File.FileSize - Result;
}
return Result;
}
void
ClearEntry(index_metadata *Entry)
{
Entry->LinkOffsets.PrevStart = 0;
Entry->LinkOffsets.NextStart = 0;
Entry->LinkOffsets.PrevEnd = 0;
Entry->LinkOffsets.NextEnd = 0;
Entry->Size = 0;
Clear(Entry->BaseFilename, sizeof(Entry->BaseFilename));
Clear(Entry->Title, sizeof(Entry->Title));
}
enum
{
EDIT_DELETION,
EDIT_ADDITION,
EDIT_REINSERTION
} index_edits;
void
GetNeighbourhood(index *Index, neighbourhood *N, int IndexEditType, bool *ThisIsPrev)
{
index_metadata Entry = { };
int EntryIndex;
int IncomingIndex = N->ThisIndex;
if(IndexEditType == EDIT_DELETION)
{
bool FoundThis = FALSE;
for(EntryIndex = IncomingIndex + 1; EntryIndex < Index->Header.EntryCount; ++EntryIndex)
{
Entry = *(index_metadata *)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * EntryIndex);
if(Entry.Size > 0)
{
FoundThis = TRUE;
break;
}
}
if(!FoundThis)
{
for(EntryIndex = IncomingIndex - 1; EntryIndex >= 0; --EntryIndex)
{
Entry = *(index_metadata *)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * EntryIndex);
if(Entry.Size > 0)
{
FoundThis = TRUE;
*ThisIsPrev = TRUE;
break;
}
}
}
if(FoundThis)
{
N->ThisIndex = EntryIndex;
N->This.Size = Entry.Size;
N->This.LinkOffsets.PrevStart = Entry.LinkOffsets.PrevStart;
N->This.LinkOffsets.PrevEnd = Entry.LinkOffsets.PrevEnd;
N->This.LinkOffsets.NextStart = Entry.LinkOffsets.NextStart;
N->This.LinkOffsets.NextEnd = Entry.LinkOffsets.NextEnd;
CopyString(N->This.BaseFilename, sizeof(N->This.BaseFilename), "%s", Entry.BaseFilename);
CopyString(N->This.Title, sizeof(N->This.Title), "%s", Entry.Title);
}
else
{
return; // NOTE(matt): We were evidently the last public entry, until now
}
}
N->PrevIsFirst = TRUE;
N->NextIsFinal = TRUE;
if(IndexEditType == EDIT_DELETION && *ThisIsPrev == FALSE)
{
EntryIndex = IncomingIndex - 1;
}
else
{
EntryIndex = N->ThisIndex - 1;
}
bool FoundPrev = FALSE;
for(; EntryIndex >= 0; --EntryIndex)
{
Entry = *(index_metadata*)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * EntryIndex);
if(Entry.Size > 0)
{
if(!FoundPrev)
{
N->PrevIndex = EntryIndex;
N->Prev.Size = Entry.Size;
N->Prev.LinkOffsets.PrevStart = Entry.LinkOffsets.PrevStart;
N->Prev.LinkOffsets.PrevEnd = Entry.LinkOffsets.PrevEnd;
N->Prev.LinkOffsets.NextStart = Entry.LinkOffsets.NextStart;
N->Prev.LinkOffsets.NextEnd = Entry.LinkOffsets.NextEnd;
CopyString(N->Prev.BaseFilename, sizeof(N->Prev.BaseFilename), "%s", Entry.BaseFilename);
CopyString(N->Prev.Title, sizeof(N->Prev.Title), "%s", Entry.Title);
FoundPrev = TRUE;
}
else
{
N->PrevIsFirst = FALSE;
break;
}
}
}
switch(IndexEditType)
{
case EDIT_DELETION:
if(*ThisIsPrev == TRUE) { EntryIndex = Index->Header.EntryCount; break; } // NOTE(matt): No need to enter the loop, else fallthrough
case EDIT_REINSERTION:
EntryIndex = N->ThisIndex + 1;
break;
case EDIT_ADDITION:
EntryIndex = N->ThisIndex;
break;
}
bool FoundNext = FALSE;
for(; EntryIndex < Index->Header.EntryCount;
++EntryIndex)
{
Entry = *(index_metadata*)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * EntryIndex);
if(Entry.Size > 0)
{
if(!FoundNext)
{
N->NextIndex = EntryIndex;
N->Next.Size = Entry.Size;
N->Next.LinkOffsets.PrevStart = Entry.LinkOffsets.PrevStart;
N->Next.LinkOffsets.PrevEnd = Entry.LinkOffsets.PrevEnd;
N->Next.LinkOffsets.NextStart = Entry.LinkOffsets.NextStart;
N->Next.LinkOffsets.NextEnd = Entry.LinkOffsets.NextEnd;
CopyString(N->Next.BaseFilename, sizeof(N->Next.BaseFilename), "%s", Entry.BaseFilename);
CopyString(N->Next.Title, sizeof(N->Next.Title), "%s", Entry.Title);
FoundNext = TRUE;
}
else
{
N->NextIsFinal = FALSE;
break;
}
}
}
switch(IndexEditType)
{
case EDIT_REINSERTION:
break;
case EDIT_DELETION:
if(*ThisIsPrev == FALSE) { --N->ThisIndex; }
if(FoundNext) { --N->NextIndex; }
break;
case EDIT_ADDITION:
if(FoundNext) { ++N->NextIndex; }
break;
}
}
int
InsertIntoIndex(index *Index, neighbourhood *N, buffers *CollationBuffers, template **BespokeTemplate, char *BaseFilename, bool RecheckingPrivacy)
{
int IndexEntryInsertionStart = -1;
int IndexEntryInsertionEnd = -1;
Index->Header.EntryCount = 0;
ClearEntry(&Index->Entry);
bool Reinserting = FALSE;
index_metadata *Entry = { 0 };
if(Index->Metadata.FileSize > 0 && Index->File.FileSize > 0)
{
// TODO(matt): Index validation?
// Maybe at least if(!StringsDiffer(..., "name: \"");
// and check that we won't crash through the end of the file when skipping to the next entry
Index->Header = *(index_header *)Index->Metadata.Buffer.Location;
Index->Header.CurrentDBVersion = CINERA_DB_VERSION;
Index->Header.CurrentAppVersion = CINERA_APP_VERSION;
Index->Header.CurrentHMMLVersion.Major = hmml_version.Major;
Index->Header.CurrentHMMLVersion.Minor = hmml_version.Minor;
Index->Header.CurrentHMMLVersion.Patch = hmml_version.Patch;
*(index_header *)Index->Metadata.Buffer.Location = Index->Header;
Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location + sizeof(Index->Header);
Index->File.Buffer.Ptr = Index->File.Buffer.Location + StringLength("---\n");
N->ThisIndex = BinarySearchForMetadataEntry(Index, &Entry, BaseFilename);
if(Entry)
{
// Reinsert
Reinserting = TRUE;
N->This.Size = Entry->Size;
N->This.LinkOffsets.PrevStart = Entry->LinkOffsets.PrevStart;
N->This.LinkOffsets.PrevEnd = Entry->LinkOffsets.PrevEnd;
N->This.LinkOffsets.NextStart = Entry->LinkOffsets.NextStart;
N->This.LinkOffsets.NextEnd = Entry->LinkOffsets.NextEnd;
IndexEntryInsertionStart = AccumulateIndexEntryInsertionOffset(Index, N->ThisIndex);
IndexEntryInsertionEnd = IndexEntryInsertionStart + Entry->Size;
}
else
{
if(N->ThisIndex == -1) { ++N->ThisIndex; } // NOTE(matt): BinarySearchForMetadataEntry returns -1 if search term precedes the set
Entry = (index_metadata*)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * N->ThisIndex);
if(StringsDiffer(BaseFilename, Entry->BaseFilename) < 0)
{
// Insert
IndexEntryInsertionStart = AccumulateIndexEntryInsertionOffset(Index, N->ThisIndex);
}
else
{
// Append
++N->ThisIndex;
}
}
GetNeighbourhood(Index, N, Reinserting ? EDIT_REINSERTION : EDIT_ADDITION, 0);
}
else
{
// NOTE(matt): Initialising new index_header
Index->Header.InitialDBVersion = Index->Header.CurrentDBVersion = CINERA_DB_VERSION;
Index->Header.InitialAppVersion = Index->Header.CurrentAppVersion = CINERA_APP_VERSION;
Index->Header.InitialHMMLVersion.Major = Index->Header.CurrentHMMLVersion.Major = hmml_version.Major;
Index->Header.InitialHMMLVersion.Minor = Index->Header.CurrentHMMLVersion.Minor = hmml_version.Minor;
Index->Header.InitialHMMLVersion.Patch = Index->Header.CurrentHMMLVersion.Patch = hmml_version.Patch;
CopyStringNoFormat(Index->Header.ProjectID, sizeof(Index->Header.ProjectID), Config.ProjectID);
for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex)
{
if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID))
{
CopyStringNoFormat(Index->Header.ProjectName, sizeof(Index->Header.ProjectName), ProjectInfo[ProjectIndex].FullName);
break;
}
}
CopyStringNoFormat(Index->Header.BaseURL, sizeof(Index->Header.BaseURL), Config.BaseURL);
CopyStringNoFormat(Index->Header.IndexLocation, sizeof(Index->Header.IndexLocation), Config.IndexLocation);
CopyStringNoFormat(Index->Header.PlayerLocation, sizeof(Index->Header.PlayerLocation), Config.PlayerLocation);
CopyStringNoFormat(Index->Header.PlayerURLPrefix, sizeof(Index->Header.PlayerURLPrefix), Config.PlayerURLPrefix);
DIR *OutputDirectoryHandle;
if(!(OutputDirectoryHandle = opendir(Config.BaseDir)))
{
if(MakeDir(Config.BaseDir) == RC_ERROR_DIRECTORY)
{
LogError(LOG_ERROR, "Unable to create directory %s: %s", Config.BaseDir, strerror(errno));
fprintf(stderr, "Unable to create directory %s: %s\n", Config.BaseDir, strerror(errno));
return RC_ERROR_DIRECTORY;
};
}
closedir(OutputDirectoryHandle);
}
char InputFile[StringLength(BaseFilename) + StringLength(".hmml") + 1];
CopyString(InputFile, sizeof(InputFile), "%s.hmml", BaseFilename);
bool VideoIsPrivate = FALSE;
switch(HMMLToBuffers(CollationBuffers, BespokeTemplate, InputFile, N))
{
// TODO(matt): Actually sort out the fatality of these cases
case RC_ERROR_FILE:
case RC_ERROR_FATAL:
return RC_ERROR_FATAL;
case RC_ERROR_HMML:
case RC_ERROR_MAX_REFS:
case RC_ERROR_QUOTE:
case RC_INVALID_REFERENCE:
return RC_ERROR_HMML;
case RC_PRIVATE_VIDEO:
VideoIsPrivate = TRUE;
case RC_SUCCESS:
break;
}
ClearCopyStringNoFormat(N->This.BaseFilename, sizeof(N->This.BaseFilename), BaseFilename);
if(N->This.Size > 0) { ClearCopyStringNoFormat(N->This.Title, sizeof(N->This.Title), CollationBuffers->Title); }
if(Reinserting)
{
// Reinsert
if(!VideoIsPrivate)
{
if(!(Index->File.Handle = fopen(Index->File.Path, "w"))) { return RC_ERROR_FILE; }
fwrite(Index->File.Buffer.Location, IndexEntryInsertionStart, 1, Index->File.Handle);
fwrite(CollationBuffers->Search.Location, N->This.Size, 1, Index->File.Handle);
fwrite(Index->File.Buffer.Location + IndexEntryInsertionEnd, Index->File.FileSize - IndexEntryInsertionEnd, 1, Index->File.Handle);
fclose(Index->File.Handle);
if(N->This.Size == IndexEntryInsertionEnd - IndexEntryInsertionStart)
{
Index->File.Buffer.Ptr = Index->File.Buffer.Location + IndexEntryInsertionStart;
CopyBufferSized(&Index->File.Buffer, &CollationBuffers->Search, N->This.Size);
}
else
{
FreeBuffer(&Index->File.Buffer);
ReadFileIntoBuffer(&Index->File, 0);
}
LogError(LOG_NOTICE, "Reinserted %s - %s", BaseFilename, CollationBuffers->Title);
fprintf(stderr, "\e[1;33mReinserted\e[0m %s - %s\n", BaseFilename, CollationBuffers->Title);
}
else if(!RecheckingPrivacy)
{
LogError(LOG_NOTICE, "Privately Reinserted %s", BaseFilename);
fprintf(stderr, "\e[0;34mPrivately Reinserted\e[0m %s\n", BaseFilename);
}
}
else
{
++Index->Header.EntryCount;
if(!(Index->Metadata.Handle = fopen(Index->Metadata.Path, "w"))) { return RC_ERROR_FILE; }
fwrite(&Index->Header, sizeof(index_header), 1, Index->Metadata.Handle);
if(!(Index->File.Handle = fopen(Index->File.Path, "w"))) { return RC_ERROR_FILE; }
if(IndexEntryInsertionStart >= 0)
{
// Insert new
fwrite(Index->Metadata.Buffer.Location + sizeof(index_header), sizeof(index_metadata) * N->ThisIndex, 1, Index->Metadata.Handle);
fwrite(&N->This, sizeof(index_metadata), 1, Index->Metadata.Handle);
fwrite(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * N->ThisIndex,
Index->Metadata.FileSize - sizeof(index_header) - sizeof(index_metadata) * N->ThisIndex,
1, Index->Metadata.Handle);
fwrite(Index->File.Buffer.Location, IndexEntryInsertionStart, 1, Index->File.Handle);
fwrite(CollationBuffers->Search.Location, N->This.Size, 1, Index->File.Handle);
fwrite(Index->File.Buffer.Location + IndexEntryInsertionStart, Index->File.FileSize - IndexEntryInsertionStart, 1, Index->File.Handle);
if(!VideoIsPrivate)
{
LogError(LOG_NOTICE, "Inserted %s - %s", BaseFilename, CollationBuffers->Title);
fprintf(stderr, "\e[1;32mInserted\e[0m %s - %s\n", BaseFilename, CollationBuffers->Title);
}
else if(!RecheckingPrivacy)
{
LogError(LOG_NOTICE, "Privately Inserted %s", BaseFilename);
fprintf(stderr, "\e[0;34mPrivately Inserted\e[0m %s\n", BaseFilename);
}
}
else
{
// Append new
if(Index->Metadata.FileSize > 0)
{
fwrite(Index->Metadata.Buffer.Location + sizeof(index_header), Index->Metadata.FileSize - sizeof(index_header), 1, Index->Metadata.Handle);
fwrite(Index->File.Buffer.Location, Index->File.FileSize, 1, Index->File.Handle);
}
else
{
fprintf(Index->File.Handle, "---\n");
}
fwrite(&N->This, sizeof(index_metadata), 1, Index->Metadata.Handle);
fwrite(CollationBuffers->Search.Location, N->This.Size, 1, Index->File.Handle);
if(!VideoIsPrivate)
{
LogError(LOG_NOTICE, "Appended %s - %s", BaseFilename, CollationBuffers->Title);
fprintf(stderr, "\e[1;32mAppended\e[0m %s - %s\n", BaseFilename, CollationBuffers->Title);
}
else if(!RecheckingPrivacy)
{
LogError(LOG_NOTICE, "Privately Appended %s", BaseFilename);
fprintf(stderr, "\e[0;34mPrivately Appended\e[0m %s\n", BaseFilename);
}
}
fclose(Index->Metadata.Handle);
FreeBuffer(&Index->Metadata.Buffer);
ReadFileIntoBuffer(&Index->Metadata, 0);
fclose(Index->File.Handle);
FreeBuffer(&Index->File.Buffer);
ReadFileIntoBuffer(&Index->File, 0);
}
// TODO(matt): Remove VideoIsPrivate in favour of generating a player page in a random location
return VideoIsPrivate ? RC_PRIVATE_VIDEO : RC_SUCCESS;
}
void
ConstructDirectoryPath(buffer *DirectoryPath, int PageType, char *PageLocation, char *BaseFilename)
{
RewindBuffer(DirectoryPath);
CopyStringToBuffer(DirectoryPath, "%s", Config.BaseDir);
switch(PageType)
{
case PAGE_INDEX:
if(StringsDiffer(PageLocation, ""))
{
CopyStringToBuffer(DirectoryPath, "/%s", PageLocation);
}
break;
case PAGE_PLAYER:
if(StringsDiffer(PageLocation, ""))
{
CopyStringToBuffer(DirectoryPath, "/%s", PageLocation);
}
if(BaseFilename)
{
if(StringsDiffer(Config.PlayerURLPrefix, ""))
{
char *Ptr = BaseFilename + StringLength(Config.ProjectID);
CopyStringToBuffer(DirectoryPath, "/%s%s", Config.PlayerURLPrefix, Ptr);
}
else
{
CopyStringToBuffer(DirectoryPath, "/%s", BaseFilename);
}
}
break;
}
}
enum
{
LINK_INCLUDE,
LINK_EXCLUDE
} link_types;
enum
{
LINK_PREV,
LINK_NEXT
} link_directions;
int
InsertNeighbourLink(file_buffer *FromFile, index_metadata *From, index_metadata *To, int LinkDirection, char *ProjectName, bool FromHasOneNeighbour)
{
if(ReadFileIntoBuffer(FromFile, 0) == RC_SUCCESS)
{
if(!(FromFile->Handle = fopen(FromFile->Path, "w"))) { FreeBuffer(&FromFile->Buffer); return RC_ERROR_FILE; };
buffer Link;
ClaimBuffer(&Link, "Link", 4096);
buffer ToPlayerURL;
if(To)
{
ClaimBuffer(&ToPlayerURL, "ToPlayerURL", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH);
ConstructPlayerURL(&ToPlayerURL, To->BaseFilename);
}
switch(LinkDirection)
{
case LINK_PREV:
{
int NewPrevEnd = 0;
int NewNextEnd = 0;
fwrite(FromFile->Buffer.Location, From->LinkOffsets.PrevStart, 1, FromFile->Handle);
if(To)
{
CopyStringToBuffer(&Link,
" <a class=\"episodeMarker prev\" href=\"%s\"><div>&#9195;</div><div>Previous: '%s'</div><div>&#9195;</div></a>\n",
ToPlayerURL.Location,
To->Title);
}
else
{
CopyStringToBuffer(&Link,
" <div class=\"episodeMarker first\"><div>&#8226;</div><div>Welcome to <cite>%s</cite></div><div>&#8226;</div></div>\n", ProjectName);
}
NewPrevEnd = Link.Ptr - Link.Location;
fwrite(Link.Location, (Link.Ptr - Link.Location), 1, FromFile->Handle);
if(FromHasOneNeighbour)
{
fwrite(FromFile->Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd, From->LinkOffsets.NextStart, 1, FromFile->Handle);
RewindBuffer(&Link);
CopyStringToBuffer(&Link,
" <div class=\"episodeMarker last\"><div>&#8226;</div><div>You have arrived at the (current) end of <cite>%s</cite></div><div>&#8226;</div></div>\n", ProjectName);
NewNextEnd = Link.Ptr - Link.Location;
fwrite(Link.Location, NewNextEnd, 1, FromFile->Handle);
fwrite(FromFile->Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd,
FromFile->FileSize - (From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd),
1,
FromFile->Handle);
}
else
{
fwrite(FromFile->Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd,
FromFile->FileSize - (From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd),
1,
FromFile->Handle);
}
From->LinkOffsets.PrevEnd = NewPrevEnd;
if(FromHasOneNeighbour) { From->LinkOffsets.NextEnd = NewNextEnd; }
} break;
case LINK_NEXT:
{
int NewPrevEnd = 0;
int NewNextEnd = 0;
if(FromHasOneNeighbour)
{
fwrite(FromFile->Buffer.Location, From->LinkOffsets.PrevStart, 1, FromFile->Handle);
CopyStringToBuffer(&Link,
" <div class=\"episodeMarker first\"><div>&#8226;</div><div>Welcome to <cite>%s</cite></div><div>&#8226;</div></div>\n", ProjectName);
NewPrevEnd = Link.Ptr - Link.Location;
fwrite(Link.Location, NewPrevEnd, 1, FromFile->Handle);
RewindBuffer(&Link);
fwrite(FromFile->Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd,
From->LinkOffsets.NextStart, 1, FromFile->Handle);
}
else
{
fwrite(FromFile->Buffer.Location, From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart, 1, FromFile->Handle);
}
if(To)
{
CopyStringToBuffer(&Link,
" <a class=\"episodeMarker next\" href=\"%s\"><div>&#9196;</div><div>Next: '%s'</div><div>&#9196;</div></a>\n",
ToPlayerURL.Location,
To->Title);
}
else
{
CopyStringToBuffer(&Link,
" <div class=\"episodeMarker last\"><div>&#8226;</div><div>You have arrived at the (current) end of <cite>%s</cite></div><div>&#8226;</div></div>\n", ProjectName);
}
NewNextEnd = Link.Ptr - Link.Location;
fwrite(Link.Location, (Link.Ptr - Link.Location), 1, FromFile->Handle);
fwrite(FromFile->Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd,
FromFile->FileSize - (From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd),
1,
FromFile->Handle);
if(FromHasOneNeighbour) { From->LinkOffsets.PrevEnd = NewPrevEnd; }
From->LinkOffsets.NextEnd = NewNextEnd;
} break;
}
if(To) { DeclaimBuffer(&ToPlayerURL); }
DeclaimBuffer(&Link);
fclose(FromFile->Handle);
FreeBuffer(&FromFile->Buffer);
return RC_SUCCESS;
}
else
{
return RC_ERROR_FILE;
}
}
int
DeleteNeighbourLinks(file_buffer *File, index_metadata *Metadata)
{
if(ReadFileIntoBuffer(File, 0) == RC_SUCCESS)
{
if(!(File->Handle = fopen(File->Path, "w"))) { FreeBuffer(&File->Buffer); return RC_ERROR_FILE; };
fwrite(File->Buffer.Location, Metadata->LinkOffsets.PrevStart, 1, File->Handle);
fwrite(File->Buffer.Location + Metadata->LinkOffsets.PrevStart + Metadata->LinkOffsets.PrevEnd, Metadata->LinkOffsets.NextStart, 1, File->Handle);
fwrite(File->Buffer.Location + Metadata->LinkOffsets.PrevStart + Metadata->LinkOffsets.PrevEnd + Metadata->LinkOffsets.NextStart + Metadata->LinkOffsets.NextEnd,
File->FileSize - (Metadata->LinkOffsets.PrevStart + Metadata->LinkOffsets.PrevEnd + Metadata->LinkOffsets.NextStart + Metadata->LinkOffsets.NextEnd),
1,
File->Handle);
fclose(File->Handle);
Metadata->LinkOffsets.PrevEnd = 0;
Metadata->LinkOffsets.NextEnd = 0;
FreeBuffer(&File->Buffer);
return RC_SUCCESS;
}
else { return RC_ERROR_FILE; }
}
int
LinkNeighbours(index *Index, neighbourhood *N, char *BaseFilename, int LinkType)
{
if(N->PrevIndex == -1 && N->NextIndex == -1)
{
buffer LonePlayerPagePath;
ClaimBuffer(&LonePlayerPagePath, "LonePlayerPagePath", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10);
ConstructDirectoryPath(&LonePlayerPagePath, PAGE_PLAYER, Config.PlayerLocation, N->This.BaseFilename);
CopyStringToBuffer(&LonePlayerPagePath, "/index.html");
file_buffer LonePlayerPage;
CopyStringNoFormat(LonePlayerPage.Path, sizeof(LonePlayerPage.Path), LonePlayerPagePath.Location);
DeleteNeighbourLinks(&LonePlayerPage, &N->This);
DeclaimBuffer(&LonePlayerPagePath);
}
else
{
if(N->PrevIndex >= 0)
{
buffer PreviousPlayerPagePath;
ClaimBuffer(&PreviousPlayerPagePath, "PreviousPlayerPagePath", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10);
ConstructDirectoryPath(&PreviousPlayerPagePath, PAGE_PLAYER, Config.PlayerLocation, N->Prev.BaseFilename);
CopyStringToBuffer(&PreviousPlayerPagePath, "/index.html");
file_buffer PreviousPlayerPage;
CopyStringNoFormat(PreviousPlayerPage.Path, sizeof(PreviousPlayerPage.Path), PreviousPlayerPagePath.Location);
switch(LinkType)
{
case LINK_EXCLUDE:
{
buffer ThisPlayerPagePath;
ClaimBuffer(&ThisPlayerPagePath, "ThisPlayerPagePath", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10);
ConstructDirectoryPath(&ThisPlayerPagePath, PAGE_PLAYER, Config.PlayerLocation, N->This.BaseFilename);
CopyStringToBuffer(&ThisPlayerPagePath, "/index.html");
file_buffer ThisPlayerPage;
CopyStringNoFormat(ThisPlayerPage.Path, sizeof(ThisPlayerPage.Path), ThisPlayerPagePath.Location);
InsertNeighbourLink(&ThisPlayerPage, &N->This, &N->Prev, LINK_PREV, Index->Header.ProjectName, N->NextIndex == -1 ? TRUE : FALSE);
DeclaimBuffer(&ThisPlayerPagePath);
}
case LINK_INCLUDE:
{
InsertNeighbourLink(&PreviousPlayerPage, &N->Prev, &N->This, LINK_NEXT, Index->Header.ProjectName, N->PrevIsFirst);
*(index_metadata *)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * N->PrevIndex) = N->Prev;
}
}
DeclaimBuffer(&PreviousPlayerPagePath);
}
if(N->NextIndex >= 0)
{
buffer NextPlayerPagePath;
ClaimBuffer(&NextPlayerPagePath, "NextPlayerPagePath", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10);
ConstructDirectoryPath(&NextPlayerPagePath, PAGE_PLAYER, Config.PlayerLocation, N->Next.BaseFilename);
CopyStringToBuffer(&NextPlayerPagePath, "/index.html");
file_buffer NextPlayerPage;
CopyStringNoFormat(NextPlayerPage.Path, sizeof(NextPlayerPage.Path), NextPlayerPagePath.Location);
switch(LinkType)
{
case LINK_EXCLUDE:
{
buffer ThisPlayerPagePath;
ClaimBuffer(&ThisPlayerPagePath, "ThisPlayerPagePath", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10);
ConstructDirectoryPath(&ThisPlayerPagePath, PAGE_PLAYER, Config.PlayerLocation, N->This.BaseFilename);
CopyStringToBuffer(&ThisPlayerPagePath, "/index.html");
file_buffer ThisPlayerPage;
CopyStringNoFormat(ThisPlayerPage.Path, sizeof(ThisPlayerPage.Path), ThisPlayerPagePath.Location);
InsertNeighbourLink(&ThisPlayerPage, &N->This, &N->Next, LINK_NEXT, Index->Header.ProjectName, N->PrevIndex == -1 ? TRUE : FALSE);
DeclaimBuffer(&ThisPlayerPagePath);
}
case LINK_INCLUDE:
{
InsertNeighbourLink(&NextPlayerPage, &N->Next, &N->This, LINK_PREV, Index->Header.ProjectName, N->NextIsFinal);
*(index_metadata *)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * N->NextIndex) = N->Next;
}
}
DeclaimBuffer(&NextPlayerPagePath);
}
}
return RC_SUCCESS;
}
void
DeleteIndexPageFromFilesystem() // NOTE(matt): Do we need to handle relocating, like the PlayerPage function?
{
buffer IndexDirectory;
ClaimBuffer(&IndexDirectory, "IndexDirectory", 1024);
ConstructDirectoryPath(&IndexDirectory, PAGE_INDEX, Config.IndexLocation, "");
char IndexPagePath[1024];
CopyString(IndexPagePath, sizeof(IndexPagePath), "%s/index.html", IndexDirectory.Location);
remove(IndexPagePath);
remove(IndexDirectory.Location);
DeclaimBuffer(&IndexDirectory);
}
int
DeletePlayerPageFromFilesystem(char *BaseFilename, char *PlayerLocation, bool Relocating)
{
// NOTE(matt): Once we have the notion of an output filename format, we'll need to use that here
buffer OutputDirectoryPath;
ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", 1024);
ConstructDirectoryPath(&OutputDirectoryPath, PAGE_PLAYER, PlayerLocation, BaseFilename);
DIR *PlayerDir;
if((PlayerDir = opendir(OutputDirectoryPath.Location))) // There is a directory for the Player, which there probably should be if not for manual intervention
{
char PlayerPagePath[256];
CopyString(PlayerPagePath, sizeof(PlayerPagePath), "%s/index.html", OutputDirectoryPath.Location);
FILE *PlayerPage;
if((PlayerPage = fopen(PlayerPagePath, "r")))
{
fclose(PlayerPage);
remove(PlayerPagePath);
}
closedir(PlayerDir);
if((remove(OutputDirectoryPath.Location) == -1))
{
LogError(LOG_NOTICE, "Mostly deleted %s. Unable to remove directory %s: %s", BaseFilename, OutputDirectoryPath.Location, strerror(errno));
fprintf(stderr, "\e[1;30mMostly deleted\e[0m %s. \e[1;31mUnable to remove directory\e[0m %s: %s", BaseFilename, OutputDirectoryPath.Location, strerror(errno));
}
else
{
if(!Relocating)
{
LogError(LOG_INFORMATIONAL, "Deleted %s", BaseFilename);
fprintf(stderr, "\e[1;30mDeleted\e[0m %s\n", BaseFilename);
}
}
}
DeclaimBuffer(&OutputDirectoryPath);
return RC_SUCCESS;
}
int
DeleteFromIndex(index *Index, neighbourhood *N, char *BaseFilename)
{
// TODO(matt): LogError()
Index->Header = *(index_header *)Index->Metadata.Buffer.Location;
Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location + sizeof(Index->Header);
index_metadata *Entry = { 0 };
int EntryIndex = BinarySearchForMetadataEntry(Index, &Entry, BaseFilename);
if(Entry)
{
int DeleteFileFrom = AccumulateIndexEntryInsertionOffset(Index, EntryIndex);
int DeleteFileTo = DeleteFileFrom + Entry->Size;
bool ThisIsPrev = FALSE;
N->ThisIndex = EntryIndex;
GetNeighbourhood(Index, N, EDIT_DELETION, &ThisIsPrev);
--Index->Header.EntryCount;
if(Index->Header.EntryCount == 0)
{
DeleteIndexPageFromFilesystem();
remove(Index->Metadata.Path);
Index->Metadata.FileSize = 0;
FreeBuffer(&Index->Metadata.Buffer);
remove(Index->File.Path);
Index->File.FileSize = 0;
FreeBuffer(&Index->File.Buffer);
}
else
{
if(!(Index->Metadata.Handle = fopen(Index->Metadata.Path, "w"))) { FreeBuffer(&Index->Metadata.Buffer); return RC_ERROR_FILE; }
if(!(Index->File.Handle = fopen(Index->File.Path, "w"))) { FreeBuffer(&Index->File.Buffer); return RC_ERROR_FILE; }
fwrite(&Index->Header, sizeof(Index->Header), 1, Index->Metadata.Handle);
fwrite(Index->Metadata.Buffer.Location + sizeof(index_header), sizeof(index_metadata) * EntryIndex, 1, Index->Metadata.Handle);
fwrite(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * (EntryIndex + 1),
Index->Metadata.FileSize - sizeof(index_header) - sizeof(index_metadata) * (EntryIndex + 1),
1, Index->Metadata.Handle);
fclose(Index->Metadata.Handle);
FreeBuffer(&Index->Metadata.Buffer);
ReadFileIntoBuffer(&Index->Metadata, 0);
fwrite(Index->File.Buffer.Location, DeleteFileFrom, 1, Index->File.Handle);
fwrite(Index->File.Buffer.Location + DeleteFileTo, Index->File.FileSize - DeleteFileTo, 1, Index->File.Handle);
fclose(Index->File.Handle);
FreeBuffer(&Index->File.Buffer);
ReadFileIntoBuffer(&Index->File, 0);
}
}
return Entry ? RC_SUCCESS : RC_NOOP;
}
int
IndexToBuffer(index *Index, buffers *CollationBuffers) // NOTE(matt): This guy malloc's CollationBuffers->Index
{
if(Index->Metadata.FileSize > 0)
{
Index->Header = *(index_header*)Index->Metadata.Buffer.Location;
int ProjectIndex;
for(ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex)
{
if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID))
{
break;
}
}
char *Theme = StringsDiffer(Config.Theme, "") ? Config.Theme : Config.ProjectID;
int Allowance = StringLength(Theme) * 2 + StringLength(Config.ProjectID) + StringLength(Config.BaseURL) + StringLength(Config.PlayerLocation);
char queryContainer[678 + Allowance]; // NOTE(matt): Update the size if changing the string
CopyString(queryContainer, sizeof(queryContainer),
"<div class=\"cineraQueryContainer %s\">\n"
" <label for=\"query\">Query:</label>\n"
" <div class=\"inputContainer\">\n"
" <input type=\"text\" autocomplete=\"off\" id=\"query\">\n"
" <div class=\"spinner\">\n"
" Downloading data...\n"
" </div>\n"
" </div>\n"
" </div>\n"
" <div id=\"cineraResultsSummary\">Found: 0 episodes, 0 markers, 0h 0m 0s total.</div>\n"
" <div id=\"cineraResults\" data-single=\"%d\"></div>\n"
"\n"
" <div id=\"cineraIndex\" class=\"%s\" data-project=\"%s\" data-baseURL=\"%s\" data-playerLocation=\"%s\">\n"
" <div id=\"cineraIndexSort\">Sort: Old to New &#9206;</div>\n"
" <div id=\"cineraIndexEntries\">\n",
Theme,
Config.Mode & MODE_SINGLETAB ? 1 : 0,
Theme,
Config.ProjectID,
Config.BaseURL,
Config.PlayerLocation);
buffer URLPrefix;
ClaimBuffer(&URLPrefix, "URLPrefix", 1024);
ConstructURLPrefix(&URLPrefix, INCLUDE_JS, PAGE_INDEX);
char Script[107 + StringLength(URLPrefix.Location)]; // NOTE(matt): Update the size if changing the string
CopyString(Script, sizeof(Script),
" </div>\n"
" </div>\n"
" <script type=\"text/javascript\" src=\"%scinera_search.js\"></script>\n",
URLPrefix.Location);
DeclaimBuffer(&URLPrefix);
buffer PlayerURL;
ClaimBuffer(&PlayerURL, "PlayerURL", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH);
ConstructPlayerURL(&PlayerURL, "");
char *ProjectUnit = 0;
if(StringsDiffer(ProjectInfo[ProjectIndex].Unit, ""))
{
ProjectUnit = ProjectInfo[ProjectIndex].Unit;
}
char Number[16];
index_metadata *This;
char Text[(ProjectUnit ? StringLength(ProjectUnit) : 0) + sizeof(Number) + sizeof(This->Title) + 3];
int EntryLength = StringLength(PlayerURL.Location) + sizeof(Text) + 82;
CollationBuffers->Index.Size = StringLength(queryContainer) + (Index->Header.EntryCount * EntryLength) + StringLength(Script);
if(!(CollationBuffers->Index.Location = malloc(CollationBuffers->Index.Size))) { return RC_ERROR_MEMORY; }
CollationBuffers->Index.ID = "Index";
CollationBuffers->Index.Ptr = CollationBuffers->Index.Location;
CopyStringToBuffer(&CollationBuffers->Index, "%s", queryContainer);
int ProjectIDLength = StringLength(Config.ProjectID);
bool IndexRequired = FALSE;
Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location + sizeof(Index->Header);
for(int EntryIndex = 0; EntryIndex < Index->Header.EntryCount; ++EntryIndex, Index->Metadata.Buffer.Ptr += sizeof(index_metadata))
{
This = (index_metadata *)Index->Metadata.Buffer.Ptr;
if(This->Size > 0)
{
IndexRequired = TRUE;
CopyString(Number, sizeof(Number), "%s", This->BaseFilename + ProjectIDLength);
if(ProjectInfo[ProjectIndex].NumberingScheme == NS_LINEAR)
{
for(int i = 0; Number[i]; ++i)
{
if(Number[i] == '_')
{
Number[i] = '.';
}
}
}
ConstructPlayerURL(&PlayerURL, This->BaseFilename);
if(ProjectUnit)
{
CopyStringToBuffer(&CollationBuffers->Index,
" <div>\n"
" <a href=\"%s\">", PlayerURL.Location);
CopyString(Text, sizeof(Text), "%s %s: %s",
ProjectUnit, // TODO(matt): Do we need to special-case the various numbering schemes?
Number,
This->Title);
CopyStringToBufferHTMLSafe(&CollationBuffers->Index, Text);
CopyStringToBuffer(&CollationBuffers->Index,
"</a>\n"
" </div>\n");
}
else
{
CopyStringToBuffer(&CollationBuffers->Index,
" <div>\n"
" <a href=\"%s\">", PlayerURL.Location);
CopyStringToBufferHTMLSafe(&CollationBuffers->Index, This->Title);
CopyStringToBuffer(&CollationBuffers->Index,
"</a>\n"
" </div>\n");
}
}
}
DeclaimBuffer(&PlayerURL);
CopyStringToBuffer(&CollationBuffers->Index, "%s", Script);
if(!IndexRequired) { return RC_NOOP; }
else { return RC_SUCCESS; }
}
else
{
return RC_ERROR_FILE;
}
}
char *
StripTrailingSlash(char *String) // NOTE(matt): For absolute paths
{
int Length = StringLength(String);
while(Length > 0 && String[Length - 1] == '/')
{
String[Length - 1] = '\0';
--Length;
}
return String;
}
char *
StripSurroundingSlashes(char *String) // NOTE(matt): For relative paths
{
int Length = StringLength(String);
if(Length > 0)
{
while((String[0]) == '/')
{
++String;
--Length;
}
while(String[Length - 1] == '/')
{
String[Length - 1] = '\0';
--Length;
}
}
return String;
}
int
GeneratePlayerPage(index *Index, neighbourhood *N, buffers *CollationBuffers, template *PlayerTemplate, char *BaseFilename)
{
buffer OutputDirectoryPath;
ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", 1024);
ConstructDirectoryPath(&OutputDirectoryPath, PAGE_PLAYER, Config.PlayerLocation, BaseFilename);
DIR *OutputDirectoryHandle;
if(!(OutputDirectoryHandle = opendir(OutputDirectoryPath.Location))) // TODO(matt): open()
{
if(MakeDir(OutputDirectoryPath.Location) == RC_ERROR_DIRECTORY)
{
LogError(LOG_ERROR, "Unable to create directory %s: %s", OutputDirectoryPath.Location, strerror(errno));
fprintf(stderr, "Unable to create directory %s: %s\n", OutputDirectoryPath.Location, strerror(errno));
return RC_ERROR_DIRECTORY;
};
}
closedir(OutputDirectoryHandle);
char PlayerPagePath[1024];
CopyString(PlayerPagePath, sizeof(PlayerPagePath), "%s/index.html", OutputDirectoryPath.Location);
DeclaimBuffer(&OutputDirectoryPath);
bool IndexInTemplate = FALSE;
for(int TagIndex = 0; TagIndex < PlayerTemplate->Metadata.TagCount; ++TagIndex)
{
if(PlayerTemplate->Metadata.Tag[TagIndex].TagCode == TAG_INDEX)
{
IndexInTemplate = TRUE;
IndexToBuffer(Index, CollationBuffers);
break;
}
}
BuffersToHTML(CollationBuffers, PlayerTemplate, PlayerPagePath, PAGE_PLAYER, &N->This.LinkOffsets.PrevStart);
*(index_metadata *)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * N->ThisIndex) = N->This;
Index->Metadata.Handle = fopen(Index->Metadata.Path, "w");
fwrite(Index->Metadata.Buffer.Location, Index->Metadata.Buffer.Size, 1, Index->Metadata.Handle);
fclose(Index->Metadata.Handle);
if(IndexInTemplate)
{
FreeBuffer(&CollationBuffers->Index);
}
return RC_SUCCESS;
}
int
GenerateIndexPage(index *Index, buffers *CollationBuffers, template *IndexTemplate)
{
buffer OutputDirectoryPath;
ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", 1024);
ConstructDirectoryPath(&OutputDirectoryPath, PAGE_INDEX, Config.IndexLocation, 0);
DIR *OutputDirectoryHandle;
if(!(OutputDirectoryHandle = opendir(OutputDirectoryPath.Location))) // TODO(matt): open()
{
if(MakeDir(OutputDirectoryPath.Location) == RC_ERROR_DIRECTORY)
{
LogError(LOG_ERROR, "Unable to create directory %s: %s", OutputDirectoryPath.Location, strerror(errno));
fprintf(stderr, "Unable to create directory %s: %s\n", OutputDirectoryPath.Location, strerror(errno));
return RC_ERROR_DIRECTORY;
};
}
closedir(OutputDirectoryHandle);
char IndexPagePath[1024];
CopyString(IndexPagePath, sizeof(IndexPagePath), "%s/index.html", OutputDirectoryPath.Location);
DeclaimBuffer(&OutputDirectoryPath);
switch(IndexToBuffer(Index, CollationBuffers))
{
case RC_SUCCESS:
{
BuffersToHTML(CollationBuffers, IndexTemplate, IndexPagePath, PAGE_INDEX, 0);
break;
}
case RC_NOOP:
{
DeleteIndexPageFromFilesystem();
break;
}
}
FreeBuffer(&CollationBuffers->Index);
return RC_SUCCESS;
}
int
DeleteEntry(index *Index, neighbourhood *Neighbourhood, char *BaseFilename)
{
if(DeleteFromIndex(Index, Neighbourhood, BaseFilename) == RC_SUCCESS)
{
if(Neighbourhood->This.Size > 0)
{
LinkNeighbours(Index, Neighbourhood, BaseFilename, LINK_EXCLUDE);
}
DeletePlayerPageFromFilesystem(BaseFilename, Config.PlayerLocation, FALSE);
return RC_SUCCESS;
}
return RC_NOOP;
}
int
InsertEntry(index *Index, neighbourhood *Neighbourhood, buffers *CollationBuffers, template *PlayerTemplate, template *BespokeTemplate, char *BaseFilename, bool RecheckingPrivacy)
{
if(InsertIntoIndex(Index, Neighbourhood, CollationBuffers, &BespokeTemplate, BaseFilename, RecheckingPrivacy) == RC_SUCCESS)
{
LinkNeighbours(Index, Neighbourhood, BaseFilename, LINK_INCLUDE);
if(StringsDiffer(BespokeTemplate->Metadata.Filename, ""))
{
GeneratePlayerPage(Index, Neighbourhood, CollationBuffers, BespokeTemplate, BaseFilename);
DeclaimTemplate(BespokeTemplate);
}
else
{
GeneratePlayerPage(Index, Neighbourhood, CollationBuffers, PlayerTemplate, BaseFilename);
}
return RC_SUCCESS;
}
return RC_NOOP;
}
int
RecheckPrivacy(index *Index, buffers *CollationBuffers, template *IndexTemplate, template *PlayerTemplate, template *BespokeTemplate)
{
if(Index->Metadata.FileSize > 0)
{
Index->Header = *(index_header*)Index->Metadata.Buffer.Location;
index_metadata Entry = { };
int PrivateEntryIndex = 0;
index_metadata PrivateEntries[Index->Header.EntryCount];
bool Inserted = FALSE;
for(int IndexEntry = 0; IndexEntry < Index->Header.EntryCount; ++IndexEntry)
{
Entry = *(index_metadata *)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * IndexEntry);
if(Entry.Size == 0)
{
PrivateEntries[PrivateEntryIndex] = Entry;
++PrivateEntryIndex;
}
}
for(int i = 0; i < PrivateEntryIndex; ++i)
{
neighbourhood Neighbourhood = { };
Neighbourhood.PrevIndex = -1;
Neighbourhood.ThisIndex = -1;
Neighbourhood.NextIndex = -1;
Inserted = (InsertEntry(Index, &Neighbourhood, CollationBuffers, PlayerTemplate, BespokeTemplate, PrivateEntries[i].BaseFilename, TRUE) == RC_SUCCESS);
}
if(Inserted)
{
GenerateIndexPage(Index, CollationBuffers, IndexTemplate);
}
LastPrivacyCheck = time(0);
}
return RC_SUCCESS;
}
int
MonitorDirectory(index *Index, buffers *CollationBuffers, template *IndexTemplate, template *PlayerTemplate, template *BespokeTemplate, int inotifyInstance, int WatchDescriptor)
{
#if DEBUG_MEM
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, "\nCalled MonitorDirectory()\n");
fclose(MemLog);
#endif
buffer Events;
if(ClaimBuffer(&Events, "inotify Events", Kilobytes(4)) == RC_ARENA_FULL) { return RC_ARENA_FULL; };
struct inotify_event *Event;
int BytesRead = read(inotifyInstance, Events.Location, Events.Size); // TODO(matt): Handle error EINVAL
if(inotifyInstance < 0) { perror("MonitorDirectory()"); }
struct inotify_event *FinalFileEvents[1024];
int FinalFileEventsCount = 0;
// TODO(matt): Test this with longer update intervals, and combinations of events...
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 = Event->name;
Ptr += (StringLength(Event->name) - StringLength(".hmml"));
if(!(StringsDiffer(Ptr, ".hmml")))
{
*Ptr = '\0';
bool FoundEvent = FALSE;
int FinalFileEventsIndex;
for(FinalFileEventsIndex = 0; FinalFileEventsIndex < FinalFileEventsCount; ++FinalFileEventsIndex)
{
if(!StringsDiffer(FinalFileEvents[FinalFileEventsIndex]->name, Event->name))
{
FinalFileEvents[FinalFileEventsIndex] = Event;
FoundEvent = TRUE;
}
}
if(!FoundEvent) { FinalFileEvents[FinalFileEventsIndex] = Event; ++FinalFileEventsCount; }
}
}
bool Deleted = FALSE;
bool Inserted = FALSE;
for(int FinalFileEventsIndex = 0; FinalFileEventsIndex < FinalFileEventsCount; ++FinalFileEventsIndex)
{
neighbourhood Neighbourhood = { };
Neighbourhood.PrevIndex = -1;
Neighbourhood.ThisIndex = -1;
Neighbourhood.NextIndex = -1;
// TODO(matt): Maybe handle IN_ALL_EVENTS
if(FinalFileEvents[FinalFileEventsIndex]->mask & IN_DELETE || FinalFileEvents[FinalFileEventsIndex]->mask & IN_MOVED_FROM)
{
Deleted |= (DeleteEntry(Index, &Neighbourhood, FinalFileEvents[FinalFileEventsIndex]->name) == RC_SUCCESS);
}
else
{
Inserted |= (InsertEntry(Index, &Neighbourhood, CollationBuffers, PlayerTemplate, BespokeTemplate, FinalFileEvents[FinalFileEventsIndex]->name, 0) == RC_SUCCESS);
}
}
if(Deleted || Inserted)
{
GenerateIndexPage(Index, CollationBuffers, IndexTemplate);
}
DeclaimBuffer(&Events);
return RC_SUCCESS;
}
int
RemoveDirectory(char *Path)
{
if((remove(Path) == -1))
{
LogError(LOG_NOTICE, "Unable to remove directory %s: %s", Path, strerror(errno));
fprintf(stderr, "\e[1;30mUnable to remove directory\e[0m %s: %s\n", Path, strerror(errno));
return RC_ERROR_DIRECTORY;
}
else
{
LogError(LOG_INFORMATIONAL, "Deleted %s", Path);
//fprintf(stderr, "\e[1;30mDeleted\e[0m %s\n", Path);
return RC_SUCCESS;
}
}
int
RemoveDirectoryRecursively(char *Path)
{
if(RemoveDirectory(Path) == RC_ERROR_DIRECTORY) { return RC_ERROR_DIRECTORY; }
char *Ptr = Path + StringLength(Path) - 1;
while(Ptr > Path)
{
if(*Ptr == '/')
{
*Ptr = '\0';
if(RemoveDirectory(Path) == RC_ERROR_DIRECTORY) { return RC_ERROR_DIRECTORY; }
}
--Ptr;
}
return RC_SUCCESS;
}
int
UpgradeDB(index *Index)
{
// DBVersion 1
typedef struct { unsigned int DBVersion; version AppVersion; version HMMLVersion; unsigned int EntryCount; } index_header1;
typedef struct { int Size; char BaseFilename[32]; } index_metadata1;
typedef struct { file_buffer File; file_buffer Metadata; index_header1 Header; index_metadata1 Entry; } index1;
//
// DBVersion 2
typedef struct { unsigned int DBVersion; version AppVersion; version HMMLVersion; unsigned int EntryCount; char IndexLocation[32]; char PlayerLocation[32]; } index_header2;
typedef struct { int Size; char BaseFilename[32]; } index_metadata2;
typedef struct { file_buffer File; file_buffer Metadata; index_header2 Header; index_metadata2 Entry; } index2;
//
// NOTE(matt): For each new DB version, we must declare and initialise one instance of each preceding version, only cast the
// incoming Index to the type of the OriginalDBVersion, and move into the final case all operations on that incoming Index
bool OnlyHeaderChanged = TRUE;
int OriginalHeaderSize = 0;
index1 Index1 = { };
index2 Index2 = { };
int OriginalDBVersion = Index->Header.CurrentDBVersion;
switch(OriginalDBVersion)
{
case 1:
{
OriginalHeaderSize = sizeof(index_header1);
Index1.Header = *(index_header1 *)Index->Metadata.Buffer.Ptr;
Index2.Header.DBVersion = CINERA_DB_VERSION;
Index2.Header.AppVersion = CINERA_APP_VERSION;
Index2.Header.HMMLVersion.Major = hmml_version.Major;
Index2.Header.HMMLVersion.Minor = hmml_version.Minor;
Index2.Header.HMMLVersion.Patch = hmml_version.Patch;
Index2.Header.EntryCount = Index1.Header.EntryCount;
Clear(Index2.Header.IndexLocation, sizeof(Index2.Header.IndexLocation));
Clear(Index2.Header.PlayerLocation, sizeof(Index2.Header.PlayerLocation));
}
case 2:
{
if(OriginalDBVersion == 2)
{
OriginalHeaderSize = sizeof(index_header2);
Index2.Header = *(index_header2 *)Index->Metadata.Buffer.Ptr;
}
Index->Header.InitialDBVersion = Index2.Header.DBVersion;
Index->Header.InitialAppVersion = Index2.Header.AppVersion;
Index->Header.InitialHMMLVersion.Major = Index2.Header.HMMLVersion.Major;
Index->Header.InitialHMMLVersion.Minor = Index2.Header.HMMLVersion.Minor;
Index->Header.InitialHMMLVersion.Patch = Index2.Header.HMMLVersion.Patch;
Index->Header.CurrentDBVersion = CINERA_DB_VERSION;
Index->Header.CurrentAppVersion = CINERA_APP_VERSION;
Index->Header.CurrentHMMLVersion.Major = hmml_version.Major;
Index->Header.CurrentHMMLVersion.Minor = hmml_version.Minor;
Index->Header.CurrentHMMLVersion.Patch = hmml_version.Patch;
Index->Header.EntryCount = Index2.Header.EntryCount;
ClearCopyStringNoFormat(Index->Header.ProjectID, sizeof(Index->Header.ProjectID), Config.ProjectID);
Clear(Index->Header.ProjectName, sizeof(Index->Header.ProjectName));
for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex)
{
if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID))
{
CopyString(Index->Header.ProjectName, sizeof(Index->Header.ProjectName), "%s", ProjectInfo[ProjectIndex].FullName);
break;
}
}
ClearCopyStringNoFormat(Index->Header.BaseURL, sizeof(Index->Header.BaseURL), Config.BaseURL);
ClearCopyStringNoFormat(Index->Header.IndexLocation, sizeof(Index->Header.IndexLocation), Index2.Header.IndexLocation);
ClearCopyStringNoFormat(Index->Header.PlayerLocation, sizeof(Index->Header.PlayerLocation), Index2.Header.PlayerLocation);
ClearCopyStringNoFormat(Index->Header.PlayerURLPrefix, sizeof(Index->Header.PlayerURLPrefix), Config.PlayerURLPrefix);
OnlyHeaderChanged = FALSE;
if(!(Index->Metadata.Handle = fopen(Index->Metadata.Path, "w"))) { FreeBuffer(&Index->Metadata.Buffer); return RC_ERROR_FILE; }
fwrite(&Index->Header, sizeof(Index->Header), 1, Index->Metadata.Handle);
Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location + OriginalHeaderSize;
Index->File.Buffer.Ptr += StringLength("---\n");
for(int EntryIndex = 0; EntryIndex < Index->Header.EntryCount; ++EntryIndex)
{
// NOTE(matt): We can use either index_metadata1 or 2 here because they are the same
index_metadata2 This = *(index_metadata2 *)Index->Metadata.Buffer.Ptr;
Index->Entry.LinkOffsets.PrevStart = 0;
Index->Entry.LinkOffsets.NextStart = 0;
Index->Entry.LinkOffsets.PrevEnd = 0;
Index->Entry.LinkOffsets.NextEnd = 0;
Index->Entry.Size = This.Size;
ClearCopyStringNoFormat(Index->Entry.BaseFilename, sizeof(Index->Entry.BaseFilename), This.BaseFilename);
char *IndexEntryStart = Index->File.Buffer.Ptr;
SeekBufferForString(&Index->File.Buffer, "title: \"", C_SEEK_FORWARDS, C_SEEK_AFTER);
Clear(Index->Entry.Title, sizeof(Index->Entry.Title));
CopyStringNoFormatT(Index->Entry.Title, sizeof(Index->Entry.Title), Index->File.Buffer.Ptr, '\n');
Index->Entry.Title[StringLength(Index->Entry.Title) - 1] = '\0';
fwrite(&Index->Entry, sizeof(Index->Entry), 1, Index->Metadata.Handle);
Index->Metadata.Buffer.Ptr += sizeof(This);
IndexEntryStart += This.Size;
Index->File.Buffer.Ptr = IndexEntryStart;
}
fclose(Index->Metadata.Handle);
FreeBuffer(&Index->Metadata.Buffer);
if(ReadFileIntoBuffer(&Index->Metadata, 0) == RC_ERROR_FILE)
{
return RC_ERROR_FILE;
}
}
}
if(OnlyHeaderChanged)
{
if(!(Index->Metadata.Handle = fopen(Index->Metadata.Path, "w"))) { FreeBuffer(&Index->Metadata.Buffer); return RC_ERROR_FILE; }
fwrite(&Index->Header, sizeof(Index->Header), 1, Index->Metadata.Handle);
fwrite(Index->Metadata.Buffer.Location + OriginalHeaderSize, Index->Metadata.FileSize - OriginalHeaderSize, 1, Index->Metadata.Handle);
fclose(Index->Metadata.Handle);
FreeBuffer(&Index->Metadata.Buffer);
if(ReadFileIntoBuffer(&Index->Metadata, 0) == RC_ERROR_FILE)
{
return RC_ERROR_FILE;
}
}
fprintf(stderr, "\n\e[1;32mUpgraded Cinera DB from %d to %d!\e[0m\n\n", OriginalDBVersion, Index->Header.CurrentDBVersion);
return RC_SUCCESS;
}
typedef struct
{
bool Present;
char ID[32];
} index_entry; // Metadata, unless we actually want to bolster this?
void
RemoveChildDirectories(buffer FullPath, char *ParentDirectory)
{
char *Ptr = FullPath.Location + StringLength(ParentDirectory);
RemoveDirectory(FullPath.Location);
while(FullPath.Ptr > Ptr)
{
if(*FullPath.Ptr == '/')
{
*FullPath.Ptr = '\0';
RemoveDirectory(FullPath.Location);
}
--FullPath.Ptr;
}
}
int
DeleteDeadIndexEntries(index *Index)
{
// TODO(matt): More rigorously figure out who we should delete
// Maybe compare the output directory and the input HMML names
Index->Header = *(index_header *)Index->Metadata.Buffer.Location;
if(Index->Header.CurrentDBVersion < CINERA_DB_VERSION)
{
if(CINERA_DB_VERSION == 4)
{
fprintf(stderr, "\n\e[1;31mHandle conversion from CINERA_DB_VERSION %d to %d!\e[0m\n\n", Index->Header.CurrentDBVersion, CINERA_DB_VERSION);
exit(RC_ERROR_FATAL);
}
if(UpgradeDB(Index) == RC_ERROR_FILE) { return RC_NOOP; }
}
else if(Index->Header.CurrentDBVersion > CINERA_DB_VERSION)
{
fprintf(stderr, "\e[1;31mUnsupported DB Version (%d). Please upgrade Cinera\e[0m\n", Index->Header.CurrentDBVersion);
exit(RC_ERROR_FATAL);
}
bool NewPlayerLocation = FALSE;
bool NewIndexLocation = FALSE;
if(StringsDiffer(Index->Header.PlayerLocation, Config.PlayerLocation))
{
buffer OldPlayerDirectory;
ClaimBuffer(&OldPlayerDirectory, "OldPlayerDirectory", 1024);
ConstructDirectoryPath(&OldPlayerDirectory, PAGE_PLAYER, Index->Header.PlayerLocation, 0);
buffer NewPlayerDirectory;
ClaimBuffer(&NewPlayerDirectory, "NewPlayerDirectory", 1024);
ConstructDirectoryPath(&NewPlayerDirectory, PAGE_PLAYER, Config.PlayerLocation, 0);
printf("\e[1;33mRelocating Player Page%s from %s to %s\e[0m\n",
Index->Header.EntryCount > 1 ? "s" : "",
OldPlayerDirectory.Location,
NewPlayerDirectory.Location);
DeclaimBuffer(&NewPlayerDirectory);
for(int EntryIndex = 0; EntryIndex < Index->Header.EntryCount; ++EntryIndex)
{
index_metadata This = *(index_metadata *)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * EntryIndex);
ConstructDirectoryPath(&OldPlayerDirectory, PAGE_PLAYER, Index->Header.PlayerLocation, This.BaseFilename);
DeletePlayerPageFromFilesystem(This.BaseFilename, Index->Header.PlayerLocation, TRUE);
}
ConstructDirectoryPath(&OldPlayerDirectory, PAGE_PLAYER, Index->Header.PlayerLocation, 0);
if(StringLength(Index->Header.PlayerLocation) > 0)
{
RemoveChildDirectories(OldPlayerDirectory, Config.BaseDir);
}
DeclaimBuffer(&OldPlayerDirectory);
ClearCopyStringNoFormat(Index->Header.PlayerLocation, sizeof(Index->Header.PlayerLocation), Config.PlayerLocation);
*(index_header *)Index->Metadata.Buffer.Location = Index->Header;
NewPlayerLocation = TRUE;
}
if(StringsDiffer(Index->Header.IndexLocation, Config.IndexLocation))
{
buffer OldIndexDirectory;
ClaimBuffer(&OldIndexDirectory, "OldIndexDirectory", 1024);
ConstructDirectoryPath(&OldIndexDirectory, PAGE_INDEX, Index->Header.IndexLocation, 0);
buffer NewIndexDirectory;
ClaimBuffer(&NewIndexDirectory, "NewIndexDirectory", 1024);
ConstructDirectoryPath(&NewIndexDirectory, PAGE_INDEX, Config.IndexLocation, 0);
printf("\e[1;33mRelocating Index Page from %s to %s\e[0m\n",
OldIndexDirectory.Location,
NewIndexDirectory.Location);
DeclaimBuffer(&NewIndexDirectory);
char IndexPagePath[2048] = { 0 };
CopyString(IndexPagePath, sizeof(IndexPagePath), "%s/index.html", OldIndexDirectory.Location);
remove(IndexPagePath);
if(StringLength(Index->Header.IndexLocation) > 0)
{
RemoveChildDirectories(OldIndexDirectory, Config.BaseDir);
}
DeclaimBuffer(&OldIndexDirectory);
ClearCopyStringNoFormat(Index->Header.IndexLocation, sizeof(Index->Header.IndexLocation), Config.IndexLocation);
*(index_header *)Index->Metadata.Buffer.Location = Index->Header;
NewIndexLocation = TRUE;
}
if(NewPlayerLocation || NewIndexLocation)
{
if(!(Index->Metadata.Handle = fopen(Index->Metadata.Path, "w"))) { FreeBuffer(&Index->Metadata.Buffer); return RC_ERROR_FILE; }
fwrite(Index->Metadata.Buffer.Location, Index->Metadata.FileSize, 1, Index->Metadata.Handle);
fclose(Index->Metadata.Handle);
}
index_entry Entries[Index->Header.EntryCount];
for(int EntryIndex = 0; EntryIndex < Index->Header.EntryCount; ++EntryIndex)
{
index_metadata This = *(index_metadata *)(Index->Metadata.Buffer.Location + sizeof(index_header) + sizeof(index_metadata) * EntryIndex);
CopyStringNoFormat(Entries[EntryIndex].ID, sizeof(Entries[EntryIndex].ID), This.BaseFilename);
Entries[EntryIndex].Present = FALSE;
}
DIR *ProjectDirHandle;
if(!(ProjectDirHandle = opendir(Config.ProjectDir)))
{
LogError(LOG_ERROR, "Unable to scan project directory %s: %s", Config.ProjectDir, strerror(errno));
fprintf(stderr, "Unable to scan project directory %s: %s\n", Config.ProjectDir, strerror(errno));
return RC_ERROR_DIRECTORY;
}
struct dirent *ProjectFiles;
while((ProjectFiles = readdir(ProjectDirHandle)))
{
char *Ptr;
Ptr = ProjectFiles->d_name;
Ptr += (StringLength(ProjectFiles->d_name) - StringLength(".hmml"));
if(!(StringsDiffer(Ptr, ".hmml")))
{
*Ptr = '\0';
for(int i = 0; i < Index->Header.EntryCount; ++i)
{
if(!StringsDiffer(Entries[i].ID, ProjectFiles->d_name))
{
Entries[i].Present = TRUE;
break;
}
}
}
}
closedir(ProjectDirHandle);
bool Deleted = FALSE;
for(int i = 0; i < Index->Header.EntryCount; ++i)
{
if(Entries[i].Present == FALSE)
{
Deleted = TRUE;
neighbourhood Neighbourhood = { };
Neighbourhood.PrevIndex = -1;
Neighbourhood.ThisIndex = -1;
Neighbourhood.NextIndex = -1;
DeleteEntry(Index, &Neighbourhood, Entries[i].ID);
}
}
return Deleted ? RC_SUCCESS : RC_NOOP;
}
int
SyncIndexWithInput(index *Index, buffers *CollationBuffers, template *IndexTemplate, template *PlayerTemplate, template *BespokeTemplate)
{
bool Deleted = FALSE;
Deleted = (Index->Metadata.FileSize > 0 && Index->File.FileSize > 0 && DeleteDeadIndexEntries(Index) == RC_SUCCESS);
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';
neighbourhood Neighbourhood = { };
Neighbourhood.PrevIndex = -1;
Neighbourhood.NextIndex = -1;
Inserted |= (InsertEntry(Index, &Neighbourhood, CollationBuffers, PlayerTemplate, BespokeTemplate, ProjectFiles->d_name, 0) == RC_SUCCESS);
}
}
closedir(ProjectDirHandle);
if(Deleted || Inserted)
{
GenerateIndexPage(Index, CollationBuffers, IndexTemplate);
}
return RC_SUCCESS;
}
void
PrintVersions()
{
curl_version_info_data *CurlVersion = curl_version_info(CURLVERSION_NOW);
printf("Cinera: %d.%d.%d\n"
"Cinera DB: %d\n"
"hmmlib: %d.%d.%d\n"
"libcurl: %s\n",
CINERA_APP_VERSION.Major, CINERA_APP_VERSION.Minor, CINERA_APP_VERSION.Patch,
CINERA_DB_VERSION,
hmml_version.Major, hmml_version.Minor, hmml_version.Patch,
CurlVersion->version);
}
int
main(int ArgC, char **Args)
{
// TODO(matt): Read all defaults from the config
config DefaultConfig = {
.RootDir = ".",
.RootURL = "",
.CSSDir = "",
.ImagesDir = "",
.JSDir = "",
.TemplatesDir = ".",
.TemplateIndexLocation = "",
.TemplatePlayerLocation = "",
.BaseDir = ".",
.BaseURL = "",
.IndexLocation = "",
.PlayerLocation = "", // Should default to the ProjectID
.Edition = EDITION_SINGLE,
.LogLevel = LOG_EMERGENCY,
.DefaultMedium = "programming",
.Mode = 0,
.OutLocation = "out.html",
.OutIntegratedLocation = "out_integrated.html",
.ProjectDir = ".",
.ProjectID = "",
.Theme = "",
.UpdateInterval = 4,
.PlayerURLPrefix = ""
};
if(getenv("XDG_CACHE_HOME"))
{
CopyString(DefaultConfig.CacheDir, sizeof(DefaultConfig.CacheDir), "%s/cinera", getenv("XDG_CACHE_HOME"));
}
else
{
CopyString(DefaultConfig.CacheDir, sizeof(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, "1a:b:B:c:d:efghi:j:l:m:n:o:p:qr:R:s:t:u:vwx:y:")) != -1)
{
switch(CommandLineArg)
{
case '1':
Config.Mode |= MODE_SINGLETAB;
break;
case 'a':
Config.PlayerLocation = StripSurroundingSlashes(optarg);
break;
case 'b':
Config.BaseDir = StripTrailingSlash(optarg);
break;
case 'B':
Config.BaseURL = StripTrailingSlash(optarg);
break;
case 'c':
Config.CSSDir = StripSurroundingSlashes(optarg);
break;
case 'd':
Config.ProjectDir = StripTrailingSlash(optarg);
break;
case 'e':
Config.Mode |= MODE_EXAMINE;
break;
case 'f':
Config.Mode |= MODE_FORCEINTEGRATION;
break;
case 'g':
Config.Mode |= MODE_NOPRIVACY;
break;
case 'i':
Config.ImagesDir = StripSurroundingSlashes(optarg);
break;
case 'j':
Config.JSDir = StripSurroundingSlashes(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 'n':
Config.IndexLocation = StripSurroundingSlashes(optarg);
break;
case 'o':
Config.OutLocation = optarg;
Config.OutIntegratedLocation = optarg;
break;
case 'p':
Config.ProjectID = optarg;
break;
case 'q':
Config.Mode |= MODE_ONESHOT;
break;
case 'r':
Config.RootDir = StripTrailingSlash(optarg);
break;
case 'R':
Config.RootURL = StripTrailingSlash(optarg);
break;
case 's':
Config.Theme = optarg;
break;
case 't':
Config.TemplatesDir = StripTrailingSlash(optarg);
break;
case 'u':
Config.UpdateInterval = StringToInt(optarg);
break;
case 'v':
PrintVersions();
return RC_SUCCESS;
case 'w':
Config.Mode |= MODE_NOCACHE;
break;
case 'x':
Config.TemplateIndexLocation = optarg;
break;
case 'y':
Config.TemplatePlayerLocation = optarg;
break;
case 'h':
default:
PrintUsage(Args[0], &DefaultConfig);
return RC_SUCCESS;
}
}
if(Config.Mode & MODE_EXAMINE)
{
index Index = { };
Index.Metadata.Buffer.ID = "IndexMetadata";
// TODO(matt): Allow optionally passing a .metadata file as an argument?
CopyString(Index.Metadata.Path, sizeof(Index.Metadata.Path), "%s/%s.metadata", Config.BaseDir, Config.ProjectID);
ExamineIndex(&Index);
exit(RC_SUCCESS);
}
// 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
// NOTE(matt): Tree structure of buffer dependencies
// IncludesPlayer
// Menus
// Player
// ScriptPlayer
//
// IncludesIndex
// Index
buffers CollationBuffers = {};
if(ClaimBuffer(&CollationBuffers.IncludesPlayer, "IncludesPlayer", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; };
if(ClaimBuffer(&CollationBuffers.Menus, "Menus", Kilobytes(32)) == RC_ARENA_FULL) { goto RIP; };
if(ClaimBuffer(&CollationBuffers.Player, "Player", Kilobytes(256)) == RC_ARENA_FULL) { goto RIP; };
if(ClaimBuffer(&CollationBuffers.ScriptPlayer, "ScriptPlayer", Kilobytes(8)) == RC_ARENA_FULL) { goto RIP; };
if(ClaimBuffer(&CollationBuffers.IncludesIndex, "IncludesIndex", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; };
if(ClaimBuffer(&CollationBuffers.Search, "Search", Kilobytes(32)) == RC_ARENA_FULL) { goto RIP; };
bool HaveConfigErrors = FALSE;
if(StringsDiffer(Config.ProjectID, ""))
{
if(StringLength(Config.ProjectID) > MAX_PROJECT_ID_LENGTH)
{
fprintf(stderr, "\e[1;31mProjectID \"%s\" is too long (%d/%d characters)\e[0m\n", Config.ProjectID, StringLength(Config.ProjectID), MAX_PROJECT_ID_LENGTH);
HaveConfigErrors = TRUE;
}
Config.Edition = EDITION_PROJECT;
bool KnownProject = FALSE;
for(int ProjectInfoIndex = 0; ProjectInfoIndex < ArrayCount(ProjectInfo); ++ProjectInfoIndex)
{
if(!StringsDiffer(Config.ProjectID, ProjectInfo[ProjectInfoIndex].ProjectID))
{
KnownProject = TRUE;
CopyStringNoFormat(CollationBuffers.ProjectID, sizeof(CollationBuffers.ProjectID), Config.ProjectID);
if(!StringsDiffer(Config.Theme, ""))
{
Config.Theme = Config.ProjectID;
}
CopyStringNoFormat(CollationBuffers.Theme, sizeof(CollationBuffers.Theme), Config.Theme);
if(StringsDiffer(ProjectInfo[ProjectInfoIndex].FullName, ""))
{
CopyStringNoFormat(CollationBuffers.ProjectName, sizeof(CollationBuffers.ProjectName), ProjectInfo[ProjectInfoIndex].FullName);
}
if(StringsDiffer(ProjectInfo[ProjectInfoIndex].Medium, ""))
{
Config.DefaultMedium = ProjectInfo[ProjectInfoIndex].Medium;
}
if(StringsDiffer(ProjectInfo[ProjectInfoIndex].AltURLPrefix, ""))
{
if(StringLength(ProjectInfo[ProjectInfoIndex].AltURLPrefix) > MAX_PLAYER_URL_PREFIX_LENGTH)
{
fprintf(stderr, "\e[1;31mPlayer URL Prefix \"%s\" is too long (%d/%d characters)\e[0m\n", ProjectInfo[ProjectInfoIndex].AltURLPrefix, StringLength(ProjectInfo[ProjectInfoIndex].AltURLPrefix), MAX_PLAYER_URL_PREFIX_LENGTH);
HaveConfigErrors = TRUE;
}
else
{
Config.PlayerURLPrefix = ProjectInfo[ProjectInfoIndex].AltURLPrefix;
}
}
if(StringLength(ProjectInfo[ProjectInfoIndex].FullName) > MAX_PROJECT_NAME_LENGTH)
{
fprintf(stderr, "\e[1;31mProject Name \"%s\" is too long (%d/%d characters)\e[0m\n", ProjectInfo[ProjectInfoIndex].FullName, StringLength(ProjectInfo[ProjectInfoIndex].FullName), MAX_PROJECT_NAME_LENGTH);
HaveConfigErrors = TRUE;
}
break;
}
}
if(!KnownProject)
{
fprintf(stderr, "\e[1;31mMissing Project Info for %s\e[0m \e[1;30m(-p)\e[0m\n", Config.ProjectID);
HaveConfigErrors = TRUE;
}
}
if(StringsDiffer(Config.BaseURL, "") && StringLength(Config.BaseURL) > MAX_BASE_URL_LENGTH)
{
fprintf(stderr, "\e[1;31mBase URL \"%s\" is too long (%d/%d characters)\e[0m\n", Config.BaseURL, StringLength(Config.BaseURL), MAX_BASE_URL_LENGTH);
HaveConfigErrors = TRUE;
}
if(StringsDiffer(Config.IndexLocation, "") && StringLength(Config.IndexLocation) > MAX_RELATIVE_PAGE_LOCATION_LENGTH)
{
fprintf(stderr, "\e[1;31mRelative Index Page Location \"%s\" is too long (%d/%d characters)\e[0m\n", Config.IndexLocation, StringLength(Config.IndexLocation), MAX_RELATIVE_PAGE_LOCATION_LENGTH);
HaveConfigErrors = TRUE;
}
if(StringsDiffer(Config.PlayerLocation, "") && StringLength(Config.PlayerLocation) > MAX_RELATIVE_PAGE_LOCATION_LENGTH)
{
fprintf(stderr, "\e[1;31mRelative Player Page Location \"%s\" is too long (%d/%d characters)\e[0m\n", Config.PlayerLocation, StringLength(Config.PlayerLocation), MAX_RELATIVE_PAGE_LOCATION_LENGTH);
HaveConfigErrors = TRUE;
}
if(!MediumExists(Config.DefaultMedium))
{
// TODO(matt): We'll want to stick around when we have multiple projects configured
HaveConfigErrors = TRUE;
}
if(HaveConfigErrors) { exit(RC_RIP); }
// 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 Init and Validate it
// In our case here, we just want to straight up Init our three possible templates
// and Validate the Index and Player templates if their locations are set
template *IndexTemplate; InitTemplate(&IndexTemplate);
template *PlayerTemplate; InitTemplate(&PlayerTemplate);
template *BespokeTemplate; InitTemplate(&BespokeTemplate);
if(StringsDiffer(Config.TemplatePlayerLocation, ""))
{
switch(ValidateTemplate(&PlayerTemplate, Config.TemplatePlayerLocation, TEMPLATE_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 && StringsDiffer(Config.TemplateIndexLocation, ""))
{
switch(ValidateTemplate(&IndexTemplate, Config.TemplateIndexLocation, TEMPLATE_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;
}
}
if(Config.Edition == EDITION_PROJECT)
{
#if DEBUG_MEM
FILE *MemLog = fopen("/home/matt/cinera_mem", "w+");
fprintf(MemLog, "Entered Project Edition\n");
fclose(MemLog);
#endif
// TODO(matt): Also log these startup messages?
PrintVersions();
printf( "\n"
"Universal\n"
" Cache Directory: \e[1;30m(XDG_CACHE_HOME)\e[0m\t%s\n"
"\n"
" Root\n"
" Directory: \e[1;30m(-r)\e[0m\t\t\t%s\n"
" URL: \e[1;30m(-R)\e[0m\t\t\t%s\n"
" Paths relative to root\n"
" CSS: \e[1;30m(-c)\e[0m\t\t\t%s\n"
" Images: \e[1;30m(-i)\e[0m\t\t\t%s\n"
" JS: \e[1;30m(-j)\e[0m\t\t\t%s\n"
"\n"
"Project\n"
" ID: \e[1;30m(-p)\e[0m\t\t\t\t%s\n"
" Default Medium: \e[1;30m(-m)\e[0m\t\t%s\n"
" Style / Theme: \e[1;30m(-s)\e[0m\t\t\t%s\n"
"\n"
"Input Paths\n"
" Annotations Directory: \e[1;30m(-d)\e[0m\t\t%s\n"
" Templates Directory: \e[1;30m(-t)\e[0m\t\t%s\n"
" Index Template: \e[1;30m(-x)\e[0m\t\t%s\n"
" Player Template: \e[1;30m(-y)\e[0m\t\t%s\n"
"\n"
"Output Paths\n"
" Base\n"
" Directory: \e[1;30m(-b)\e[0m\t\t\t%s\n"
" URL: \e[1;30m(-B)\e[0m\t\t\t%s\n"
" Paths relative to base\n"
" Index Page: \e[1;30m(-n)\e[0m\t\t\t%s\n"
/* NOTE(matt): Here, I think, is where we'll split into sub-projects (...really?...) */
" Player Page(s): \e[1;30m(-a)\e[0m\t\t%s\n"
" Player Page Prefix: \e[1;30m(hardcoded)\e[0m\t%s\n"
"\n"
"Modes\n"
" Single browser tab: \e[1;30m(-1)\e[0m\t\t%s\n"
" Force template integration: \e[1;30m(-f)\e[0m\t%s\n"
" Ignore video privacy status: \e[1;30m(-g)\e[0m\t%s\n"
" Quit after sync: \e[1;30m(-q)\e[0m\t\t%s\n"
" Force quote cache rebuild: \e[1;30m(-w)\e[0m\t%s\n"
"\n",
Config.CacheDir,
Config.RootDir,
StringsDiffer(Config.RootURL, "") ? Config.RootURL : "[empty]",
StringsDiffer(Config.CSSDir, "") ? Config.CSSDir : "(same as root)",
StringsDiffer(Config.ImagesDir, "") ? Config.ImagesDir : "(same as root)",
StringsDiffer(Config.JSDir, "") ? Config.JSDir : "(same as root)",
Config.ProjectID,
Config.DefaultMedium,
Config.Theme,
Config.ProjectDir,
Config.TemplatesDir,
StringsDiffer(Config.TemplateIndexLocation, "") ? Config.TemplateIndexLocation : "[none set]",
StringsDiffer(Config.TemplatePlayerLocation, "") ? Config.TemplatePlayerLocation : "[none set]",
Config.BaseDir,
StringsDiffer(Config.BaseURL, "") ? Config.BaseURL : "[empty]",
StringsDiffer(Config.IndexLocation, "") ? Config.IndexLocation : "(same as base)",
StringsDiffer(Config.PlayerLocation, "") ? Config.PlayerLocation : "(directly descended from base)",
StringsDiffer(Config.PlayerURLPrefix, "") ? Config.PlayerURLPrefix : Config.ProjectID,
Config.Mode & MODE_SINGLETAB ? "on" : "off",
Config.Mode & MODE_FORCEINTEGRATION ? "on" : "off",
Config.Mode & MODE_NOPRIVACY ? "on" : "off",
Config.Mode & MODE_ONESHOT ? "on" : "off",
Config.Mode & MODE_NOCACHE ? "on" : "off");
if((StringsDiffer(Config.IndexLocation, "") || StringsDiffer(Config.PlayerLocation, ""))
&& StringLength(Config.BaseURL) == 0)
{
printf("\e[1;33mPlease set a Project Base URL (-B) so we can output the Index / Player pages to\n"
"locations other than the defaults\e[0m\n");
return(RC_SUCCESS);
}
index Index = { };
Index.Metadata.Buffer.ID = "IndexMetadata";
CopyString(Index.Metadata.Path, sizeof(Index.Metadata.Path), "%s/%s.metadata", Config.BaseDir, Config.ProjectID);
ReadFileIntoBuffer(&Index.Metadata, 0); // NOTE(matt): Could we actually catch errors (permissions?) here and bail?
Index.File.Buffer.ID = "IndexFile";
CopyString(Index.File.Path, sizeof(Index.File.Path), "%s/%s.index", Config.BaseDir, Config.ProjectID);
ReadFileIntoBuffer(&Index.File, 0); // NOTE(matt): Could we actually catch errors (permissions?) here and bail?
printf("┌╼ Synchronising with Annotations Directory ╾┐\n");
SyncIndexWithInput(&Index, &CollationBuffers, IndexTemplate, PlayerTemplate, BespokeTemplate);
if(Config.Mode & MODE_ONESHOT)
{
goto RIP;
}
printf("\n┌╼ Monitoring Annotations Directory for \e[1;32mnew\e[0m, \e[1;33medited\e[0m and \e[1;30mdeleted\e[0m .hmml files ╾┐\n");
int inotifyInstance = inotify_init1(IN_NONBLOCK);
// NOTE(matt): Do we want to also watch IN_DELETE_SELF events?
int WatchDescriptor = inotify_add_watch(inotifyInstance, Config.ProjectDir, IN_CLOSE_WRITE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO);
while(MonitorDirectory(&Index, &CollationBuffers, IndexTemplate, PlayerTemplate, BespokeTemplate, inotifyInstance, WatchDescriptor) != RC_ARENA_FULL)
{
// TODO(matt): Refetch the quotes and rebuild player pages if needed
//
// Every sixty mins, redownload the quotes and, I suppose, SyncIndexWithInput(). But here we still don't even know
// who the speaker is. To know, we'll probably have to store all quoted speakers in the project's .metadata. Maybe
// postpone this for now, but we will certainly need this to happen
//
// The most ideal solution is possibly that we store quote numbers in the Metadata->Entry, listen for and handle a
// REST PUT request from insobot when a quote changes (unless we're supposed to poll insobot for them?), and rebuild
// the player page(s) accordingly.
//
if(!(Config.Mode & MODE_NOPRIVACY) && time(0) - LastPrivacyCheck > 60 * 60 * 4)
{
RecheckPrivacy(&Index, &CollationBuffers, IndexTemplate, PlayerTemplate, BespokeTemplate);
}
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:
// TODO(matt): Just change the default output location so all these guys won't overwrite each other
for(int FileIndex = optind; FileIndex < ArgC; ++FileIndex)
{
bool HasBespokeTemplate = FALSE;
char *Ptr = Args[FileIndex];
Ptr += (StringLength(Args[FileIndex]) - StringLength(".hmml"));
if(!(StringsDiffer(Ptr, ".hmml")))
{
CopyString(Config.SingleHMMLFilePath, sizeof(Config.SingleHMMLFilePath), "%s", Args[FileIndex]);
switch(HMMLToBuffers(&CollationBuffers, &BespokeTemplate, Args[FileIndex], 0))
{
// TODO(matt): Actually sort out the fatality of these cases
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;
};
HasBespokeTemplate = StringsDiffer(BespokeTemplate->Metadata.Filename, "");
switch(BuffersToHTML(&CollationBuffers,
HasBespokeTemplate ? BespokeTemplate : PlayerTemplate,
0,
PAGE_PLAYER, 0))
{
// TODO(matt): Actually sort out the fatality of these cases
case RC_INVALID_TEMPLATE:
if(HasBespokeTemplate) { DeclaimTemplate(BespokeTemplate); }
if(FileIndex < (ArgC - 1)) { goto NextFile; }
case RC_ERROR_MEMORY:
case RC_ERROR_FILE:
case RC_ARENA_FULL:
goto RIP;
case RC_SUCCESS:
#if 0
fprintf(stdout, "\e[1;32mWritten\e[0m %s\n", HasBespokeTemplate ? Config.OutIntegratedLocation : Config.OutLocation);
#endif
if(HasBespokeTemplate) { DeclaimTemplate(BespokeTemplate); }
break;
};
}
}
}
if(StringsDiffer(PlayerTemplate->Metadata.Filename, ""))
{
DeclaimTemplate(PlayerTemplate);
}
if(Config.Edition == EDITION_PROJECT && StringsDiffer(IndexTemplate->Metadata.Filename, ""))
{
DeclaimTemplate(IndexTemplate);
}
DeclaimBuffer(&CollationBuffers.Search);
DeclaimBuffer(&CollationBuffers.IncludesIndex);
DeclaimBuffer(&CollationBuffers.ScriptPlayer);
DeclaimBuffer(&CollationBuffers.Player);
DeclaimBuffer(&CollationBuffers.Menus);
DeclaimBuffer(&CollationBuffers.IncludesPlayer);
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
}