Annotation-System/cinera/cinera.c

9185 lines
355 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
#include <stdint.h>
typedef struct
{
uint32_t Major, Minor, Patch;
} version;
version CINERA_APP_VERSION = {
.Major = 0,
.Minor = 6,
.Patch = 1
};
#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 <wordexp.h>
#define __USE_XOPEN2K8 // NOTE(matt): O_NOFOLLOW
#include <fcntl.h> // NOTE(matt): open()
#define __USE_XOPEN2K // NOTE(matt): readlink()
#include <unistd.h> // NOTE(matt): sleep()
typedef unsigned int bool;
#define TRUE 1
#define FALSE 0
#define enum8(type) int8_t
#define enum16(type) int16_t
#define enum32(type) int32_t
#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_DIR_LENGTH 127
#define MAX_BASE_URL_LENGTH 127
#define MAX_RELATIVE_PAGE_LOCATION_LENGTH 31
#define MAX_PLAYER_URL_PREFIX_LENGTH 15
#define MAX_ROOT_DIR_LENGTH 127
#define MAX_ROOT_URL_LENGTH 127
#define MAX_RELATIVE_ASSET_LOCATION_LENGTH 31
#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 db_entry is 128 bytes total
#define MAX_ASSET_FILENAME_LENGTH 63
// TODO(matt): Stop distinguishing between short / long and lift the size limit once we're on the LUT
#define MAX_CUSTOM_SNIPPET_SHORT_LENGTH 255
#define MAX_CUSTOM_SNIPPET_LONG_LENGTH 1023
#define ArrayCount(A) sizeof(A)/sizeof(*(A))
#define Assert(Expression) if(!(Expression)) { printf("l.%d: \e[1;31mAssertion failure\e[0m\n", __LINE__); __asm__("int3"); }
#define FOURCC(String) ((uint32_t)(String[0] << 0) | (uint32_t)(String[1] << 8) | (uint32_t)(String[2] << 16) | (uint32_t)(String[3] << 24))
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,
MODE_NOREVVEDRESOURCE = 1 << 6
} 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_PARSING,
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];
enum8(editions) Edition;
enum8(log_levels) LogLevel;
enum8(modes) 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}
char *QueryString;
// Per Project
char *ProjectID;
char *Theme;
char *DefaultMedium;
// Per Project - Input
char *ProjectDir; // Absolute
char *TemplatesDir; // Absolute
char *TemplateSearchLocation; // Relative to TemplatesDir ???
char *TemplatePlayerLocation; // Relative to TemplatesDir ???
// Per Project - Output
char *BaseDir; // Absolute
char *BaseURL;
char *SearchLocation; // 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;
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;
char *AssetTypeNames[] =
{
"Generic",
"CSS",
"Image",
"JavaScript"
};
enum
{
ASSET_GENERIC,
ASSET_CSS,
ASSET_IMG,
ASSET_JS,
ASSET_TYPE_COUNT
} asset_types;
typedef struct asset
{
int32_t Hash;
enum8(asset_types) Type;
char Filename[MAX_ASSET_FILENAME_LENGTH + 1];
int32_t FilenameAt:29;
int32_t Known:1;
int32_t OffsetLandmarks:1;
int32_t DeferredUpdate:1;
uint32_t SearchLandmarkCapacity;
uint32_t SearchLandmarkCount;
uint32_t *SearchLandmark;
uint32_t PlayerLandmarkCapacity;
uint32_t PlayerLandmarkCount;
uint32_t *PlayerLandmark;
} asset;
asset BuiltinAssets[] =
{
{ 0, ASSET_CSS, "cinera.css" },
{ 0, ASSET_CSS }, // NOTE(matt): .Filename set by InitBuiltinAssets()
{ 0, ASSET_CSS, "cinera_topics.css" },
{ 0, ASSET_IMG, "cinera_icon_filter.png" },
{ 0, ASSET_JS, "cinera_search.js" },
{ 0, ASSET_JS, "cinera_player_pre.js" },
{ 0, ASSET_JS, "cinera_player_post.js" },
};
enum
{
ASSET_CSS_CINERA,
ASSET_CSS_THEME,
ASSET_CSS_TOPICS,
ASSET_IMG_FILTER,
ASSET_JS_SEARCH,
ASSET_JS_PLAYER_PRE,
ASSET_JS_PLAYER_POST,
BUILTIN_ASSETS_COUNT,
} builtin_assets;
typedef struct
{
int Count;
int Capacity;
asset *Asset;
} assets;
enum
{
WT_HMML,
WT_ASSET
} watch_types;
typedef struct
{
int Descriptor;
enum8(watch_types) Type;
char Path[MAX_ROOT_DIR_LENGTH + 1 + MAX_RELATIVE_ASSET_LOCATION_LENGTH];
} watch_handle;
typedef struct
{
int Count;
int Capacity;
watch_handle *Handle;
} watch_handles;
// DBVersion 1
typedef struct { unsigned int DBVersion; version AppVersion; version HMMLVersion; unsigned int EntryCount; } db_header1;
typedef struct { int Size; char BaseFilename[32]; } db_entry1;
typedef struct { file_buffer File; file_buffer Metadata; db_header1 Header; db_entry1 Entry; } database1;
//
// DBVersion 2
typedef struct { unsigned int DBVersion; version AppVersion; version HMMLVersion; unsigned int EntryCount; char SearchLocation[32]; char PlayerLocation[32]; } db_header2;
typedef db_entry1 db_entry2;
typedef struct { file_buffer File; file_buffer Metadata; db_header2 Header; db_entry2 Entry; } database2;
//
// TODO(matt): Increment CINERA_DB_VERSION!
typedef struct
{
unsigned int CurrentDBVersion;
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 SearchLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1];
char PlayerLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1];
char PlayerURLPrefix[MAX_PLAYER_URL_PREFIX_LENGTH + 1];
} db_header3;
typedef struct
{
unsigned int PrevStart, NextStart;
unsigned short int PrevEnd, NextEnd;
} link_insertion_offsets; // NOTE(matt): PrevStart is Absolute (or relative to start of file), the others are Relative to PrevStart
typedef struct
{
link_insertion_offsets LinkOffsets;
unsigned short int Size;
char BaseFilename[MAX_BASE_FILENAME_LENGTH + 1];
char Title[MAX_TITLE_LENGTH + 1];
} db_entry3;
typedef struct
{
file_buffer File;
file_buffer Metadata;
db_header3 Header;
db_entry3 Entry;
} database3;
#pragma pack(push, 1)
typedef struct
{
uint32_t HexSignature; // 'CNRA'
uint32_t CurrentDBVersion;
version CurrentAppVersion;
version CurrentHMMLVersion;
uint32_t InitialDBVersion;
version InitialAppVersion;
version InitialHMMLVersion;
uint32_t BlockCount;
} db_header4;
typedef struct
{
uint32_t BlockID; // 'NTRY'
uint16_t Count;
char ProjectID[MAX_PROJECT_ID_LENGTH + 1];
char ProjectName[MAX_PROJECT_NAME_LENGTH + 1];
char BaseURL[MAX_BASE_URL_LENGTH + 1];
char SearchLocation[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
} db_header_entries4;
typedef db_entry3 db_entry4;
typedef struct
{
uint32_t BlockID; // 'ASET'
uint16_t Count;
char RootDir[MAX_ROOT_DIR_LENGTH + 1];
char RootURL[MAX_ROOT_URL_LENGTH + 1];
char CSSDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH + 1];
char ImagesDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH + 1];
char JSDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH + 1];
} db_header_assets4;
typedef struct
{
int32_t Hash;
uint32_t LandmarkCount;
enum8(asset_types) Type;
char Filename[MAX_ASSET_FILENAME_LENGTH + 1];
} db_asset4;
typedef struct
{
int32_t EntryIndex;
uint32_t Position;
} db_landmark4;
typedef struct
{
file_buffer File;
file_buffer Metadata;
db_header4 Header;
db_header_entries4 EntriesHeader;
db_entry4 Entry;
db_header_assets4 AssetsHeader;
db_asset4 Asset;
db_landmark4 Landmark;
} database4;
#pragma pack(pop)
#define CINERA_DB_VERSION 4
#define db_header db_header4
#define db_header_entries db_header_entries4
#define db_entry db_entry4
#define db_header_assets db_header_assets4
#define db_asset db_asset4
#define db_landmark db_landmark4
#define database database4
// TODO(matt): Increment CINERA_DB_VERSION!
// NOTE(matt): Globals
arena MemoryArena;
config Config;
assets Assets;
int inotifyInstance;
watch_handles WatchHandles;
database DB;
time_t LastPrivacyCheck;
time_t LastQuoteFetch;
//
enum
{
PAGE_TYPE_SEARCH = -1,
} page_type_indices;
typedef struct
{
buffer IncludesSearch;
buffer SearchEntry;
buffer Search; // 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 URLSearch[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];
char VODPlatform[16];
} buffers;
enum
{
// Contents and Player Pages Mandatory
TAG_INCLUDES,
// Contents Page Mandatory
TAG_SEARCH,
// 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,
TAG_VOD_PLATFORM,
// Anywhere Optional
TAG_ASSET,
TAG_CSS,
TAG_IMAGE,
TAG_JS,
TAG_PROJECT,
TAG_PROJECT_ID,
TAG_SEARCH_URL,
TAG_THEME,
TAG_URL,
TEMPLATE_TAG_COUNT,
} template_tag_codes;
char *TemplateTags[] = {
"__CINERA_INCLUDES__",
"__CINERA_SEARCH__",
"__CINERA_MENUS__",
"__CINERA_PLAYER__",
"__CINERA_SCRIPT__",
"__CINERA_CUSTOM0__",
"__CINERA_CUSTOM1__",
"__CINERA_CUSTOM2__",
"__CINERA_CUSTOM3__",
"__CINERA_CUSTOM4__",
"__CINERA_CUSTOM5__",
"__CINERA_CUSTOM6__",
"__CINERA_CUSTOM7__",
"__CINERA_CUSTOM8__",
"__CINERA_CUSTOM9__",
"__CINERA_CUSTOM10__",
"__CINERA_CUSTOM11__",
"__CINERA_CUSTOM12__",
"__CINERA_CUSTOM13__",
"__CINERA_CUSTOM14__",
"__CINERA_CUSTOM15__",
"__CINERA_TITLE__",
"__CINERA_VIDEO_ID__",
"__CINERA_VOD_PLATFORM__",
"__CINERA_ASSET__",
"__CINERA_CSS__",
"__CINERA_IMAGE__",
"__CINERA_JS__",
"__CINERA_PROJECT__",
"__CINERA_PROJECT_ID__",
"__CINERA_SEARCH_URL__",
"__CINERA_THEME__",
"__CINERA_URL__",
};
typedef struct
{
int Offset;
uint32_t AssetIndex;
enum8(template_tag_codes) TagCode;
} tag_offset;
typedef struct
{
int Validity; // NOTE(matt): Bitmask describing which page the template is valid for, i.e. contents and / or player page
int TagCapacity;
int TagCount;
tag_offset *Tags;
} template_metadata;
typedef struct
{
file_buffer File;
template_metadata Metadata;
} 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 MAX_REF_IDENTIFIER_COUNT 64
typedef struct
{
char RefTitle[620];
char ID[512];
char URL[512];
char Source[256];
identifier Identifier[MAX_REF_IDENTIFIER_COUNT];
int IdentifierCount;
} ref_info;
typedef struct
{
char Marker[32];
char WrittenText[32];
} category_info;
typedef struct
{
category_info Category[64];
int Count;
} categories;
char *SupportIcons[] =
{
"cinera_sprite_patreon.png",
"cinera_sprite_sendowl.png",
};
typedef enum
{
ICON_PATREON = BUILTIN_ASSETS_COUNT,
ICON_SENDOWL,
SUPPORT_ICON_COUNT,
} support_icons;
// TODO(matt): Parse this stuff out of a config file
typedef struct
{
char *Username;
char *CreditedName;
char *HomepageURL;
enum8(support_icons) SupportIconIndex;
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/", ICON_PATREON, "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/", ICON_SENDOWL, "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", ICON_SENDOWL, "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/", ICON_PATREON, "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/", ICON_PATREON, "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", ICON_PATREON, "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"},
{ "blackboard", "&#128396;", "Blackboard"},
{ "drawing", "&#127912;", "Drawing"},
{ "experience", "&#127863;", "Experience"},
{ "hat", "&#127913;", "Hat"},
{ "multimedia", "&#127916;", "Media Clip"},
{ "owl", "&#129417;", "Owl of Shame"},
{ "programming", "&#128430;", "Programming"},
{ "rant", "&#128162;", "Rant"},
{ "research", "&#128214;", "Research"},
{ "run", "&#127939;", "In-Game"}, // TODO(matt): Potentially make this written name 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
enum8(numbering_schemes) 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", "" },
};
char *ColourStrings[] =
{
"\e[0m",
"\e[0;33m",
"\e[0;34m",
"\e[0;35m", "\e[0;35m",
"\e[1;30m", "\e[1;30m",
"\e[1;31m",
"\e[1;32m", "\e[1;32m",
"\e[1;33m",
};
enum
{
CS_END,
CS_WARNING,
CS_PRIVATE,
CS_ONGOING, CS_PURPLE,
CS_COMMENT, CS_DELETION,
CS_ERROR,
CS_ADDITION, CS_SUCCESS,
CS_REINSERTION,
} colour_strings;
typedef struct
{
char *Name;
enum8(colour_strings) Colour;
} edit_type;
enum
{
EDIT_INSERTION,
EDIT_APPEND,
EDIT_REINSERTION,
EDIT_DELETION,
EDIT_ADDITION,
} edit_types;
edit_type EditTypes[] =
{
{ "Inserted", CS_ADDITION },
{ "Appended", CS_ADDITION },
{ "Reinserted", CS_REINSERTION },
{ "Deleted", CS_DELETION },
{ "Added", CS_ADDITION }
};
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 CopyStringToBufferNoFormatL(Dest, Length, String) CopyStringToBufferNoFormatL_(__LINE__, Dest, Length, String)
void
CopyStringToBufferNoFormatL_(int LineNumber, buffer *Dest, int Length, char *String)
{
char *Start = String;
for(int i = 0; i < Length; ++i)
{
*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");
}
*Dest->Ptr = '\0';
}
#define CopyStringToBufferHTMLPercentEncoded(Dest, String) CopyStringToBufferHTMLPercentEncoded_(__LINE__, Dest, String)
void
CopyStringToBufferHTMLPercentEncoded_(int LineNumber, buffer *Dest, char *String)
{
char *Start = String;
int Length = StringLength(String);
while(*String)
{
switch(*String)
{
case ' ': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '0'; Length += 2; break;
case '\"': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '2'; Length += 2; break;
case '%': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '5'; Length += 2; break;
case '&': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '6'; Length += 2; break;
case '<': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = 'C'; Length += 2; break;
case '>': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = 'E'; Length += 2; break;
case '?': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = 'F'; Length += 2; break;
case '\\': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '5'; *Dest->Ptr++ = 'C'; Length += 2; break;
case '^': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '5'; *Dest->Ptr++ = 'E'; Length += 2; break;
case '`': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '6'; *Dest->Ptr++ = '0'; Length += 2; break;
case '{': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '7'; *Dest->Ptr++ = 'B'; Length += 2; break;
case '|': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '7'; *Dest->Ptr++ = 'C'; Length += 2; break;
case '}': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '7'; *Dest->Ptr++ = 'D'; Length += 2; break;
default: *Dest->Ptr++ = *String; break;
}
++String;
}
if(Dest->Ptr - Dest->Location >= Dest->Size)
{
fprintf(stderr, "CopyStringToBufferHTMLPercentEncodedL(%s) call on line %d cannot accommodate %d(+1)-character percent-encoded string:\n"
"%s\n", Dest->ID, LineNumber, Length, Start);
__asm__("int3");
}
*Dest->Ptr = '\0';
}
#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)
{
// NOTE(matt): Similar to CopyBuffer(), just without null-terminating
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;
}
}
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,
enum8(seek_directions) Direction,
enum8(seek_positions) Position)
{
// 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;
}
int
StripComponentFromPath(char *Path)
{
char *Ptr = Path + StringLength(Path) - 1;
if(Ptr < Path) { return RC_ERROR_DIRECTORY; }
while(Ptr > Path && *Ptr != '/')
{
--Ptr;
}
*Ptr = '\0';
return RC_SUCCESS;
}
int ClaimBuffer(buffer *Buffer, char *ID, int Size);
void DeclaimBuffer(buffer *Buffer);
int
ResolvePath(char *Path)
{
buffer B;
ClaimBuffer(&B, "ResolvedPath", StringLength(Path) + 1);
CopyStringToBufferNoFormat(&B, Path);
B.Ptr = B.Location;
while(SeekBufferForString(&B, "/../", C_SEEK_FORWARDS, C_SEEK_END) == RC_SUCCESS)
{
char *NextComponentHead = B.Ptr;
int RemainingChars = StringLength(NextComponentHead);
--B.Ptr;
SeekBufferForString(&B, "/", C_SEEK_BACKWARDS, C_SEEK_BEFORE);
SeekBufferForString(&B, "/", C_SEEK_BACKWARDS, C_SEEK_START);
CopyStringToBufferNoFormat(&B, NextComponentHead);
Clear(B.Ptr, B.Size - (B.Ptr - B.Location));
B.Ptr -= RemainingChars;
}
CopyStringNoFormat(Path, StringLength(Path) + 1, B.Location);
DeclaimBuffer(&B);
return RC_SUCCESS;
}
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;
}
if(StripComponentFromPath(Path) == RC_ERROR_DIRECTORY) { return RC_ERROR_DIRECTORY; }
++Ancestors;
}
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);
}
}
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;
}
void
FreeBuffer(buffer *Buffer)
{
free(Buffer->Location);
Buffer->Location = 0;
Buffer->Ptr = 0;
Buffer->Size = 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
Buffer->ID = 0;
}
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 miblodelcarpio@gmail.com
LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", Buffer->ID, PercentageUsed);
fprintf(stderr, "%sWarning%s: %s used %.2f%% of its allotted memory\n",
ColourStrings[CS_ERROR], ColourStrings[CS_END],
Buffer->ID, PercentageUsed);
}
else if(PercentageUsed >= 80.0f)
{
// TODO(matt): Implement either dynamically growing buffers, or phoning home to miblodelcarpio@gmail.com
LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", Buffer->ID, PercentageUsed);
fprintf(stderr, "%sWarning%s: %s used %.2f%% of its allotted memory\n",
ColourStrings[CS_WARNING], ColourStrings[CS_END],
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_SEARCH,
TEMPLATE_PLAYER,
TEMPLATE_BESPOKE
} template_types;
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, enum8(template_types) Type)
{
// NOTE(matt): Bespoke template paths are set relative to:
// in Project Edition: ProjectDir
// in Single Edition: Parent directory of .hmml file
if(Template->File.Path[0] != '/')
{
char Temp[256];
CopyString(Temp, sizeof(Temp), "%s", Template->File.Path);
char *Ptr = Template->File.Path;
char *End = Template->File.Path + sizeof(Template->File.Path);
if(Type == 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);
}
}
void
FitTemplateTag(template *Template)
{
int BlockSize = 16;
if(Template->Metadata.TagCount == Template->Metadata.TagCapacity)
{
Template->Metadata.TagCapacity += BlockSize;
if(Template->Metadata.Tags)
{
Template->Metadata.Tags = realloc(Template->Metadata.Tags, Template->Metadata.TagCapacity * sizeof(*Template->Metadata.Tags));
}
else
{
Template->Metadata.Tags = calloc(Template->Metadata.TagCapacity, sizeof(*Template->Metadata.Tags));
}
}
}
void
PushTemplateTag(template *Template, int Offset, enum8(template_tag_types) TagType, int AssetIndex)
{
FitTemplateTag(Template);
Template->Metadata.Tags[Template->Metadata.TagCount].Offset = Offset;
Template->Metadata.Tags[Template->Metadata.TagCount].TagCode = TagType;
Template->Metadata.Tags[Template->Metadata.TagCount].AssetIndex = AssetIndex;
++Template->Metadata.TagCount;
}
void
ClearTemplateMetadata(template *Template)
{
Template->Metadata.TagCapacity = 0;
Template->Metadata.TagCount = 0;
Template->Metadata.Validity = 0;
}
void
InitTemplate(template *Template, char *Location, enum8(template_types) Type)
{
CopyStringNoFormat(Template->File.Path, sizeof(Template->File.Path), Location);
ConstructTemplatePath(Template, Type);
ReadFileIntoBuffer(&Template->File, 0);
ClearTemplateMetadata(Template);
}
void
FreeTemplate(template *Template)
{
FreeBuffer(&Template->File.Buffer);
Clear(Template->File.Path, sizeof(Template->File.Path));
Template->File.FileSize = 0;
free(Template->Metadata.Tags);
Template->Metadata.Tags = 0;
ClearTemplateMetadata(Template);
}
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
{
PAGE_PLAYER = 1 << 0,
PAGE_SEARCH = 1 << 1
} pages;
void
ConstructURLPrefix(buffer *URLPrefix, enum8(asset_types) AssetType, enum8(pages) PageType)
{
RewindBuffer(URLPrefix);
if(StringsDiffer(Config.RootURL, ""))
{
CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config.RootURL);
CopyStringToBuffer(URLPrefix, "/");
}
else
{
if(Config.Edition == EDITION_PROJECT)
{
if(PageType == PAGE_PLAYER)
{
CopyStringToBuffer(URLPrefix, "../");
}
CopyStringToBuffer(URLPrefix, "../");
}
}
switch(AssetType)
{
case ASSET_CSS:
if(StringsDiffer(Config.CSSDir, ""))
{
CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config.CSSDir);
CopyStringToBuffer(URLPrefix, "/");
}
break;
case ASSET_IMG:
if(StringsDiffer(Config.ImagesDir, ""))
{
CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config.ImagesDir);
CopyStringToBuffer(URLPrefix, "/");
}
break;
case ASSET_JS:
if(StringsDiffer(Config.JSDir, ""))
{
CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config.JSDir);
CopyStringToBuffer(URLPrefix, "/");
}
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;
size_t
StringToFletcher32(const char *Data, size_t Length)
{// https://en.wikipedia.org/wiki/Fletcher%27s_checksum
size_t c0, c1;
unsigned short int i;
for(c0 = c1 = 0; Length >= 360; Length -= 360)
{
for(i = 0; i < 360; ++i)
{
c0 += *Data++;
c1 += c0;
}
c0 %= 65535;
c1 %= 65535;
}
for(i = 0; i < Length; ++i)
{
c0 += *Data++;
c1 += c0;
}
c0 %= 65535;
c1 %= 65535;
return (c1 << 16 | c0);
}
void
ConstructAssetPath(file_buffer *AssetFile, char *Filename, int Type)
{
buffer Path;
ClaimBuffer(&Path, "Path", Kilobytes(4));
if(StringsDiffer(Config.RootDir, ""))
{
CopyStringToBuffer(&Path, "%s", Config.RootDir);
}
char *AssetDir = 0;
switch(Type)
{
case ASSET_CSS: AssetDir = Config.CSSDir; break;
case ASSET_IMG: AssetDir = Config.ImagesDir; break;
case ASSET_JS: AssetDir = Config.JSDir; break;
}
if(AssetDir && StringsDiffer(AssetDir, "")) { CopyStringToBuffer(&Path, "/%s", AssetDir); }
if(Filename) { CopyStringToBuffer(&Path, "/%s", Filename); }
CopyString(AssetFile->Path, sizeof(AssetFile->Path), Path.Location);
DeclaimBuffer(&Path);
}
void
CycleFile(file_buffer *File)
{
fclose(File->Handle);
// TODO(matt): Rather than freeing the buffer, why not just realloc it to fit the new file? Couldn't that work easily?
// The reason we Free / Reread is to save us having to shuffle the buffer contents around, basically
// If we switch the whole database over to use linked lists - the database being the only file we actually cycle
// at the moment - then we may actually obviate the need to cycle at all
FreeBuffer(&File->Buffer);
ReadFileIntoBuffer(File, 0);
}
void
ConstructDirectoryPath(buffer *DirectoryPath, int PageType, char *PageLocation, char *BaseFilename)
{
RewindBuffer(DirectoryPath);
CopyStringToBuffer(DirectoryPath, "%s", Config.BaseDir);
switch(PageType)
{
case PAGE_SEARCH:
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;
}
}
int
ReadSearchPageIntoBuffer(file_buffer *File)
{
buffer SearchPagePath;
ClaimBuffer(&SearchPagePath, "SearchPagePath", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + 10);
ConstructDirectoryPath(&SearchPagePath, PAGE_SEARCH, Config.SearchLocation, 0);
CopyString(File->Path, sizeof(File->Path), "%s/index.html", SearchPagePath.Location);
DeclaimBuffer(&SearchPagePath);
return(ReadFileIntoBuffer(File, 0));
}
int
ReadPlayerPageIntoBuffer(file_buffer *File, db_entry *Entry)
{
buffer PlayerPagePath;
ClaimBuffer(&PlayerPagePath, "PlayerPagePath", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10);
ConstructDirectoryPath(&PlayerPagePath, PAGE_PLAYER, Config.PlayerLocation, Entry->BaseFilename);
CopyString(File->Path, sizeof(File->Path), "%s/index.html", PlayerPagePath.Location);
DeclaimBuffer(&PlayerPagePath);
return(ReadFileIntoBuffer(File, 0));
}
void
ClearTerminalRow(int Length)
{
fprintf(stderr, "\r");
for(int i = 0; i < Length; ++i)
{
fprintf(stderr, " ");
}
fprintf(stderr, "\r");
}
typedef struct
{
uint32_t First;
uint32_t Length;
} landmark_range;
uint32_t
GetIndexRangeLength(void *FirstLandmark, int EntryIndex, int LandmarkIndex, int LandmarkCount)
{
uint32_t Result = 1;
db_landmark Landmark = *(db_landmark*)(FirstLandmark + sizeof(Landmark) * LandmarkIndex);
while(EntryIndex == Landmark.EntryIndex && LandmarkIndex < LandmarkCount - 1)
{
++LandmarkIndex;
Landmark = *(db_landmark*)(FirstLandmark + sizeof(Landmark) * LandmarkIndex);
if(EntryIndex == Landmark.EntryIndex)
{
++Result;
}
}
return Result;
}
landmark_range
GetIndexRange(void *FirstLandmark, int EntryIndex, int LandmarkIndex, int LandmarkCount)
{
landmark_range Result = {};
db_landmark Landmark = *(db_landmark*)(FirstLandmark + sizeof(Landmark) * LandmarkIndex);
while(EntryIndex == Landmark.EntryIndex && LandmarkIndex > 0)
{
--LandmarkIndex;
Landmark = *(db_landmark*)(FirstLandmark + sizeof(Landmark) * LandmarkIndex);
}
if(Landmark.EntryIndex != EntryIndex)
{
++LandmarkIndex;
}
Landmark = *(db_landmark*)(FirstLandmark + sizeof(Landmark) * LandmarkIndex);
Result.First = LandmarkIndex;
Result.Length = GetIndexRangeLength(FirstLandmark, EntryIndex, LandmarkIndex, LandmarkCount);
return Result;
}
landmark_range
BinarySearchForMetadataLandmark(void *FirstLandmark, int EntryIndex, int LandmarkCount)
{
// NOTE(matt): Depends on FirstLandmark being positioned after an Asset "header"
landmark_range Result = {};
if(LandmarkCount > 0)
{
int Lower = 0;
db_landmark *LowerLandmark = (db_landmark*)(FirstLandmark + sizeof(LowerLandmark) * Lower);
if(EntryIndex < LowerLandmark->EntryIndex)
{
Result.First = 0;
Result.Length = 0;
return Result;
}
int Upper = LandmarkCount - 1;
db_landmark *UpperLandmark;
// TODO(matt): Is there a slicker way of doing this?
if(Upper >= 0)
{
UpperLandmark = (db_landmark*)(FirstLandmark + sizeof(db_landmark) * Upper);
if(EntryIndex > UpperLandmark->EntryIndex)
{
Result.First = LandmarkCount;
Result.Length = 0;
return Result;
}
}
int Pivot = Upper - ((Upper - Lower) >> 1);
db_landmark *PivotLandmark;
do {
LowerLandmark = (db_landmark*)(FirstLandmark + sizeof(db_landmark) * Lower);
PivotLandmark = (db_landmark*)(FirstLandmark + sizeof(db_landmark) * Pivot);
UpperLandmark = (db_landmark*)(FirstLandmark + sizeof(db_landmark) * Upper);
if(EntryIndex == LowerLandmark->EntryIndex)
{
return GetIndexRange(FirstLandmark, EntryIndex, Lower, LandmarkCount);
}
if(EntryIndex == PivotLandmark->EntryIndex)
{
return GetIndexRange(FirstLandmark, EntryIndex, Pivot, LandmarkCount);
}
if(EntryIndex == UpperLandmark->EntryIndex)
{
return GetIndexRange(FirstLandmark, EntryIndex, Upper, LandmarkCount);
}
if(EntryIndex < PivotLandmark->EntryIndex) { Upper = Pivot; }
else { Lower = Pivot; }
Pivot = Upper - ((Upper - Lower) >> 1);
} while(Upper > Pivot);
Result.First = Upper;
Result.Length = 0;
return Result;
}
return Result;
}
void
SnipeChecksumAndCloseFile(file_buffer *File, void *FirstLandmark, int LandmarksInFile, buffer *Checksum, int *RunningLandmarkIndex)
{
for(int j = 0; j < LandmarksInFile; ++j, ++*RunningLandmarkIndex)
{
db_landmark Landmark = *(db_landmark *)(FirstLandmark + sizeof(DB.Landmark) * *RunningLandmarkIndex);
File->Buffer.Ptr = File->Buffer.Location + Landmark.Position;
CopyBufferSized(&File->Buffer, Checksum, Checksum->Ptr - Checksum->Location);
}
File->Handle = fopen(File->Path, "w");
fwrite(File->Buffer.Location, File->FileSize, 1, File->Handle);
fclose(File->Handle);
FreeBuffer(&File->Buffer);
}
void
SnipeChecksumIntoHTML(void *FirstLandmark, buffer *Checksum)
{
landmark_range SearchRange = BinarySearchForMetadataLandmark(FirstLandmark, PAGE_TYPE_SEARCH, DB.Asset.LandmarkCount);
int RunningLandmarkIndex = 0;
if(SearchRange.Length > 0)
{
file_buffer HTML;
ReadSearchPageIntoBuffer(&HTML);
SnipeChecksumAndCloseFile(&HTML, FirstLandmark, SearchRange.Length, Checksum, &RunningLandmarkIndex);
}
for(; RunningLandmarkIndex < DB.Asset.LandmarkCount;)
{
db_landmark Landmark = *(db_landmark *)(FirstLandmark + sizeof(Landmark) * RunningLandmarkIndex);
db_entry Entry = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * Landmark.EntryIndex);
int Length = GetIndexRangeLength(FirstLandmark, Landmark.EntryIndex, RunningLandmarkIndex, DB.Asset.LandmarkCount);
file_buffer HTML;
ReadPlayerPageIntoBuffer(&HTML, &Entry);
SnipeChecksumAndCloseFile(&HTML, FirstLandmark, Length, Checksum, &RunningLandmarkIndex);
}
}
void
PrintWatchHandles(void)
{
printf("\n"
"PrintWatchHandles()\n");
for(int i = 0; i < WatchHandles.Count; ++i)
{
printf(" %d • %s • %s\n",
WatchHandles.Handle[i].Descriptor,
WatchHandles.Handle[i].Type == WT_HMML ? "WT_HMML " : "WT_ASSET",
WatchHandles.Handle[i].Path);
}
}
bool
IsSymlink(char *Filepath)
{
int File = open(Filepath, O_RDONLY | O_NOFOLLOW);
bool Result = (errno == ELOOP);
close(File);
return Result;
}
void
FitWatchHandle(void)
{
int BlockSize = 8;
if(WatchHandles.Count == WatchHandles.Capacity)
{
WatchHandles.Capacity += BlockSize;
if(WatchHandles.Handle)
{
WatchHandles.Handle = realloc(WatchHandles.Handle, WatchHandles.Capacity * sizeof(*WatchHandles.Handle));
}
else
{
WatchHandles.Handle = calloc(WatchHandles.Capacity, sizeof(*WatchHandles.Handle));
}
}
}
void
PushHMMLWatchHandle(void)
{
FitWatchHandle();
CopyString(WatchHandles.Handle[WatchHandles.Count].Path, sizeof(WatchHandles.Handle[0].Path), Config.ProjectDir);
WatchHandles.Handle[WatchHandles.Count].Descriptor = inotify_add_watch(inotifyInstance, Config.ProjectDir, IN_CLOSE_WRITE | IN_DELETE);
WatchHandles.Handle[WatchHandles.Count].Type = WT_HMML;
++WatchHandles.Count;
}
void
PushAssetWatchHandle(file_buffer *AssetFile, uint32_t AssetIndex)
{
if(IsSymlink(AssetFile->Path))
{
char ResolvedSymlinkPath[4096] = {};
readlink(AssetFile->Path, ResolvedSymlinkPath, 4096);
Clear(AssetFile->Path, sizeof(AssetFile->Path));
CopyString(AssetFile->Path, sizeof(AssetFile->Path), ResolvedSymlinkPath);
}
ResolvePath(AssetFile->Path);
StripComponentFromPath(AssetFile->Path);
for(int i = 0; i < WatchHandles.Count; ++i)
{
if(!StringsDiffer(WatchHandles.Handle[i].Path, AssetFile->Path))
{
return;
}
}
FitWatchHandle();
WatchHandles.Handle[WatchHandles.Count].Type = WT_ASSET;
CopyString(WatchHandles.Handle[WatchHandles.Count].Path, sizeof(WatchHandles.Handle[0].Path), AssetFile->Path);
WatchHandles.Handle[WatchHandles.Count].Descriptor = inotify_add_watch(inotifyInstance, AssetFile->Path, IN_CLOSE_WRITE);
++WatchHandles.Count;
}
void
UpdateAssetInDB(int AssetIndex)
{
DB.Header = *(db_header *)DB.Metadata.Buffer.Location;
if(DB.Header.HexSignature != FOURCC("CNRA"))
{
printf("line %d: Malformed .metadata file. HexSignature not in expected location\n", __LINE__);
exit(1);
}
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + sizeof(DB.Header);
DB.EntriesHeader = *(db_header_entries *)DB.Metadata.Buffer.Ptr;
if(DB.EntriesHeader.BlockID != FOURCC("NTRY"))
{
printf("line %d: Malformed .metadata file. Entries BlockID not in expected location\n", __LINE__);
exit(1);
}
DB.Metadata.Buffer.Ptr += sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * DB.EntriesHeader.Count;
DB.AssetsHeader = *(db_header_assets *)DB.Metadata.Buffer.Ptr;
if(DB.AssetsHeader.BlockID != FOURCC("ASET"))
{
printf("line %d: Malformed .metadata file. Assets BlockID not in expected location\n", __LINE__);
exit(1);
}
int AssetsHeaderLocation = DB.Metadata.Buffer.Ptr - DB.Metadata.Buffer.Location;
DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader);
bool Found = FALSE;
for(int i = 0; i < DB.AssetsHeader.Count; ++i)
{
DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr;
if(!StringsDiffer(DB.Asset.Filename, Assets.Asset[AssetIndex].Filename) && DB.Asset.Type == Assets.Asset[AssetIndex].Type)
{
Found = TRUE;
break;
}
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset) + sizeof(DB.Landmark) * DB.Asset.LandmarkCount;
}
if(Found)
{
if(DB.Asset.Hash != Assets.Asset[AssetIndex].Hash)
{
DB.Asset.Hash = Assets.Asset[AssetIndex].Hash;
*(db_asset *)DB.Metadata.Buffer.Ptr = DB.Asset;
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset);
buffer Checksum;
ClaimBuffer(&Checksum, "Checksum", 16);
CopyStringToBuffer(&Checksum, "%08x", DB.Asset.Hash);
file_buffer AssetFile;
ConstructAssetPath(&AssetFile, DB.Asset.Filename, DB.Asset.Type);
ResolvePath(AssetFile.Path);
char Message[256] = { };
CopyString(Message, sizeof(Message), "%sUpdating%s checksum %s of %s in HTML files", ColourStrings[CS_ONGOING], ColourStrings[CS_END], Checksum.Location, AssetFile.Path);
fprintf(stderr, Message);
SnipeChecksumIntoHTML(DB.Metadata.Buffer.Ptr, &Checksum);
ClearTerminalRow(StringLength(Message));
fprintf(stderr, "%sUpdated%s checksum %s of %s\n", ColourStrings[CS_REINSERTION], ColourStrings[CS_END], Checksum.Location, AssetFile.Path);
DeclaimBuffer(&Checksum);
DB.Metadata.Handle = fopen(DB.Metadata.Path, "w");
fwrite(DB.Metadata.Buffer.Location, DB.Metadata.FileSize, 1, DB.Metadata.Handle);
CycleFile(&DB.Metadata);
Assets.Asset[AssetIndex].DeferredUpdate = FALSE;
}
}
else
{
// Append new asset, not bothering to insertion sort because there likely won't be many
++DB.AssetsHeader.Count;
DB.Metadata.Handle = fopen(DB.Metadata.Path, "w");
fwrite(DB.Metadata.Buffer.Location, AssetsHeaderLocation, 1, DB.Metadata.Handle);
fwrite(&DB.AssetsHeader, sizeof(DB.AssetsHeader), 1, DB.Metadata.Handle);
fwrite(DB.Metadata.Buffer.Location + AssetsHeaderLocation + sizeof(DB.AssetsHeader),
DB.Metadata.FileSize - AssetsHeaderLocation - sizeof(DB.AssetsHeader),
1,
DB.Metadata.Handle);
db_asset Asset = {};
Asset.Hash = Assets.Asset[AssetIndex].Hash;
Asset.Type = Assets.Asset[AssetIndex].Type;
ClearCopyStringNoFormat(Asset.Filename, sizeof(Asset.Filename), Assets.Asset[AssetIndex].Filename);
fwrite(&Asset, sizeof(Asset), 1, DB.Metadata.Handle);
printf("%sAppended%s %s asset: %s [%08x]\n", ColourStrings[CS_ADDITION], ColourStrings[CS_END], AssetTypeNames[Asset.Type], Asset.Filename, Asset.Hash);
CycleFile(&DB.Metadata);
}
if(!Assets.Asset[AssetIndex].Known)
{
Assets.Asset[AssetIndex].Known = TRUE;
}
}
void
FitAssetLandmark(enum8(builtin_assets + support_icons) AssetIndex, int PageType)
{
int BlockSize = 2;
if(PageType == PAGE_PLAYER)
{
if(Assets.Asset[AssetIndex].PlayerLandmarkCount == Assets.Asset[AssetIndex].PlayerLandmarkCapacity)
{
Assets.Asset[AssetIndex].PlayerLandmarkCapacity += BlockSize;
if(Assets.Asset[AssetIndex].PlayerLandmark)
{
Assets.Asset[AssetIndex].PlayerLandmark =
realloc(Assets.Asset[AssetIndex].PlayerLandmark,
Assets.Asset[AssetIndex].PlayerLandmarkCapacity * sizeof(*Assets.Asset[AssetIndex].PlayerLandmark));
}
else
{
Assets.Asset[AssetIndex].PlayerLandmark =
calloc(Assets.Asset[AssetIndex].PlayerLandmarkCapacity, sizeof(*Assets.Asset[AssetIndex].PlayerLandmark));
}
}
}
else
{
if(Assets.Asset[AssetIndex].SearchLandmarkCount == Assets.Asset[AssetIndex].SearchLandmarkCapacity)
{
Assets.Asset[AssetIndex].SearchLandmarkCapacity += BlockSize;
if(Assets.Asset[AssetIndex].SearchLandmark)
{
Assets.Asset[AssetIndex].SearchLandmark =
realloc(Assets.Asset[AssetIndex].SearchLandmark,
Assets.Asset[AssetIndex].SearchLandmarkCapacity * sizeof(*Assets.Asset[AssetIndex].SearchLandmark));
}
else
{
Assets.Asset[AssetIndex].SearchLandmark =
calloc(Assets.Asset[AssetIndex].SearchLandmarkCapacity, sizeof(*Assets.Asset[AssetIndex].SearchLandmark));
}
}
}
}
void
PushAssetLandmark(buffer *Dest, int AssetIndex, int PageType)
{
if(!(Config.Mode & MODE_NOREVVEDRESOURCE))
{
FitAssetLandmark(AssetIndex, PageType);
CopyStringToBuffer(Dest, "?%s=", Config.QueryString);
if(PageType == PAGE_PLAYER)
{
Assets.Asset[AssetIndex].PlayerLandmark[Assets.Asset[AssetIndex].PlayerLandmarkCount] = Dest->Ptr - Dest->Location;
++Assets.Asset[AssetIndex].PlayerLandmarkCount;
}
else
{
Assets.Asset[AssetIndex].SearchLandmark[Assets.Asset[AssetIndex].SearchLandmarkCount] = Dest->Ptr - Dest->Location;
++Assets.Asset[AssetIndex].SearchLandmarkCount;
}
CopyStringToBuffer(Dest, "%08x", Assets.Asset[AssetIndex].Hash);
}
}
void
ResetAssetLandmarks(void)
{
for(int AssetIndex = 0; AssetIndex < Assets.Count; ++AssetIndex)
{
for(int LandmarkIndex = 0; LandmarkIndex < Assets.Asset[AssetIndex].PlayerLandmarkCount; ++LandmarkIndex)
{
Assets.Asset[AssetIndex].PlayerLandmark[LandmarkIndex] = 0;
}
Assets.Asset[AssetIndex].PlayerLandmarkCount = 0;
for(int LandmarkIndex = 0; LandmarkIndex < Assets.Asset[AssetIndex].SearchLandmarkCount; ++LandmarkIndex)
{
Assets.Asset[AssetIndex].SearchLandmark[LandmarkIndex] = 0;
}
Assets.Asset[AssetIndex].SearchLandmarkCount = 0;
Assets.Asset[AssetIndex].OffsetLandmarks = FALSE;
}
}
void
FitAsset(void)
{
int BlockSize = 16;
if(Assets.Count == Assets.Capacity)
{
Assets.Capacity += BlockSize;
if(Assets.Asset)
{
Assets.Asset = realloc(Assets.Asset, Assets.Capacity * sizeof(*Assets.Asset));
}
else
{
Assets.Asset = calloc(Assets.Capacity, sizeof(*Assets.Asset));
}
}
}
void
UpdateAsset(uint32_t AssetIndex, bool Defer)
{
file_buffer File;
ConstructAssetPath(&File, Assets.Asset[AssetIndex].Filename, Assets.Asset[AssetIndex].Type);
if(ReadFileIntoBuffer(&File, 0) == RC_SUCCESS)
{
Assets.Asset[AssetIndex].Hash = StringToFletcher32(File.Buffer.Location, File.FileSize);
if(!Defer)
{
UpdateAssetInDB(AssetIndex);
}
else
{
Assets.Asset[AssetIndex].DeferredUpdate = TRUE;
}
FreeBuffer(&File.Buffer);
}
}
int
FinalPathComponentPosition(char *Path)
{
char *Ptr = Path + StringLength(Path) - 1;
while(Ptr > Path && *Ptr != '/')
{
--Ptr;
}
if(*Ptr == '/')
{
++Ptr;
}
return Ptr - Path;
}
int
PlaceAsset(char *Filename, int Type, int Position)
{
FitAsset();
Assets.Asset[Position].Type = Type;
CopyString(Assets.Asset[Position].Filename, sizeof(Assets.Asset[0].Filename), Filename);
Assets.Asset[Position].FilenameAt = FinalPathComponentPosition(Filename);
if(Position == Assets.Count && !Assets.Asset[Position].Known) { ++Assets.Count; }
file_buffer File;
ConstructAssetPath(&File, Filename, Type);
if(ReadFileIntoBuffer(&File, 0) == RC_SUCCESS)
{
Assets.Asset[Position].Hash = StringToFletcher32(File.Buffer.Location, File.FileSize);
FreeBuffer(&File.Buffer);
PushAssetWatchHandle(&File, Position);
return RC_SUCCESS;
}
else
{
ResolvePath(File.Path);
printf("%sNonexistent%s %s asset: %s\n", ColourStrings[CS_WARNING], ColourStrings[CS_END], AssetTypeNames[Type], File.Path);
return RC_ERROR_FILE;
}
}
int
PushAsset(char *Filename, int Type, uint32_t *AssetIndexPtr)
{
for(*AssetIndexPtr = 0; *AssetIndexPtr < Assets.Count; ++*AssetIndexPtr)
{
if(!StringsDiffer(Filename, Assets.Asset[*AssetIndexPtr].Filename) && Type == Assets.Asset[*AssetIndexPtr].Type)
{
break;
}
}
return PlaceAsset(Filename, Type, *AssetIndexPtr);
}
void
InitBuiltinAssets(void)
{
Assert(BUILTIN_ASSETS_COUNT == ArrayCount(BuiltinAssets));
CopyString(BuiltinAssets[ASSET_CSS_THEME].Filename, sizeof(BuiltinAssets[0].Filename), "cinera__%s.css", Config.Theme);
for(int AssetIndex = 0; AssetIndex < BUILTIN_ASSETS_COUNT; ++AssetIndex)
{
if(PlaceAsset(BuiltinAssets[AssetIndex].Filename, BuiltinAssets[AssetIndex].Type, AssetIndex) == RC_ERROR_FILE && AssetIndex == ASSET_CSS_TOPICS)
{
printf( " %s└───────┴────┴─── Don't worry about this one. We'll generate it if needed%s\n", ColourStrings[CS_COMMENT], ColourStrings[CS_END]);
}
}
Assets.Count = BUILTIN_ASSETS_COUNT;
Assert(SUPPORT_ICON_COUNT - BUILTIN_ASSETS_COUNT == ArrayCount(SupportIcons));
for(int SupportIconIndex = BUILTIN_ASSETS_COUNT; SupportIconIndex < SUPPORT_ICON_COUNT; ++SupportIconIndex)
{
PlaceAsset(SupportIcons[SupportIconIndex - BUILTIN_ASSETS_COUNT], ASSET_IMG, SupportIconIndex);
}
}
void
SkipEntriesBlock(void *EntriesHeaderLocation)
{
DB.EntriesHeader = *(db_header_entries *)EntriesHeaderLocation;
DB.Metadata.Buffer.Ptr = EntriesHeaderLocation + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * DB.EntriesHeader.Count;
}
void
LocateAssetsBlock(void)
{
DB.Header = *(db_header *)DB.Metadata.Buffer.Location;
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + sizeof(DB.Header);
for(int BlockIndex = 0; BlockIndex < DB.Header.BlockCount; ++BlockIndex)
{
uint32_t FirstInt = *(uint32_t *)DB.Metadata.Buffer.Ptr;
if(FirstInt == FOURCC("NTRY"))
{
SkipEntriesBlock(DB.Metadata.Buffer.Ptr);
}
else if(FirstInt == FOURCC("ASET"))
{
return;
}
}
}
void
InitAssets(void)
{
InitBuiltinAssets();
LocateAssetsBlock();
DB.AssetsHeader = *(db_header_assets *)DB.Metadata.Buffer.Ptr;
DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader);
for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex)
{
DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr;
uint32_t AI;
PushAsset(DB.Asset.Filename, DB.Asset.Type, &AI);
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset) + sizeof(DB.Landmark) * DB.Asset.LandmarkCount;
}
}
void
ConstructResolvedAssetURL(buffer *Buffer, uint32_t AssetIndex, enum8(pages) PageType)
{
ClaimBuffer(Buffer, "URL", (MAX_ROOT_URL_LENGTH + 1 + MAX_RELATIVE_ASSET_LOCATION_LENGTH + 1) * 2);
ConstructURLPrefix(Buffer, Assets.Asset[AssetIndex].Type, PageType);
CopyStringToBufferHTMLPercentEncoded(Buffer, Assets.Asset[AssetIndex].Filename);
ResolvePath(Buffer->Location);
}
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].SupportURL)
{
buffer URL;
ConstructResolvedAssetURL(&URL, Credentials[CredentialIndex].SupportIconIndex, PAGE_PLAYER);
CopyStringToBuffer(CreditsMenu,
" <a class=\"support\" href=\"%s\" target=\"_blank\"><div class=\"support_icon\" data-sprite=\"%s",
Credentials[CredentialIndex].SupportURL,
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(CreditsMenu, Credentials[CredentialIndex].SupportIconIndex, PAGE_PLAYER);
CopyStringToBuffer(CreditsMenu, "\"></div></a>\n");
}
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 miblodelcarpio@gmail.com 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 miblodelcarpio@gmail.com 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 miblodelcarpio@gmail.com 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 miblodelcarpio@gmail.com 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, "%sFetching%s quotes: %s\n", ColourStrings[CS_ONGOING], ColourStrings[CS_END], 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)
{
// TODO(matt): Error handling
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/%s", Config.RootDir, Config.CSSDir, BuiltinAssets[ASSET_CSS_TOPICS].Filename);
}
else
{
CopyString(Topics.Path, sizeof(Topics.Path), "%s/%s", Config.RootDir, BuiltinAssets[ASSET_CSS_TOPICS].Filename);
}
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);
if(Assets.Asset[ASSET_CSS_TOPICS].Known)
{
UpdateAsset(ASSET_CSS_TOPICS, TRUE);
}
else
{
PlaceAsset(BuiltinAssets[ASSET_CSS_TOPICS].Filename, ASSET_CSS, ASSET_CSS_TOPICS);
}
return RC_SUCCESS;
}
else
{
// NOTE(matt): Maybe it shouldn't be possible to hit this case now that we MakeDir the actual 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: %s(advisedly universal, but may be set per-(sub)project as required)%s\n"
" -r <assets root directory>\n"
" Override default assets root directory (\"%s\")\n"
" -R <assets root URL>\n"
" Override default assets root URL (\"%s\")\n"
" %sIMPORTANT%s: -r and -R must correspond to the same location\n"
" %sUNSUPPORTED: If you move files from RootDir, the RootURL should\n"
" correspond to the resulting location%s\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"
" -Q <revved resources query string>\n"
" Override default query string (\"%s\")\n"
" To disable revved resources, set an empty string with -Q \"\"\n"
"\n"
" Project Settings:\n"
" -p <project ID>\n"
" Set the project ID, triggering PROJECT EDITION\n"
" -m <default medium>\n"
" Override default default medium (\"%s\")\n"
" %sKnown project defaults:\n",
BinaryLocation,
ColourStrings[CS_COMMENT], ColourStrings[CS_END], DefaultConfig->RootDir,
DefaultConfig->RootURL,
ColourStrings[CS_ERROR], ColourStrings[CS_END],
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
DefaultConfig->CSSDir,
DefaultConfig->ImagesDir,
DefaultConfig->JSDir,
DefaultConfig->QueryString,
DefaultConfig->DefaultMedium,
ColourStrings[CS_COMMENT]);
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,
"%s -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 <search template location>\n"
" Set search 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 <search location>\n"
" Override default search 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"
" %sUNSUPPORTED: This is likely to be removed in the future%s\n"
" -w\n"
" Force quote cache rebuild %s(memory aid: \"wget\")%s\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) database and exit\n"
" -v\n"
" Display version and exit\n"
" -h\n"
" Display this help\n"
"\n"
"Template:\n"
" A valid Search Template shall contain exactly one each of the following tags:\n"
" <!-- __CINERA_INCLUDES__ -->\n"
" to put inside your own <head></head>\n"
" <!-- __CINERA_SEARCH__ -->\n"
"\n"
" A valid 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"
" <!-- __CINERA_VOD_PLATFORM__ -->\n"
"\n"
" Other tags available for use in either template:\n"
" Asset tags:\n"
" <!-- __CINERA_ASSET__ path.ext -->\n"
" General purpose tag that outputs the URL of the specified asset\n"
" relative to the Asset Root URL %s(-R)%s\n"
" <!-- __CINERA_IMAGE__ path.ext -->\n"
" General purpose tag that outputs the URL of the specified asset\n"
" relative to the Images Directory %s(-i)%s\n"
" <!-- __CINERA_CSS__ path.ext -->\n"
" Convenience tag that outputs a <link rel=\"stylesheet\"...> node\n"
" for the specified asset relative to the CSS Directory %s(-c)%s, for\n"
" use inside your <head> block\n"
" <!-- __CINERA_JS__ path.ext -->\n"
" Convenience tag that outputs a <script type=\"text/javascript\"...>\n"
" node for the specified asset relative to the JS Directory %s(-j)%s,\n"
" for use wherever a <script> node is valid\n"
" The path.ext in these tags supports parent directories to locate the\n"
" asset file relative to its specified type directory (generic, CSS, image\n"
" or JS), including the \"../\" directory, and paths containing spaces must\n"
" be surrounded with double-quotes (\\-escapable if the quoted path itself\n"
" contains double-quotes).\n"
"\n"
" All these asset tags additionally enable revving, appending a query\n"
" string %s(-Q)%s and the file's checksum to the URL. Changes to a file\n"
" trigger a rehash and edit of all HTML pages citing this asset.\n"
"\n"
" <!-- __CINERA_PROJECT__ -->\n"
" <!-- __CINERA_PROJECT_ID__ -->\n"
" <!-- __CINERA_SEARCH_URL__ -->\n"
" <!-- __CINERA_THEME__ -->\n"
" <!-- __CINERA_URL__ -->\n"
" Only really usable if BaseURL is set %s(-B)%s\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",
ColourStrings[CS_END],
DefaultConfig->ProjectDir,
DefaultConfig->TemplatesDir,
DefaultConfig->BaseDir,
DefaultConfig->BaseURL,
DefaultConfig->SearchLocation,
DefaultConfig->PlayerLocation,
DefaultConfig->OutLocation,
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
DefaultConfig->LogLevel,
DefaultConfig->UpdateInterval,
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
ColourStrings[CS_COMMENT], ColourStrings[CS_END]);
}
void
DepartComment(buffer *Template)
{
while(Template->Ptr - Template->Location < Template->Size)
{
if(!StringsDifferT("-->", Template->Ptr, 0))
{
Template->Ptr += StringLength("-->");
break;
}
++Template->Ptr;
}
}
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
{
char *Ptr = String;
int Length = StringLength(Ptr);
if(Length > 0)
{
while((Ptr[0]) == '/')
{
++Ptr;
--Length;
}
while(Ptr[Length - 1] == '/')
{
Ptr[Length - 1] = '\0';
--Length;
}
}
return Ptr;
}
void
ConsumeWhitespace(buffer *Buffer, char **Ptr)
{
while(*Ptr - Buffer->Location < Buffer->Size)
{
switch(**Ptr)
{
case ' ':
case '\t':
case '\n':
++*Ptr; break;
default: return;
}
}
}
int
ParseQuotedString(buffer *Dest, buffer *Src, char **Ptr, enum8(template_tag_codes) TagIndex)
{
char *End;
bool MissingPath = FALSE;
bool MissingClosingQuote = FALSE;
if(**Ptr == '\"')
{
++*Ptr;
End = *Ptr;
while(End - Src->Location < Src->Size && End - *Ptr < Dest->Size)
{
switch(*End)
{
case '-':
{
if((End + 2) - Src->Location < Src->Size && End[1] == '-' && End[2] == '>')
{
MissingClosingQuote = TRUE;
goto Finalise;
}
}
case '\"': goto Finalise;
case '\\': ++End;
default: *Dest->Ptr++ = *End++; break;
}
}
}
else
{
End = *Ptr;
if((End + 3) - Src->Location < Src->Size && End[0] == '-' && End[1] == '-' && End[2] == '>')
{
MissingPath = TRUE;
goto Finalise;
}
while(End - Src->Location < Src->Size && *End != ' ' && Dest->Ptr - Dest->Location < Dest->Size)
{
*Dest->Ptr++ = *End++;
}
}
*Dest->Ptr = '\0';
Finalise:
if(MissingClosingQuote)
{
printf("%sQuoted string%s seems to be missing its closing quote mark:\n"
" %.*s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], (int)(End - *Ptr), *Ptr);
return RC_ERROR_PARSING;
}
if(MissingPath || StringLength(Dest->Location) == 0)
{
printf("%s%s tag%s seems to be missing a file path\n"
" %.*s\n", ColourStrings[CS_ERROR], TemplateTags[TagIndex], ColourStrings[CS_END], (int)(End - *Ptr), *Ptr);
return RC_ERROR_PARSING;
}
if(End - *Ptr == Dest->Size)
{
printf("%sAsset file path%s is too long (we support paths up to %d characters):\n"
" %.*s...\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], MAX_ASSET_FILENAME_LENGTH, (int)(End - *Ptr), *Ptr);
return RC_ERROR_PARSING;
}
return RC_SUCCESS;
}
void
StripPWDIndicators(char *Path)
{
buffer B;
ClaimBuffer(&B, "PWDStrippedPtr", (StringLength(Path) + 1) * 2);
CopyStringToBufferNoFormat(&B, Path);
B.Ptr = B.Location;
if(B.Size >= 2 && B.Ptr[0] == '.' && B.Ptr[1] == '/')
{
char *NextComponentHead = B.Ptr + 2;
int RemainingChars = StringLength(NextComponentHead);
CopyStringToBufferNoFormat(&B, NextComponentHead);
Clear(B.Ptr, B.Size - (B.Ptr - B.Location));
B.Ptr -= RemainingChars;
}
while(SeekBufferForString(&B, "/./", C_SEEK_FORWARDS, C_SEEK_END) == RC_SUCCESS)
{
char *NextComponentHead = B.Ptr;
int RemainingChars = StringLength(NextComponentHead);
--B.Ptr;
SeekBufferForString(&B, "/", C_SEEK_BACKWARDS, C_SEEK_START);
CopyStringToBufferNoFormat(&B, NextComponentHead);
Clear(B.Ptr, B.Size - (B.Ptr - B.Location));
B.Ptr -= RemainingChars;
}
CopyStringNoFormat(Path, StringLength(Path) + 1, B.Location);
DeclaimBuffer(&B);
}
int
ParseAssetString(template *Template, enum8(template_tag_codes) TagIndex, uint32_t *AssetIndexPtr)
{
char *Ptr = Template->File.Buffer.Ptr + StringLength(TemplateTags[TagIndex]);
ConsumeWhitespace(&Template->File.Buffer, &Ptr);
buffer AssetString;
ClaimBuffer(&AssetString, "AssetString", MAX_ASSET_FILENAME_LENGTH);
if(ParseQuotedString(&AssetString, &Template->File.Buffer, &Ptr, TagIndex) == RC_ERROR_PARSING)
{
DeclaimBuffer(&AssetString);
return RC_ERROR_PARSING;
}
int AssetType = 0;
switch(TagIndex)
{
case TAG_ASSET: AssetType = ASSET_GENERIC; break;
case TAG_CSS: AssetType = ASSET_CSS; break;
case TAG_IMAGE: AssetType = ASSET_IMG; break;
case TAG_JS: AssetType = ASSET_JS; break;
}
AssetString.Ptr = StripSurroundingSlashes(AssetString.Location);
StripPWDIndicators(AssetString.Ptr);
PushAsset(AssetString.Ptr, AssetType, AssetIndexPtr);
DeclaimBuffer(&AssetString);
return RC_SUCCESS;
}
int
PackTemplate(template *Template, char *Location, enum8(template_types) Type)
{
// TODO(matt): Record line numbers and contextual information:
// <? ?>
// <!-- -->
// < >
// <script </script>
InitTemplate(Template, Location, Type);
buffer Errors;
if(ClaimBuffer(&Errors, "Errors", Kilobytes(1)) == RC_ARENA_FULL) { FreeTemplate(Template); return RC_ARENA_FULL; };
bool HaveErrors = FALSE;
bool HaveAssetParsingErrors = FALSE;
bool FoundIncludes = FALSE;
bool FoundMenus = FALSE;
bool FoundPlayer = FALSE;
bool FoundScript = FALSE;
bool FoundSearch = FALSE;
char *Previous = Template->File.Buffer.Location;
while(Template->File.Buffer.Ptr - Template->File.Buffer.Location < Template->File.Buffer.Size)
{
NextTagSearch:
if(*Template->File.Buffer.Ptr == '!' && (Template->File.Buffer.Ptr > Template->File.Buffer.Location && !StringsDifferT("<!--", &Template->File.Buffer.Ptr[-1], 0)))
{
char *CommentStart = &Template->File.Buffer.Ptr[-1];
Template->File.Buffer.Ptr += StringLength("!--");
while(Template->File.Buffer.Ptr - Template->File.Buffer.Location < Template->File.Buffer.Size && StringsDifferT("-->", Template->File.Buffer.Ptr, 0))
{
for(int TagIndex = 0; TagIndex < TEMPLATE_TAG_COUNT; ++TagIndex)
{
if(!(StringsDifferT(TemplateTags[TagIndex], Template->File.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
*
*/
uint32_t AssetIndex = 0;
switch(TagIndex)
{
case TAG_SEARCH:
FoundSearch = TRUE;
goto RecordTag;
case TAG_INCLUDES:
if(!(Config.Mode & MODE_FORCEINTEGRATION) && FoundIncludes == TRUE)
{
CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", TemplateTags[TagIndex]);
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", TemplateTags[TagIndex]);
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", TemplateTags[TagIndex]);
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", TemplateTags[TagIndex]);
HaveErrors = TRUE;
}
if(!(Config.Mode & MODE_FORCEINTEGRATION) && FoundScript == TRUE)
{
CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", TemplateTags[TagIndex]);
HaveErrors = TRUE;
}
FoundScript = TRUE;
goto RecordTag;
case TAG_ASSET:
case TAG_CSS:
case TAG_IMAGE:
case TAG_JS:
{
if(ParseAssetString(Template, TagIndex, &AssetIndex) == RC_ERROR_PARSING)
{
HaveErrors = TRUE;
HaveAssetParsingErrors = TRUE;
}
} goto RecordTag;
default: // NOTE(matt): All freely usable tags should hit this case
RecordTag:
{
int Offset = CommentStart - Previous;
PushTemplateTag(Template, Offset, TagIndex, AssetIndex);
DepartComment(&Template->File.Buffer);
Previous = Template->File.Buffer.Ptr;
goto NextTagSearch;
}
};
}
}
++Template->File.Buffer.Ptr;
}
}
else
{
++Template->File.Buffer.Ptr;
}
}
if(HaveAssetParsingErrors)
{
DeclaimBuffer(&Errors);
FreeTemplate(Template);
return RC_INVALID_TEMPLATE;
}
if(FoundSearch)
{
Template->Metadata.Validity |= PAGE_SEARCH;
}
if(!HaveErrors && FoundIncludes && FoundMenus && FoundPlayer && FoundScript)
{
Template->Metadata.Validity |= PAGE_PLAYER;
}
if(!(Config.Mode & MODE_FORCEINTEGRATION))
{
if(Type == TEMPLATE_SEARCH && !(Template->Metadata.Validity & PAGE_SEARCH))
{
CopyStringToBuffer(&Errors, "Search template %s must include one <!-- __CINERA_SEARCH__ --> tag\n", Template->File.Path);
fprintf(stderr, "%s", Errors.Location);
DeclaimBuffer(&Errors);
FreeTemplate(Template);
return RC_INVALID_TEMPLATE;
}
else if((Type == TEMPLATE_PLAYER || Type == TEMPLATE_BESPOKE) && !(Template->Metadata.Validity & PAGE_PLAYER))
{
if(!FoundIncludes){ CopyStringToBuffer(&Errors, "Player template %s must include one <!-- __CINERA_INCLUDES__ --> tag\n", Template->File.Path); };
if(!FoundMenus){ CopyStringToBuffer(&Errors, "Player template %s must include one <!-- __CINERA_MENUS__ --> tag\n", Template->File.Path); };
if(!FoundPlayer){ CopyStringToBuffer(&Errors, "Player template %s must include one <!-- __CINERA_PLAYER__ --> tag\n", Template->File.Path); };
if(!FoundScript){ CopyStringToBuffer(&Errors, "Player template %s must include one <!-- __CINERA_SCRIPT__ --> tag\n", Template->File.Path); };
fprintf(stderr, "%s", Errors.Location);
DeclaimBuffer(&Errors);
FreeTemplate(Template);
return RC_INVALID_TEMPLATE;
}
}
DeclaimBuffer(&Errors);
return RC_SUCCESS;
}
void
ConstructSearchURL(buffer *SearchURL)
{
RewindBuffer(SearchURL);
if(StringsDiffer(Config.BaseURL, ""))
{
CopyStringToBuffer(SearchURL, "%s/", Config.BaseURL);
if(StringsDiffer(Config.SearchLocation, ""))
{
CopyStringToBuffer(SearchURL, "%s/", Config.SearchLocation);
}
}
}
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 miblodelcarpio@gmail.com\n", Medium);
return FALSE;
}
typedef struct
{
char *BaseFilename;
char *Title;
} neighbour;
typedef struct
{
neighbour Prev;
neighbour Next;
} neighbours;
void
ExamineDB1(file_buffer File)
{
database1 LocalDB;
LocalDB.Header = *(db_header1 *)File.Buffer.Location;
printf("Current:\n"
"\tDBVersion: %d\n"
"\tAppVersion: %d.%d.%d\n"
"\tHMMLVersion: %d.%d.%d\n"
"\n"
"Entries: %d\n",
LocalDB.Header.DBVersion,
LocalDB.Header.AppVersion.Major, LocalDB.Header.AppVersion.Minor, LocalDB.Header.AppVersion.Patch,
LocalDB.Header.HMMLVersion.Major, LocalDB.Header.HMMLVersion.Minor, LocalDB.Header.HMMLVersion.Patch,
LocalDB.Header.EntryCount);
File.Buffer.Ptr = File.Buffer.Location + sizeof(LocalDB.Header);
for(int EntryIndex = 0; EntryIndex < LocalDB.Header.EntryCount; ++EntryIndex)
{
LocalDB.Entry = *(db_entry1 *)File.Buffer.Ptr;
printf(" %3d\t%s%sSize: %4d\n",
EntryIndex + 1, LocalDB.Entry.BaseFilename,
StringLength(LocalDB.Entry.BaseFilename) > 8 ? "\t" : "\t\t", // NOTE(matt): Janktasm
LocalDB.Entry.Size);
File.Buffer.Ptr += sizeof(LocalDB.Entry);
}
}
void
ExamineDB2(file_buffer File)
{
database2 LocalDB;
LocalDB.Header = *(db_header2 *)File.Buffer.Location;
printf("Current:\n"
"\tDBVersion: %d\n"
"\tAppVersion: %d.%d.%d\n"
"\tHMMLVersion: %d.%d.%d\n"
"\n"
"Relative Search Page Location: %s\n"
"Relative Player Page Location: %s\n"
"\n"
"Entries: %d\n",
LocalDB.Header.DBVersion,
LocalDB.Header.AppVersion.Major, LocalDB.Header.AppVersion.Minor, LocalDB.Header.AppVersion.Patch,
LocalDB.Header.HMMLVersion.Major, LocalDB.Header.HMMLVersion.Minor, LocalDB.Header.HMMLVersion.Patch,
LocalDB.Header.SearchLocation[0] != '\0' ? LocalDB.Header.SearchLocation : "(same as Base URL)",
LocalDB.Header.PlayerLocation[0] != '\0' ? LocalDB.Header.PlayerLocation : "(directly descended from Base URL)",
LocalDB.Header.EntryCount);
File.Buffer.Ptr = File.Buffer.Location + sizeof(LocalDB.Header);
for(int EntryIndex = 0; EntryIndex < LocalDB.Header.EntryCount; ++EntryIndex)
{
LocalDB.Entry = *(db_entry2 *)File.Buffer.Ptr;
printf(" %3d\t%s%sSize: %4d\n",
EntryIndex + 1, LocalDB.Entry.BaseFilename,
StringLength(LocalDB.Entry.BaseFilename) > 8 ? "\t" : "\t\t", // NOTE(matt): Janktasm
LocalDB.Entry.Size);
File.Buffer.Ptr += sizeof(LocalDB.Entry);
}
}
void
ExamineDB3(file_buffer File)
{
database3 LocalDB;
LocalDB.Header = *(db_header3 *)File.Buffer.Location;
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 Search Page Location: %s\n"
"Relative Player Page Location: %s\n"
"Player Page URL Prefix: %s\n"
"\n"
"Entries: %d\n",
LocalDB.Header.CurrentDBVersion,
LocalDB.Header.CurrentAppVersion.Major, LocalDB.Header.CurrentAppVersion.Minor, LocalDB.Header.CurrentAppVersion.Patch,
LocalDB.Header.CurrentHMMLVersion.Major, LocalDB.Header.CurrentHMMLVersion.Minor, LocalDB.Header.CurrentHMMLVersion.Patch,
LocalDB.Header.InitialDBVersion,
LocalDB.Header.InitialAppVersion.Major, LocalDB.Header.InitialAppVersion.Minor, LocalDB.Header.InitialAppVersion.Patch,
LocalDB.Header.InitialHMMLVersion.Major, LocalDB.Header.InitialHMMLVersion.Minor, LocalDB.Header.InitialHMMLVersion.Patch,
LocalDB.Header.ProjectID,
LocalDB.Header.ProjectName,
LocalDB.Header.BaseURL,
LocalDB.Header.SearchLocation[0] != '\0' ? LocalDB.Header.SearchLocation : "(same as Base URL)",
LocalDB.Header.PlayerLocation[0] != '\0' ? LocalDB.Header.PlayerLocation : "(directly descended from Base URL)",
LocalDB.Header.PlayerURLPrefix[0] != '\0' ? LocalDB.Header.PlayerURLPrefix : "(no special prefix, the player page URLs equal their entry's base filename)",
LocalDB.Header.EntryCount);
File.Buffer.Ptr = File.Buffer.Location + sizeof(LocalDB.Header);
for(int EntryIndex = 0; EntryIndex < LocalDB.Header.EntryCount; ++EntryIndex)
{
LocalDB.Entry = *(db_entry3 *)File.Buffer.Ptr;
printf(" %3d\t%s%sSize: %4d\t%d\t%d\t%d\t%d\n"
"\t %s\n",
EntryIndex + 1, LocalDB.Entry.BaseFilename,
StringLength(LocalDB.Entry.BaseFilename) > 8 ? "\t" : "\t\t", // NOTE(matt): Janktasm
LocalDB.Entry.Size,
LocalDB.Entry.LinkOffsets.PrevStart,
LocalDB.Entry.LinkOffsets.PrevEnd,
LocalDB.Entry.LinkOffsets.NextStart,
LocalDB.Entry.LinkOffsets.NextEnd,
LocalDB.Entry.Title);
File.Buffer.Ptr += sizeof(LocalDB.Entry);
}
}
void
ExamineDB4(file_buffer File)
{
database4 LocalDB;
LocalDB.Header = *(db_header4 *)File.Buffer.Location;
LocalDB.EntriesHeader = *(db_header_entries4 *)(File.Buffer.Location + sizeof(LocalDB.Header));
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"
"Database File Location: %s/%s.metadata\n"
"Search File Location: %s/%s.index\n"
"\n"
"Project ID: %s\n"
"Project Full Name: %s\n"
"\n"
"Base URL: %s\n"
"Relative Search Page Location: %s\n"
"Relative Player Page Location: %s\n"
"Player Page URL Prefix: %s\n"
"\n"
"Entries: %d\n",
LocalDB.Header.CurrentDBVersion,
LocalDB.Header.CurrentAppVersion.Major, LocalDB.Header.CurrentAppVersion.Minor, LocalDB.Header.CurrentAppVersion.Patch,
LocalDB.Header.CurrentHMMLVersion.Major, LocalDB.Header.CurrentHMMLVersion.Minor, LocalDB.Header.CurrentHMMLVersion.Patch,
LocalDB.Header.InitialDBVersion,
LocalDB.Header.InitialAppVersion.Major, LocalDB.Header.InitialAppVersion.Minor, LocalDB.Header.InitialAppVersion.Patch,
LocalDB.Header.InitialHMMLVersion.Major, LocalDB.Header.InitialHMMLVersion.Minor, LocalDB.Header.InitialHMMLVersion.Patch,
Config.BaseDir, Config.ProjectID,
Config.BaseDir, Config.ProjectID,
LocalDB.EntriesHeader.ProjectID,
LocalDB.EntriesHeader.ProjectName,
LocalDB.EntriesHeader.BaseURL,
LocalDB.EntriesHeader.SearchLocation[0] != '\0' ? LocalDB.EntriesHeader.SearchLocation : "(same as Base URL)",
LocalDB.EntriesHeader.PlayerLocation[0] != '\0' ? LocalDB.EntriesHeader.PlayerLocation : "(directly descended from Base URL)",
LocalDB.EntriesHeader.PlayerURLPrefix[0] != '\0' ? LocalDB.EntriesHeader.PlayerURLPrefix : "(no special prefix, the player page URLs equal their entry's base filename)",
LocalDB.EntriesHeader.Count);
File.Buffer.Ptr = File.Buffer.Location + sizeof(LocalDB.Header) + sizeof(LocalDB.EntriesHeader);
for(int EntryIndex = 0; EntryIndex < LocalDB.EntriesHeader.Count; ++EntryIndex)
{
LocalDB.Entry = *(db_entry4 *)File.Buffer.Ptr;
printf(" %3d\t%s%sSize: %4d\t%d\t%d\t%d\t%d\n"
"\t %s\n",
EntryIndex, LocalDB.Entry.BaseFilename,
StringLength(LocalDB.Entry.BaseFilename) > 8 ? "\t" : "\t\t", // NOTE(matt): Janktasm
LocalDB.Entry.Size,
LocalDB.Entry.LinkOffsets.PrevStart,
LocalDB.Entry.LinkOffsets.PrevEnd,
LocalDB.Entry.LinkOffsets.NextStart,
LocalDB.Entry.LinkOffsets.NextEnd,
LocalDB.Entry.Title);
File.Buffer.Ptr += sizeof(LocalDB.Entry);
}
LocalDB.AssetsHeader = *(db_header_assets4 *)File.Buffer.Ptr;
File.Buffer.Ptr += sizeof(LocalDB.AssetsHeader);
printf( "\n"
"Asset Root Directory: %s\n"
"Asset Root URL: %s\n"
" CSS Directory: %s\n"
" Images Directory: %s\n"
" JavaScript Directory: %s\n"
"Assets: %d\n"
"\n"
"%sLandmarks are displayed%s i•%sp%s %swhere i is the Entry Index and p is the Position\n"
"in bytes into the HTML file. Entry Index%s -1 %scorresponds to the Search Page%s\n",
LocalDB.AssetsHeader.RootDir,
LocalDB.AssetsHeader.RootURL,
StringsDiffer(LocalDB.AssetsHeader.CSSDir, "") ? LocalDB.AssetsHeader.CSSDir : "(same as root)",
StringsDiffer(LocalDB.AssetsHeader.ImagesDir, "") ? LocalDB.AssetsHeader.ImagesDir : "(same as root)",
StringsDiffer(LocalDB.AssetsHeader.JSDir, "") ? LocalDB.AssetsHeader.JSDir : "(same as root)",
LocalDB.AssetsHeader.Count,
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
ColourStrings[CS_PURPLE], ColourStrings[CS_END],
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
ColourStrings[CS_COMMENT], ColourStrings[CS_END]);
for(int AssetIndex = 0; AssetIndex < LocalDB.AssetsHeader.Count; ++AssetIndex)
{
LocalDB.Asset = *(db_asset4*)File.Buffer.Ptr;
printf("\n"
"%s\n"
"Type: %s\n"
"Checksum: %08x\n"
"Landmarks: %d\n",
LocalDB.Asset.Filename,
AssetTypeNames[LocalDB.Asset.Type],
LocalDB.Asset.Hash,
LocalDB.Asset.LandmarkCount);
File.Buffer.Ptr += sizeof(LocalDB.Asset);
for(int LandmarkIndex = 0; LandmarkIndex < LocalDB.Asset.LandmarkCount; ++LandmarkIndex)
{
LocalDB.Landmark = *(db_landmark *)File.Buffer.Ptr;
File.Buffer.Ptr += sizeof(LocalDB.Landmark);
printf(" %d•%s%d%s", LocalDB.Landmark.EntryIndex, ColourStrings[CS_PURPLE], LocalDB.Landmark.Position, ColourStrings[CS_END]);
}
printf("\n");
}
}
int
ExamineDB(void)
{
file_buffer DBFile;
DBFile.Buffer.ID = "DBFile";
CopyString(DBFile.Path, sizeof(DBFile.Path), "%s/%s.metadata", Config.BaseDir, Config.ProjectID);
int DBFileReadCode = ReadFileIntoBuffer(&DBFile, 0);
switch(DBFileReadCode)
{
case RC_ERROR_MEMORY:
return RC_ERROR_MEMORY;
case RC_ERROR_FILE:
fprintf(stderr, "Unable to open metadata file %s: %s\n", DBFile.Path, strerror(errno));
return RC_ERROR_FILE;
case RC_SUCCESS:
break;
}
uint32_t FirstInt = *(uint32_t *)DBFile.Buffer.Location;
if(FirstInt != FOURCC("CNRA"))
{
switch(FirstInt)
{
case 1: ExamineDB1(DBFile); break;
case 2: ExamineDB2(DBFile); break;
case 3: ExamineDB3(DBFile); break;
default: printf("Invalid metadata file: %s\n", DBFile.Path); break;
}
}
else
{
uint32_t SecondInt = *(uint32_t *)(DBFile.Buffer.Location + sizeof(uint32_t));
switch(SecondInt)
{
case 4: ExamineDB4(DBFile); break;
default: printf("Invalid metadata file: %s\n", DBFile.Path); break;
}
}
FreeBuffer(&DBFile.Buffer);
return RC_SUCCESS;
}
#define HMMLCleanup() \
DeclaimBuffer(&CreditsMenu); \
DeclaimBuffer(&FilterMedia); \
DeclaimBuffer(&FilterTopics); \
DeclaimBuffer(&FilterMenu); \
DeclaimBuffer(&ReferenceMenu); \
DeclaimBuffer(&QuoteMenu); \
hmml_free(&HMML);
bool
VideoIsPrivate(char *VideoID)
{
// NOTE(matt): Currently only supports YouTube
char Message[128];
CopyString(Message, sizeof(Message), "%sChecking%s privacy status of: https://youtube.com/watch?v=%s", ColourStrings[CS_ONGOING], ColourStrings[CS_END], 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
{
db_entry Prev, This, Next;
uint32_t PreLinkPrevOffsetTotal, PreLinkThisOffsetTotal, PreLinkNextOffsetTotal;
uint32_t PrevOffsetModifier, ThisOffsetModifier, NextOffsetModifier;
bool FormerIsFirst, LatterIsFinal;
bool DeletedEntryWasFirst, DeletedEntryWasFinal;
short int PrevIndex, PreDeletionThisIndex, 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;
}
// NOTE(matt): Perhaps these OffsetLandmarks* could be made redundant / generalised once we're on the LUT
void
OffsetLandmarks(buffer *Src, int AssetIndex, int PageType)
{
if(!(Config.Mode & MODE_NOREVVEDRESOURCE))
{
if(PageType == PAGE_PLAYER)
{
for(int LandmarkIndex = 0; LandmarkIndex < Assets.Asset[AssetIndex].PlayerLandmarkCount; ++LandmarkIndex)
{
Assets.Asset[AssetIndex].PlayerLandmark[LandmarkIndex] += Src->Ptr - Src->Location;
}
}
else
{
for(int LandmarkIndex = 0; LandmarkIndex < Assets.Asset[AssetIndex].SearchLandmarkCount; ++LandmarkIndex)
{
Assets.Asset[AssetIndex].SearchLandmark[LandmarkIndex] += Src->Ptr - Src->Location;
}
}
}
}
void
OffsetLandmarksIncludes(buffer *Src, int PageType)
{
OffsetLandmarks(Src, ASSET_CSS_CINERA, PageType);
OffsetLandmarks(Src, ASSET_CSS_THEME, PageType);
OffsetLandmarks(Src, ASSET_CSS_TOPICS, PageType);
if(PageType == PAGE_PLAYER)
{
OffsetLandmarks(Src, ASSET_JS_PLAYER_PRE, PageType);
}
}
void
OffsetLandmarksCredits(buffer *Src)
{
OffsetLandmarks(Src, ICON_SENDOWL, PAGE_PLAYER);
OffsetLandmarks(Src, ICON_PATREON, PAGE_PLAYER);
}
void
OffsetLandmarksMenus(buffer *Src)
{
OffsetLandmarks(Src, ASSET_IMG_FILTER, PAGE_PLAYER);
OffsetLandmarksCredits(Src);
}
//
int
HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, char *Filename, neighbourhood *N)
{
RewindBuffer(&CollationBuffers->IncludesPlayer);
RewindBuffer(&CollationBuffers->Menus);
RewindBuffer(&CollationBuffers->Player);
RewindBuffer(&CollationBuffers->ScriptPlayer);
RewindBuffer(&CollationBuffers->SearchEntry);
*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';
*CollationBuffers->URLPlayer = '\0';
*CollationBuffers->URLSearch = '\0';
*CollationBuffers->VODPlatform = '\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);
char *BaseFilename = GetBaseFilename(Filename, ".hmml");
if(HMML.well_formed)
{
bool HaveErrors = FALSE;
if(StringLength(BaseFilename) > MAX_BASE_FILENAME_LENGTH)
{
fprintf(stderr, "%sBase filename \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], BaseFilename, StringLength(BaseFilename), MAX_BASE_FILENAME_LENGTH, ColourStrings[CS_END]);
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, "%sVideo title \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], HMML.metadata.title, StringLength(HMML.metadata.title), MAX_TITLE_LENGTH, ColourStrings[CS_END]);
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;
}
else
{
CopyString(CollationBuffers->VideoID, sizeof(CollationBuffers->VideoID), "%s", HMML.metadata.id);
}
if(!HMML.metadata.vod_platform)
{
fprintf(stderr, "Please set the vod_platform attribute in the [video] node of your .hmml file\n");
HaveErrors = TRUE;
}
else
{
CopyString(CollationBuffers->VODPlatform, sizeof(CollationBuffers->VODPlatform), "%s", HMML.metadata.vod_platform);
}
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, "%sCustom string %d \"%s%s%s\" is too long (%d/%d characters)%s\n",
ColourStrings[CS_ERROR], CustomIndex, ColourStrings[CS_END], HMML.metadata.custom[CustomIndex], ColourStrings[CS_ERROR],
LengthOfString, CustomIndex < 12 ? MAX_CUSTOM_SNIPPET_SHORT_LENGTH : MAX_CUSTOM_SNIPPET_LONG_LENGTH, ColourStrings[CS_END]);
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(PackTemplate(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, "%sSkipping%s %s", ColourStrings[CS_ERROR], ColourStrings[CS_END], 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, "%sSkipping%s %s - %s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], 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->SearchEntry, "name: \"");
if(StringsDiffer(Config.PlayerURLPrefix, ""))
{
char *Ptr = BaseFilename + StringLength(Config.ProjectID);
CopyStringToBuffer(&CollationBuffers->SearchEntry, "%s%s", Config.PlayerURLPrefix, Ptr);
}
else
{
CopyStringToBuffer(&CollationBuffers->SearchEntry, "%s", BaseFilename);
}
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\n"
"title: \"");
CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, HMML.metadata.title);
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\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"
"%sSkipping%s %s - %s\n",
Filename, Anno->line, Anno->time,
ColourStrings[CS_ERROR], ColourStrings[CS_END], 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 miblodelcarpio@gmail.com\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 == MAX_REF_IDENTIFIER_COUNT)
{
LogError(LOG_EMERGENCY, "REF_MAX_IDENTIFIER (%d) reached. Contact miblodelcarpio@gmail.com", MAX_REF_IDENTIFIER_COUNT);
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 miblodelcarpio@gmail.com\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"
"%sSkipping%s %s - %s\n",
Speaker, Anno->quote.id,
ColourStrings[CS_ERROR], ColourStrings[CS_END], 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->SearchEntry, "\"%d\": \"", TimecodeToSeconds(Anno->time));
if(Anno->is_quote && !Anno->text[0])
{
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\u201C");
CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, QuoteInfo.Text);
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\u201D");
}
else
{
CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, Anno->text);
}
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\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->SearchEntry, "---\n");
N->This.Size = CollationBuffers->SearchEntry.Ptr - CollationBuffers->SearchEntry.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 URL;
ConstructResolvedAssetURL(&URL, ASSET_IMG_FILTER, PAGE_PLAYER);
CopyStringToBuffer(&FilterMenu,
" <div class=\"menu filter\">\n"
" <span><img src=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&FilterMenu, ASSET_IMG_FILTER, PAGE_PLAYER);
CopyStringToBuffer(&FilterMenu,
"\"></span>\n"
" <div class=\"filter_container\">\n"
" <div class=\"filter_mode exclusive\">Filter mode: </div>\n"
" <div class=\"filters\">\n");
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");
OffsetLandmarks(&CollationBuffers->Menus, ASSET_IMG_FILTER, PAGE_PLAYER);
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)
{
OffsetLandmarksCredits(&CollationBuffers->Menus);
CopyBuffer(&CollationBuffers->Menus, &CreditsMenu);
}
CopyStringToBuffer(&CollationBuffers->Menus,
" <div class=\"help\">\n"
" <span>?</span>\n"
" <div class=\"help_container\">\n"
" <span class=\"help_key\">?</span><h1>Keyboard Navigation</h1>\n"
"\n"
" <h2>Global Keys</h2>\n"
" <span class=\"help_key\">[</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"
" <span class=\"help_key%s\">V</span> <span class=\"help_text%s\">Revert filter to original state</span> <span class=\"help_key\">Y</span> <span class=\"help_text\">Select link (requires manual Ctrl-c)</span>\n",
HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable");
CopyStringToBuffer(&CollationBuffers->Menus,
"\n"
" <h2>Menu toggling</h2>\n"
" <span class=\"help_key%s\">q</span> <span class=\"help_text%s\">Quotes</span>\n"
" <span class=\"help_key%s\">r</span> <span class=\"help_text%s\">References</span>\n"
" <span class=\"help_key%s\">f</span> <span class=\"help_text%s\">Filter</span>\n"
" <span class=\"help_key\">y</span> <span class=\"help_text\">Link</span>\n"
" <span class=\"help_key%s\">c</span> <span class=\"help_text%s\">Credits</span>\n",
HasQuoteMenu ? "" : " unavailable", HasQuoteMenu ? "" : " unavailable",
HasReferenceMenu ? "" : " unavailable", HasReferenceMenu ? "" : " unavailable",
HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable",
HasCreditsMenu ? "" : " unavailable", HasCreditsMenu ? "" : " unavailable");
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");
CopyStringToBuffer(&CollationBuffers->Menus,
" <h2>%sQuotes %sand%s References%s Menus%s</h2>\n"
" <span class=\"help_key word%s\">Enter</span> <span class=\"help_text%s\">Jump to timecode</span><br>\n",
// Q R
//
// 0 0 <h2><span off>Quotes and References Menus</span></h2>
// 0 1 <h2><span off>Quotes and</span> References Menus</h2>
// 1 0 <h2>Quotes <span off>and References</span> Menus</h2>
// 1 1 <h2>Quotes and References Menus</h2>
HasQuoteMenu ? "" : "<span class=\"unavailable\">",
HasQuoteMenu && !HasReferenceMenu ? "<span class=\"unavailable\">" : "",
!HasQuoteMenu && HasReferenceMenu ? "</span>" : "",
HasQuoteMenu && !HasReferenceMenu ? "</span>" : "",
!HasQuoteMenu && !HasReferenceMenu ? "</span>" : "",
HasQuoteMenu || HasReferenceMenu ? "" : " unavailable", HasQuoteMenu || HasReferenceMenu ? "" : " unavailable");
CopyStringToBuffer(&CollationBuffers->Menus,
"\n"
" <h2>%sQuotes%s,%s References %sand%s Credits%s Menus%s</h2>"
" <span class=\"help_key%s\">o</span> <span class=\"help_text%s\">Open URL (in new tab)</span>\n",
// Q R C
//
// 0 0 0 <h2><span off>Quotes, References and Credits Menus</span></h2>
// 0 0 1 <h2><span off>Quotes, References and</span> Credits Menus</h2>
// 0 1 0 <h2><span off>Quotes,</span> References <span off>and Credits</span> Menus</h2>
// 0 1 1 <h2><span off>Quotes,</span> References and Credits Menus</h2>
// 1 0 0 <h2>Quotes<span off>, References and Credits</span> Menus</h2>
// 1 0 1 <h2>Quotes<span off>, References </span>and Credits Menus</h2>
// 1 1 0 <h2>Quotes, References <span off>and Credits</span> Menus</h2>
// 1 1 1 <h2>Quotes, References and Credits Menus</h2>
/* 0 */ HasQuoteMenu ? "" : "<span class=\"unavailable\">",
/* 1 */ HasQuoteMenu && !HasReferenceMenu ? "<span class=\"unavailable\">" : "",
/* 2 */ !HasQuoteMenu && HasReferenceMenu ? "</span>" : "",
/* 3 */ HasReferenceMenu && !HasCreditsMenu ? "<span class=\"unavailable\">" : HasQuoteMenu && !HasReferenceMenu && HasCreditsMenu ? "</span>" : "",
/* 4 */ !HasQuoteMenu && !HasReferenceMenu && HasCreditsMenu ? "</span>" : "",
/* 5 */ !HasCreditsMenu && (HasQuoteMenu || HasReferenceMenu) ? "</span>" : "",
/* 6 */ !HasQuoteMenu && !HasReferenceMenu && !HasCreditsMenu ? "</span>" : "",
HasQuoteMenu || HasReferenceMenu || HasCreditsMenu ? "" : " unavailable",
HasQuoteMenu || HasReferenceMenu || HasCreditsMenu ? "" : " unavailable");
CopyStringToBuffer(&CollationBuffers->Menus,
"\n"
" <h2>%sFilter Menu%s</h2>\n"
" <span class=\"help_key%s\">x</span>, <span class=\"help_key word%s\">Space</span> <span class=\"help_text%s\">Toggle category and focus next</span><br>\n"
" <span class=\"help_key%s\">X</span>, <span class=\"help_key word modifier%s\">Shift</span><span class=\"help_key word%s\">Space</span> <span class=\"help_text%s\">Toggle category and focus previous</span><br>\n"
" <span class=\"help_key%s\">v</span> <span class=\"help_text%s\">Invert topics / media as per focus</span>\n"
"\n"
" <h2>%sFilter and%s Link Menus</h2>\n"
" <span class=\"help_key\">z</span> <span class=\"help_text\">Toggle %sfilter /%s linking mode</span>\n",
HasFilterMenu ? "" : "<span class=\"unavailable\">", HasFilterMenu ? "" : "</span>",
HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable",
HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable",
HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable",
HasFilterMenu ? "" : "<span class=\"unavailable\">", HasFilterMenu ? "" : "</span>",
HasFilterMenu ? "" : "<span class=\"help_text unavailable\">", HasFilterMenu ? "" : "</span>");
CopyStringToBuffer(&CollationBuffers->Menus,
"\n"
" <h2>%sCredits Menu%s</h2>\n"
" <span class=\"help_key word%s\">Enter</span> <span class=\"help_text%s\">Open URL (in new tab)</span><br>\n",
HasCreditsMenu ? "" : "<span class=\"unavailable\">", HasCreditsMenu ? "" : "</span>",
HasCreditsMenu ? "" : " unavailable", HasCreditsMenu ? "" : " unavailable");
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>");
buffer URLSearch;
ClaimBuffer(&URLSearch, "URLSearch", MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1);
ConstructSearchURL(&URLSearch);
CopyString(CollationBuffers->URLSearch, sizeof(CollationBuffers->URLSearch), "%s", URLSearch.Location);
DeclaimBuffer(&URLSearch);
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");
}
buffer URL;
ConstructResolvedAssetURL(&URL, ASSET_CSS_CINERA, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesPlayer, ASSET_CSS_CINERA, PAGE_PLAYER);
ConstructResolvedAssetURL(&URL, ASSET_CSS_THEME, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"\">\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesPlayer, ASSET_CSS_THEME, PAGE_PLAYER);
ConstructResolvedAssetURL(&URL, ASSET_CSS_TOPICS, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"\">\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesPlayer, ASSET_CSS_TOPICS, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"\">\n");
ConstructResolvedAssetURL(&URL, ASSET_JS_PLAYER_PRE, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
" <script type=\"text/javascript\" src=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesPlayer, ASSET_JS_PLAYER_PRE, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"\"></script>");
ConstructResolvedAssetURL(&URL, ASSET_JS_PLAYER_POST, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->ScriptPlayer,
"<script type=\"text/javascript\" src=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->ScriptPlayer, ASSET_JS_PLAYER_POST, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->ScriptPlayer,
"\"></script>");
// 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, "%sSkipping%s %s:%d: %s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], 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(Template->File.Buffer.Location)
{
if((Template->Metadata.Validity & PageType) || Config.Mode & MODE_FORCEINTEGRATION)
{
buffer Output;
Output.Size = Template->File.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->File.Buffer.Ptr = Template->File.Buffer.Location;
for(int i = 0; i < Template->Metadata.TagCount; ++i)
{
int j = 0;
while(Template->Metadata.Tags[i].Offset > j)
{
*Output.Ptr++ = *Template->File.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.Tags[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_SEARCH_URL: CopyStringToBufferNoFormat(&Output, CollationBuffers->URLSearch); break; // NOTE(matt): Not HTML-safe
case TAG_THEME: CopyStringToBufferNoFormat(&Output, CollationBuffers->Theme); break; // NOTE(matt): Not HTML-safe
case TAG_TITLE: CopyStringToBufferHTMLSafe(&Output, CollationBuffers->Title); break;
case TAG_URL:
if(PageType == PAGE_PLAYER)
{
CopyStringToBufferNoFormat(&Output, CollationBuffers->URLPlayer);
}
else
{
CopyStringToBufferNoFormat(&Output, CollationBuffers->URLSearch);
}
break;
case TAG_VIDEO_ID: CopyStringToBufferNoFormat(&Output, CollationBuffers->VideoID); break;
case TAG_VOD_PLATFORM: CopyStringToBufferNoFormat(&Output, CollationBuffers->VODPlatform); break;
case TAG_SEARCH:
if(Config.Edition == EDITION_SINGLE)
{
fprintf(stderr, "Template contains a <!-- __CINERA_SEARCH__ --> tag\n"
"Skipping just this tag, because a search cannot be generated for inclusion in a\n"
"bespoke template in Single Edition\n");
}
else
{
OffsetLandmarks(&Output, ASSET_JS_SEARCH, PAGE_SEARCH);
CopyBuffer(&Output, &CollationBuffers->Search);
}
break;
case TAG_INCLUDES:
if(PageType == PAGE_PLAYER)
{
OffsetLandmarksIncludes(&Output, PageType);
CopyBuffer(&Output, &CollationBuffers->IncludesPlayer);
}
else
{
OffsetLandmarksIncludes(&Output, PageType);
CopyBuffer(&Output, &CollationBuffers->IncludesSearch);
}
break;
case TAG_MENUS:
OffsetLandmarksMenus(&Output);
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:
OffsetLandmarks(&Output, ASSET_JS_PLAYER_POST, PAGE_PLAYER);
CopyBuffer(&Output, &CollationBuffers->ScriptPlayer);
break;
case TAG_ASSET:
{
buffer URL;
ConstructResolvedAssetURL(&URL, Template->Metadata.Tags[i].AssetIndex, PageType);
CopyStringToBuffer(&Output, "%s", URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&Output, Template->Metadata.Tags[i].AssetIndex, PageType);
} break;
case TAG_CSS:
{
buffer URL;
ConstructResolvedAssetURL(&URL, Template->Metadata.Tags[i].AssetIndex, PageType);
CopyStringToBuffer(&Output,
"<link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&Output, Template->Metadata.Tags[i].AssetIndex, PageType);
CopyStringToBuffer(&Output, "\">");
} break;
case TAG_IMAGE:
{
buffer URL;
ConstructResolvedAssetURL(&URL, Template->Metadata.Tags[i].AssetIndex, PageType);
CopyStringToBuffer(&Output, "%s", URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&Output, Template->Metadata.Tags[i].AssetIndex, PageType);
} break;
case TAG_JS:
{
buffer URL;
ConstructResolvedAssetURL(&URL, Template->Metadata.Tags[i].AssetIndex, PageType);
CopyStringToBuffer(&Output,
"<script type=\"text/javascript\" src=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&Output, Template->Metadata.Tags[i].AssetIndex, PageType);
CopyStringToBuffer(&Output, "\"></script>");
} 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->File.Buffer);
}
while(Template->File.Buffer.Ptr - Template->File.Buffer.Location < Template->File.Buffer.Size)
{
*Output.Ptr++ = *Template->File.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));
FreeBuffer(&Output);
#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);
FreeBuffer(&Output);
#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"
" ");
OffsetLandmarksIncludes(&Master, PageType);
CopyBuffer(&Master, PageType == PAGE_PLAYER ? &CollationBuffers->IncludesPlayer : &CollationBuffers->IncludesSearch);
CopyStringToBuffer(&Master, "\n");
CopyStringToBuffer(&Master,
" </head>\n"
" <body>\n"
" ");
if(PageType == PAGE_PLAYER)
{
CopyStringToBuffer(&Master, "<div>\n"
" ");
OffsetLandmarksMenus(&Master);
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"
" ");
OffsetLandmarks(&Master, ASSET_JS_PLAYER_POST, PAGE_PLAYER);
CopyBuffer(&Master, &CollationBuffers->ScriptPlayer);
CopyStringToBuffer(&Master, "\n");
}
else
{
OffsetLandmarks(&Master, ASSET_JS_SEARCH, PAGE_SEARCH);
CopyBuffer(&Master, &CollationBuffers->Search);
}
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(db_entry **Entry, char *SearchTerm)
{
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader);
int Lower = 0;
db_entry *LowerEntry = (db_entry*)(DB.Metadata.Buffer.Ptr + sizeof(DB.Entry) * Lower);
if(StringsDiffer(SearchTerm, LowerEntry->BaseFilename) < 0 ) { return -1; }
int Upper = DB.EntriesHeader.Count - 1;
int Pivot = Upper - ((Upper - Lower) >> 1);
db_entry *UpperEntry;
db_entry *PivotEntry;
do {
LowerEntry = (db_entry*)(DB.Metadata.Buffer.Ptr + sizeof(DB.Entry) * Lower);
PivotEntry = (db_entry*)(DB.Metadata.Buffer.Ptr + sizeof(DB.Entry) * Pivot);
UpperEntry = (db_entry*)(DB.Metadata.Buffer.Ptr + sizeof(DB.Entry) * 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
AccumulateDBEntryInsertionOffset(int EntryIndex)
{
int Result = 0;
db_entry *AccEntry = { 0 };
if(EntryIndex < DB.EntriesHeader.Count >> 1)
{
for(; EntryIndex > 0; --EntryIndex)
{
AccEntry = (db_entry*)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * (EntryIndex - 1));
Result += AccEntry->Size;
}
Result += StringLength("---\n");
}
else
{
for(; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex)
{
AccEntry = (db_entry*)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex);
Result += AccEntry->Size;
}
Result = DB.File.FileSize - Result;
}
return Result;
}
void
ClearEntry(db_entry *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));
}
void
InitNeighbourhood(neighbourhood *N)
{
N->PrevIndex = -1;
N->PreDeletionThisIndex = N->ThisIndex = 0;
N->NextIndex = -1;
N->PreLinkPrevOffsetTotal = N->PreLinkThisOffsetTotal = N->PreLinkNextOffsetTotal = 0;
N->PrevOffsetModifier = N->ThisOffsetModifier = N->NextOffsetModifier = 0;
N->FormerIsFirst = N->LatterIsFinal = FALSE;
N->DeletedEntryWasFirst = N->DeletedEntryWasFinal = FALSE;
N->Prev.LinkOffsets.PrevStart = N->Prev.LinkOffsets.PrevEnd = N->Prev.LinkOffsets.NextStart = N->Prev.LinkOffsets.NextEnd = 0;
N->This.LinkOffsets.PrevStart = N->This.LinkOffsets.PrevEnd = N->This.LinkOffsets.NextStart = N->This.LinkOffsets.NextEnd = 0;
N->Next.LinkOffsets.PrevStart = N->Next.LinkOffsets.PrevEnd = N->Next.LinkOffsets.NextStart = N->Next.LinkOffsets.NextEnd = 0;
N->Prev.Size = N->This.Size = N->Next.Size = 0;
Clear(N->Prev.BaseFilename, sizeof(N->Prev.BaseFilename));
Clear(N->This.BaseFilename, sizeof(N->This.BaseFilename));
Clear(N->Next.BaseFilename, sizeof(N->Next.BaseFilename));
Clear(N->Prev.Title, sizeof(N->Prev.Title));
Clear(N->This.Title, sizeof(N->This.Title));
Clear(N->Next.Title, sizeof(N->Next.Title));
}
#define PrintNeighbourhood(N) PrintNeighbourhood_(N, __LINE__)
void
PrintNeighbourhood_(neighbourhood *N, int Line)
{
printf( "\n"
" Neighbourhood (line %d):\n", Line);
if(N->PrevIndex >= 0)
{
printf(
" Prev [%d]: %s: %6d %6d %6d %6d\n",
N->PrevIndex, N->Prev.BaseFilename,
N->Prev.LinkOffsets.PrevStart,
N->Prev.LinkOffsets.PrevEnd,
N->Prev.LinkOffsets.NextStart,
N->Prev.LinkOffsets.NextEnd);
}
if(N->ThisIndex >= 0)
{
printf(
" This [%d (pre-deletion %d)]: %s: %6d %6d %6d %6d\n",
N->ThisIndex, N->PreDeletionThisIndex, N->This.BaseFilename,
N->This.LinkOffsets.PrevStart,
N->This.LinkOffsets.PrevEnd,
N->This.LinkOffsets.NextStart,
N->This.LinkOffsets.NextEnd);
}
if(N->NextIndex >= 0)
{
printf(
" Next [%d]: %s: %6d %6d %6d %6d\n",
N->NextIndex, N->Next.BaseFilename,
N->Next.LinkOffsets.PrevStart,
N->Next.LinkOffsets.PrevEnd,
N->Next.LinkOffsets.NextStart,
N->Next.LinkOffsets.NextEnd);
}
printf(
" OffsetModifiers: Prev %6d • This %6d • Next %6d\n"
" PreLinkOffsetTotals: Prev %6d • This %6d • Next %6d\n"
" DeletedEntryWasFirst: %s\n"
" DeletedEntryWasFinal: %s\n"
" FormerIsFirst: %s\n"
" LatterIsFinal: %s\n"
"\n",
N->PrevOffsetModifier, N->ThisOffsetModifier, N->NextOffsetModifier,
N->PreLinkPrevOffsetTotal, N->PreLinkThisOffsetTotal, N->PreLinkNextOffsetTotal,
N->DeletedEntryWasFirst ? "yes" : "no", N->DeletedEntryWasFinal ? "yes" : "no",
N->FormerIsFirst ? "yes" : "no", N->LatterIsFinal ? "yes" : "no");
}
void
SetNeighbour(db_entry *Dest, db_entry *Src)
{
Dest->Size = Src->Size;
Dest->LinkOffsets.PrevStart = Src->LinkOffsets.PrevStart;
Dest->LinkOffsets.PrevEnd = Src->LinkOffsets.PrevEnd;
Dest->LinkOffsets.NextStart = Src->LinkOffsets.NextStart;
Dest->LinkOffsets.NextEnd = Src->LinkOffsets.NextEnd;
CopyString(Dest->BaseFilename, sizeof(Dest->BaseFilename), "%s", Src->BaseFilename);
CopyString(Dest->Title, sizeof(Dest->Title), "%s", Src->Title);
}
void
GetNeighbourhoodForAddition(neighbourhood *N, enum8(edit_types) EditType)
{
db_entry Entry = { };
int EntryIndex;
bool FoundPrev = FALSE;
bool FoundNext = FALSE;
N->FormerIsFirst = TRUE;
EntryIndex = N->ThisIndex - 1;
for(; EntryIndex >= 0; --EntryIndex)
{
Entry = *(db_entry*)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex);
if(Entry.Size > 0)
{
if(!FoundPrev)
{
N->PrevIndex = EntryIndex;
SetNeighbour(&N->Prev, &Entry);
FoundPrev = TRUE;
}
else
{
N->FormerIsFirst = FALSE;
break;
}
}
}
switch(EditType)
{
case EDIT_REINSERTION:
EntryIndex = N->ThisIndex + 1;
break;
case EDIT_ADDITION:
EntryIndex = N->ThisIndex;
break;
}
N->LatterIsFinal = TRUE;
for(; EntryIndex < DB.EntriesHeader.Count;
++EntryIndex)
{
Entry = *(db_entry*)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex);
if(Entry.Size > 0)
{
if(!FoundNext)
{
N->NextIndex = EntryIndex;
SetNeighbour(&N->Next, &Entry);
FoundNext = TRUE;
}
else
{
N->LatterIsFinal = FALSE;
break;
}
}
}
if(EditType == EDIT_ADDITION && FoundNext)
{
++N->NextIndex;
}
}
void
GetNeighbourhoodForDeletion(neighbourhood *N)
{
db_entry Entry = { };
Entry = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * N->ThisIndex);
N->PreDeletionThisIndex = N->ThisIndex;
SetNeighbour(&N->This, &Entry);
int EntryIndex;
N->DeletedEntryWasFirst = TRUE;
N->FormerIsFirst = TRUE;
bool FoundPrev = FALSE;
for(EntryIndex = N->PreDeletionThisIndex - 1; EntryIndex >= 0; --EntryIndex)
{
Entry = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex);
if(Entry.Size > 0)
{
if(!FoundPrev)
{
FoundPrev = TRUE;
N->DeletedEntryWasFirst = FALSE;
N->PrevIndex = EntryIndex;
SetNeighbour(&N->Prev, &Entry);
}
else
{
N->FormerIsFirst = FALSE;
break;
}
}
}
N->DeletedEntryWasFinal = TRUE;
N->LatterIsFinal = TRUE;
bool FoundNext = FALSE;
for(EntryIndex = N->PreDeletionThisIndex + 1; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex)
{
Entry = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex);
if(Entry.Size > 0)
{
if(!FoundNext)
{
FoundNext = TRUE;
N->DeletedEntryWasFinal = FALSE;
N->NextIndex = EntryIndex - 1;
SetNeighbour(&N->Next, &Entry);
}
else
{
N->LatterIsFinal = FALSE;
break;
}
}
}
}
void
GetNeighbourhood(neighbourhood *N, enum8(edit_types) EditType)
{
if(EditType == EDIT_DELETION)
{
GetNeighbourhoodForDeletion(N);
}
else
{
GetNeighbourhoodForAddition(N, EditType);
}
}
void
SnipeEntryIntoMetadataBuffer(db_entry *Entry, int EntryIndex)
{
*(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex) = *Entry;
}
int
InsertIntoDB(neighbourhood *N, buffers *CollationBuffers, template *BespokeTemplate, char *BaseFilename, bool RecheckingPrivacy, bool *Reinserting)
{
enum8(edit_types) EditType = EDIT_APPEND;
int EntryInsertionStart = StringLength("---\n");
int EntryInsertionEnd;
if(DB.EntriesHeader.Count > 0)
{
db_entry *Entry = { 0 };
N->ThisIndex = BinarySearchForMetadataEntry(&Entry, BaseFilename);
if(Entry)
{
// Reinsert
*Reinserting = TRUE;
EntryInsertionStart = AccumulateDBEntryInsertionOffset(N->ThisIndex);
EntryInsertionEnd = EntryInsertionStart + Entry->Size;
EditType = EDIT_REINSERTION;
}
else
{
if(N->ThisIndex == -1) { ++N->ThisIndex; } // NOTE(matt): BinarySearchForMetadataEntry returns -1 if search term precedes the set
Entry = (db_entry*)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * N->ThisIndex);
if(StringsDiffer(BaseFilename, Entry->BaseFilename) < 0)
{
// Insert
EditType = EDIT_INSERTION;
}
else
{
// Append
++N->ThisIndex;
EditType = EDIT_APPEND;
}
EntryInsertionStart = AccumulateDBEntryInsertionOffset(N->ThisIndex);
}
GetNeighbourhood(N, *Reinserting ? EDIT_REINSERTION : EDIT_ADDITION);
}
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(!VideoIsPrivate) { ClearCopyStringNoFormat(N->This.Title, sizeof(N->This.Title), CollationBuffers->Title); }
if(EditType == EDIT_REINSERTION)
{
// NOTE(matt): To save opening the DB.Metadata file, we defer sniping N->This in until InsertNeighbourLink()
if(!VideoIsPrivate)
{
if(!(DB.File.Handle = fopen(DB.File.Path, "w"))) { return RC_ERROR_FILE; }
fwrite(DB.File.Buffer.Location, EntryInsertionStart, 1, DB.File.Handle);
fwrite(CollationBuffers->SearchEntry.Location, N->This.Size, 1, DB.File.Handle);
fwrite(DB.File.Buffer.Location + EntryInsertionEnd, DB.File.FileSize - EntryInsertionEnd, 1, DB.File.Handle);
fclose(DB.File.Handle);
if(N->This.Size == EntryInsertionEnd - EntryInsertionStart)
{
DB.File.Buffer.Ptr = DB.File.Buffer.Location + EntryInsertionStart;
CopyBufferSized(&DB.File.Buffer, &CollationBuffers->SearchEntry, N->This.Size);
}
else
{
FreeBuffer(&DB.File.Buffer);
ReadFileIntoBuffer(&DB.File, 0);
}
}
}
else
{
int ExistingEntryCount = DB.EntriesHeader.Count;
++DB.EntriesHeader.Count;
if(!(DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"))) { return RC_ERROR_FILE; }
fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle);
fwrite(&DB.EntriesHeader, sizeof(DB.EntriesHeader), 1, DB.Metadata.Handle);
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader);
fwrite(DB.Metadata.Buffer.Ptr,
sizeof(DB.Entry),
N->ThisIndex,
DB.Metadata.Handle);
DB.Metadata.Buffer.Ptr += sizeof(DB.Entry) * N->ThisIndex;
fwrite(&N->This, sizeof(DB.Entry), 1, DB.Metadata.Handle);
fwrite(DB.Metadata.Buffer.Ptr,
sizeof(DB.Entry),
ExistingEntryCount - N->ThisIndex,
DB.Metadata.Handle);
DB.Metadata.Buffer.Ptr += sizeof(DB.Entry) * (ExistingEntryCount - N->ThisIndex);
fwrite(DB.Metadata.Buffer.Ptr,
DB.Metadata.FileSize - (DB.Metadata.Buffer.Ptr - DB.Metadata.Buffer.Location),
1,
DB.Metadata.Handle);
CycleFile(&DB.Metadata);
if(!(DB.File.Handle = fopen(DB.File.Path, "w"))) { return RC_ERROR_FILE; }
fwrite(DB.File.Buffer.Location,
DB.File.FileSize - (DB.File.FileSize - EntryInsertionStart),
1, DB.File.Handle);
fwrite(CollationBuffers->SearchEntry.Location, N->This.Size, 1, DB.File.Handle);
fwrite(DB.File.Buffer.Location + EntryInsertionStart, DB.File.FileSize - EntryInsertionStart, 1, DB.File.Handle);
CycleFile(&DB.File);
}
if(!VideoIsPrivate)
{
LogError(LOG_NOTICE, "%s %s - %s", EditTypes[EditType].Name, BaseFilename, CollationBuffers->Title);
fprintf(stderr, "%s%s%s %s - %s\n", ColourStrings[EditTypes[EditType].Colour], EditTypes[EditType].Name, ColourStrings[CS_END], BaseFilename, CollationBuffers->Title);
}
else if(!RecheckingPrivacy)
{
LogError(LOG_NOTICE, "Privately %s %s", EditTypes[EditType].Name, BaseFilename);
fprintf(stderr, "%sPrivately %s%s %s\n", ColourStrings[CS_PRIVATE], EditTypes[EditType].Name, ColourStrings[CS_END], BaseFilename);
}
// TODO(matt): Remove VideoIsPrivate in favour of generating a player page in a random location
return VideoIsPrivate ? RC_PRIVATE_VIDEO : RC_SUCCESS;
}
void
WritePastAssetsHeader(void)
{
DB.Metadata.Handle = fopen(DB.Metadata.Path, "w");
fwrite(DB.Metadata.Buffer.Location,
sizeof(DB.Header)
+ sizeof(DB.EntriesHeader)
+ sizeof(DB.Entry) * DB.EntriesHeader.Count
+ sizeof(DB.AssetsHeader),
1,
DB.Metadata.Handle);
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location
+ sizeof(DB.Header)
+ sizeof(DB.EntriesHeader)
+ sizeof(DB.Entry) * DB.EntriesHeader.Count;
DB.AssetsHeader = *(db_header_assets *)DB.Metadata.Buffer.Ptr;
DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader);
}
void
PrintLandmarks(void *FirstLandmark, int LandmarkCount)
{
printf("PrintLandmarks()\n");
for(int i = 0; i < LandmarkCount; ++i)
{
db_landmark Landmark = *(db_landmark *)(FirstLandmark + sizeof(Landmark) * i);
printf(" %4d %d\n", Landmark.EntryIndex, Landmark.Position);
}
}
void
ProcessPrevLandmarks(neighbourhood *N, void *FirstLandmark, int ExistingLandmarkCount, landmark_range *CurrentTarget, int *RunningIndex)
{
if(N->PrevIndex >= 0)
{
landmark_range FormerTarget = BinarySearchForMetadataLandmark(FirstLandmark, N->PrevIndex, ExistingLandmarkCount);
fwrite(FirstLandmark, sizeof(db_landmark), FormerTarget.First, DB.Metadata.Handle);
*RunningIndex += FormerTarget.First;
for(int j = 0; j < FormerTarget.Length; ++j, ++*RunningIndex)
{
db_landmark Landmark = *(db_landmark *)(FirstLandmark + sizeof(Landmark) * *RunningIndex);
if(Landmark.Position >= N->PreLinkPrevOffsetTotal)
{
Landmark.Position += N->PrevOffsetModifier;
}
fwrite(&Landmark, sizeof(Landmark), 1, DB.Metadata.Handle);
}
}
else
{
fwrite(FirstLandmark + sizeof(db_landmark) * *RunningIndex, sizeof(db_landmark), CurrentTarget->First, DB.Metadata.Handle);
*RunningIndex += CurrentTarget->First;
}
}
void
ProcessNextLandmarks(neighbourhood *N, void *FirstLandmark, int ExistingLandmarkCount, int *RunningIndex, enum8(edit_types) EditType)
{
if(N->NextIndex >= 0 && *RunningIndex < ExistingLandmarkCount)
{
db_landmark Landmark = *(db_landmark *)(FirstLandmark + sizeof(Landmark) * *RunningIndex);
landmark_range LatterTarget = BinarySearchForMetadataLandmark(FirstLandmark, Landmark.EntryIndex, ExistingLandmarkCount);
for(int j = 0; j < LatterTarget.Length; ++j, ++*RunningIndex)
{
Landmark = *(db_landmark *)(FirstLandmark + sizeof(Landmark) * *RunningIndex);
if(Landmark.Position >= N->PreLinkNextOffsetTotal)
{
Landmark.Position += N->NextOffsetModifier;
}
switch(EditType)
{
case EDIT_DELETION: --Landmark.EntryIndex; break;
case EDIT_ADDITION: ++Landmark.EntryIndex; break;
}
fwrite(&Landmark, sizeof(Landmark), 1, DB.Metadata.Handle);
}
for(; *RunningIndex < ExistingLandmarkCount; ++*RunningIndex)
{
Landmark = *(db_landmark *)(FirstLandmark + sizeof(Landmark) * *RunningIndex);
switch(EditType)
{
case EDIT_DELETION: --Landmark.EntryIndex; break;
case EDIT_ADDITION: ++Landmark.EntryIndex; break;
}
fwrite(&Landmark, sizeof(Landmark), 1, DB.Metadata.Handle);
}
}
}
void
DeleteStaleAssets(void)
{
LocateAssetsBlock();
char *AssetsHeaderLocation = DB.Metadata.Buffer.Ptr;
DB.AssetsHeader = *(db_header_assets *)AssetsHeaderLocation;
int AssetDeletionCount = 0;
int AssetDeletionLocations[DB.AssetsHeader.Count];
DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader);
for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex)
{
DB.Asset = *(db_asset*)DB.Metadata.Buffer.Ptr;
if(DB.Asset.LandmarkCount == 0)
{
AssetDeletionLocations[AssetDeletionCount] = DB.Metadata.Buffer.Ptr - DB.Metadata.Buffer.Location;
++AssetDeletionCount;
--DB.AssetsHeader.Count;
}
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset) + sizeof(DB.Landmark) * DB.Asset.LandmarkCount;
}
if(AssetDeletionCount > 0)
{
DB.Metadata.Handle = fopen(DB.Metadata.Path, "w");
fwrite(DB.Metadata.Buffer.Location, AssetsHeaderLocation - DB.Metadata.Buffer.Location, 1, DB.Metadata.Handle);
fwrite(&DB.AssetsHeader, sizeof(DB.AssetsHeader), 1, DB.Metadata.Handle);
int WrittenBytes = (AssetsHeaderLocation - DB.Metadata.Buffer.Location) + sizeof(DB.AssetsHeader);
for(int DeletionIndex = 0; DeletionIndex < AssetDeletionCount; ++DeletionIndex)
{
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + AssetDeletionLocations[DeletionIndex];
fwrite(DB.Metadata.Buffer.Location + WrittenBytes,
(DB.Metadata.Buffer.Ptr - DB.Metadata.Buffer.Location) - WrittenBytes,
1,
DB.Metadata.Handle);
WrittenBytes += (DB.Metadata.Buffer.Ptr - DB.Metadata.Buffer.Location) - WrittenBytes + sizeof(DB.Asset);
}
fwrite(DB.Metadata.Buffer.Location + WrittenBytes, DB.Metadata.FileSize - WrittenBytes, 1, DB.Metadata.Handle);
CycleFile(&DB.Metadata);
}
}
void
DeleteStaleLandmarks(void)
{
WritePastAssetsHeader();
for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex)
{
DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr;
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset) + sizeof(DB.Landmark) * DB.Asset.LandmarkCount;
DB.Asset.LandmarkCount = 0;
fwrite(&DB.Asset, sizeof(DB.Asset), 1, DB.Metadata.Handle);
}
CycleFile(&DB.Metadata);
}
void
DeleteLandmarks(neighbourhood *N)
{
for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex)
{
DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr;
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset);
db_landmark *FirstLandmark = (db_landmark *)DB.Metadata.Buffer.Ptr;
int ExistingLandmarkCount = DB.Asset.LandmarkCount;
int RunningIndex = 0;
landmark_range DeletionTarget = BinarySearchForMetadataLandmark(FirstLandmark, N->PreDeletionThisIndex, ExistingLandmarkCount);
DB.Asset.LandmarkCount -= DeletionTarget.Length;
fwrite(&DB.Asset, sizeof(DB.Asset), 1, DB.Metadata.Handle);
ProcessPrevLandmarks(N, FirstLandmark, ExistingLandmarkCount, &DeletionTarget, &RunningIndex);
RunningIndex += DeletionTarget.Length;
ProcessNextLandmarks(N, FirstLandmark, ExistingLandmarkCount, &RunningIndex, EDIT_DELETION);
DB.Metadata.Buffer.Ptr += sizeof(db_landmark) * ExistingLandmarkCount;
}
CycleFile(&DB.Metadata);
}
void UpdateLandmarksForNeighbourhood(neighbourhood *N, enum8(edit_types) EditType);
void
AddLandmarks(neighbourhood *N, enum8(edit_types) EditType)
{
for(int i = 0; i < Assets.Count; ++i)
{
Assets.Asset[i].Known = FALSE;
}
for(int StoredAssetIndex = 0; StoredAssetIndex < DB.AssetsHeader.Count; ++StoredAssetIndex)
{
DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr;
int ExistingLandmarkCount = DB.Asset.LandmarkCount;
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset);
void *FirstLandmark;
if(ExistingLandmarkCount > 0)
{
FirstLandmark = DB.Metadata.Buffer.Ptr;
}
int RunningIndex = 0;
for(int i = 0; i < Assets.Count; ++i)
{
if(!StringsDiffer(DB.Asset.Filename, Assets.Asset[i].Filename) && DB.Asset.Type == Assets.Asset[i].Type)
{
Assets.Asset[i].Known = TRUE;
if(!Assets.Asset[i].OffsetLandmarks)
{
DB.Asset.LandmarkCount += Assets.Asset[i].PlayerLandmarkCount;
landmark_range ThisTarget;
if(ExistingLandmarkCount > 0)
{
ThisTarget = BinarySearchForMetadataLandmark(FirstLandmark, N->ThisIndex, ExistingLandmarkCount);
if(EditType == EDIT_REINSERTION) { DB.Asset.LandmarkCount -= ThisTarget.Length; }
}
fwrite(&DB.Asset, sizeof(DB.Asset), 1, DB.Metadata.Handle);
if(ExistingLandmarkCount > 0)
{
ProcessPrevLandmarks(N, FirstLandmark, ExistingLandmarkCount, &ThisTarget, &RunningIndex);
}
for(int j = 0; j < Assets.Asset[i].PlayerLandmarkCount; ++j)
{
db_landmark Landmark;
Landmark.EntryIndex = N->ThisIndex;
Landmark.Position = Assets.Asset[i].PlayerLandmark[j];
fwrite(&Landmark, sizeof(Landmark), 1, DB.Metadata.Handle);
}
if(ExistingLandmarkCount > 0)
{
if(EditType == EDIT_REINSERTION) { RunningIndex += ThisTarget.Length; }
ProcessNextLandmarks(N, FirstLandmark, ExistingLandmarkCount, &RunningIndex, EditType);
}
Assets.Asset[i].OffsetLandmarks = TRUE;
}
else
{
fwrite(&DB.Asset, sizeof(DB.Asset), 1, DB.Metadata.Handle);
fwrite(DB.Metadata.Buffer.Ptr, sizeof(db_landmark) * ExistingLandmarkCount, 1, DB.Metadata.Handle);
}
break;
}
}
DB.Metadata.Buffer.Ptr += sizeof(db_landmark) * ExistingLandmarkCount;
}
CycleFile(&DB.Metadata);
bool NewAsset = FALSE;
for(int i = 0; i < Assets.Count; ++i)
{
if(!Assets.Asset[i].Known && Assets.Asset[i].PlayerLandmarkCount > 0)
{
UpdateAssetInDB(i);
NewAsset = TRUE;
}
}
if(NewAsset) {
UpdateLandmarksForNeighbourhood(N, EDIT_REINSERTION); // NOTE(matt): EDIT_REINSERTION this time because all existing
// assets will have been updated in the first pass
}
}
void
UpdateLandmarksForNeighbourhood(neighbourhood *N, enum8(edit_types) EditType)
{
if(!(Config.Mode & MODE_NOREVVEDRESOURCE))
{
WritePastAssetsHeader();
switch(EditType)
{
case EDIT_DELETION: DeleteLandmarks(N); break;
case EDIT_ADDITION: case EDIT_REINSERTION: { AddLandmarks(N, EditType); break; }
}
}
}
void
DeleteLandmarksForSearch(void)
{
if(!(Config.Mode & MODE_NOREVVEDRESOURCE))
{
WritePastAssetsHeader();
for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex)
{
DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr;
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset);
db_landmark *FirstLandmark = (db_landmark *)DB.Metadata.Buffer.Ptr;
int ExistingLandmarkCount = DB.Asset.LandmarkCount;
landmark_range DeletionTarget = BinarySearchForMetadataLandmark(FirstLandmark, PAGE_TYPE_SEARCH, ExistingLandmarkCount);
DB.Asset.LandmarkCount -= DeletionTarget.Length;
fwrite(&DB.Asset, sizeof(DB.Asset), 1, DB.Metadata.Handle);
DB.Metadata.Buffer.Ptr += sizeof(DB.Landmark) * DeletionTarget.Length;
fwrite(DB.Metadata.Buffer.Ptr, sizeof(DB.Landmark), ExistingLandmarkCount - DeletionTarget.Length, DB.Metadata.Handle);
DB.Metadata.Buffer.Ptr += sizeof(DB.Landmark) * ExistingLandmarkCount - DeletionTarget.Length;
}
CycleFile(&DB.Metadata);
}
}
void
UpdateLandmarksForSearch(void)
{
if(!(Config.Mode & MODE_NOREVVEDRESOURCE))
{
for(int i = 0; i < Assets.Count; ++i)
{
Assets.Asset[i].Known = FALSE;
}
WritePastAssetsHeader();
for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex)
{
DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr;
int ExistingLandmarkCount = DB.Asset.LandmarkCount;
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset);
void *FirstLandmark = DB.Metadata.Buffer.Ptr;
for(int i = 0; i < Assets.Count; ++i)
{
if(!StringsDiffer(DB.Asset.Filename, Assets.Asset[i].Filename) && DB.Asset.Type == Assets.Asset[i].Type)
{
Assets.Asset[i].Known = TRUE;
landmark_range Target = BinarySearchForMetadataLandmark(FirstLandmark, PAGE_TYPE_SEARCH, DB.Asset.LandmarkCount);
DB.Asset.LandmarkCount += Assets.Asset[i].SearchLandmarkCount - Target.Length;
fwrite(&DB.Asset, sizeof(DB.Asset), 1, DB.Metadata.Handle);
for(int j = 0; j < Assets.Asset[i].SearchLandmarkCount; ++j)
{
DB.Landmark.EntryIndex = PAGE_TYPE_SEARCH;
DB.Landmark.Position = Assets.Asset[i].SearchLandmark[j];
fwrite(&DB.Landmark, sizeof(DB.Landmark), 1, DB.Metadata.Handle);
}
DB.Metadata.Buffer.Ptr += sizeof(DB.Landmark) * (Target.First + Target.Length);
fwrite(DB.Metadata.Buffer.Ptr, sizeof(DB.Landmark), ExistingLandmarkCount - (Target.First + Target.Length), DB.Metadata.Handle);
DB.Metadata.Buffer.Ptr += sizeof(DB.Landmark) * (ExistingLandmarkCount - (Target.First + Target.Length));
break;
}
}
}
CycleFile(&DB.Metadata);
bool NewAsset = FALSE;
for(int InternalAssetIndex = 0; InternalAssetIndex < Assets.Count; ++InternalAssetIndex)
{
if(!Assets.Asset[InternalAssetIndex].Known && Assets.Asset[InternalAssetIndex].SearchLandmarkCount > 0)
{
NewAsset = TRUE;
UpdateAssetInDB(InternalAssetIndex);
}
}
if(NewAsset) { UpdateLandmarksForSearch(); }
}
}
enum
{
LINK_INCLUDE,
LINK_EXCLUDE
} link_types;
enum
{
LINK_FORWARDS,
LINK_BACKWARDS
} link_directions;
int
InsertNeighbourLink(db_entry *From, int FromIndex, db_entry *To, enum8(link_directions) LinkDirection, bool FromHasOneNeighbour)
{
file_buffer HTML;
if(ReadPlayerPageIntoBuffer(&HTML, From) == RC_SUCCESS)
{
if(!(HTML.Handle = fopen(HTML.Path, "w"))) { FreeBuffer(&HTML.Buffer); return RC_ERROR_FILE; };
buffer Link;
ClaimBuffer(&Link, "Link", Kilobytes(4));
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);
}
int NewPrevEnd = 0;
int NewNextEnd = 0;
switch(LinkDirection)
{
case LINK_BACKWARDS:
{
fwrite(HTML.Buffer.Location, From->LinkOffsets.PrevStart, 1, HTML.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", DB.EntriesHeader.ProjectName);
}
NewPrevEnd = Link.Ptr - Link.Location;
fwrite(Link.Location, (Link.Ptr - Link.Location), 1, HTML.Handle);
if(FromHasOneNeighbour)
{
fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd, From->LinkOffsets.NextStart, 1, HTML.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", DB.EntriesHeader.ProjectName);
NewNextEnd = Link.Ptr - Link.Location;
fwrite(Link.Location, NewNextEnd, 1, HTML.Handle);
fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd,
HTML.FileSize - (From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd),
1,
HTML.Handle);
}
else
{
fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd,
HTML.FileSize - (From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd),
1,
HTML.Handle);
}
From->LinkOffsets.PrevEnd = NewPrevEnd;
if(FromHasOneNeighbour) { From->LinkOffsets.NextEnd = NewNextEnd; }
} break;
case LINK_FORWARDS:
{
if(FromHasOneNeighbour)
{
fwrite(HTML.Buffer.Location, From->LinkOffsets.PrevStart, 1, HTML.Handle);
CopyStringToBuffer(&Link,
" <div class=\"episodeMarker first\"><div>&#8226;</div><div>Welcome to <cite>%s</cite></div><div>&#8226;</div></div>\n", DB.EntriesHeader.ProjectName);
NewPrevEnd = Link.Ptr - Link.Location;
fwrite(Link.Location, NewPrevEnd, 1, HTML.Handle);
RewindBuffer(&Link);
fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd,
From->LinkOffsets.NextStart, 1, HTML.Handle);
}
else
{
fwrite(HTML.Buffer.Location, From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart, 1, HTML.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", DB.EntriesHeader.ProjectName);
}
NewNextEnd = Link.Ptr - Link.Location;
fwrite(Link.Location, (Link.Ptr - Link.Location), 1, HTML.Handle);
fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd,
HTML.FileSize - (From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd),
1,
HTML.Handle);
if(FromHasOneNeighbour) { From->LinkOffsets.PrevEnd = NewPrevEnd; }
From->LinkOffsets.NextEnd = NewNextEnd;
} break;
}
if(To) { DeclaimBuffer(&ToPlayerURL); }
DeclaimBuffer(&Link);
fclose(HTML.Handle);
FreeBuffer(&HTML.Buffer);
SnipeEntryIntoMetadataBuffer(From, FromIndex);
return RC_SUCCESS;
}
else
{
return RC_ERROR_FILE;
}
}
int
DeleteNeighbourLinks(neighbourhood *N)
{
db_entry *Entry;
int EntryIndex;
if(N->PrevIndex >= 0)
{
Entry = &N->Prev;
EntryIndex = N->PrevIndex;
N->PreLinkPrevOffsetTotal = N->Prev.LinkOffsets.PrevEnd
+ N->Prev.LinkOffsets.NextStart
+ N->Prev.LinkOffsets.NextEnd;
}
else
{
Entry = &N->Next;
EntryIndex = N->NextIndex;
N->PreLinkNextOffsetTotal = N->Next.LinkOffsets.PrevEnd
+ N->Next.LinkOffsets.NextStart
+ N->Next.LinkOffsets.NextEnd;
}
file_buffer HTML;
if(ReadPlayerPageIntoBuffer(&HTML, Entry) == RC_SUCCESS)
{
if(!(HTML.Handle = fopen(HTML.Path, "w"))) { FreeBuffer(&HTML.Buffer); return RC_ERROR_FILE; };
fwrite(HTML.Buffer.Location, Entry->LinkOffsets.PrevStart, 1, HTML.Handle);
fwrite(HTML.Buffer.Location + Entry->LinkOffsets.PrevStart + Entry->LinkOffsets.PrevEnd, Entry->LinkOffsets.NextStart, 1, HTML.Handle);
fwrite(HTML.Buffer.Location + Entry->LinkOffsets.PrevStart + Entry->LinkOffsets.PrevEnd + Entry->LinkOffsets.NextStart + Entry->LinkOffsets.NextEnd,
HTML.FileSize - (Entry->LinkOffsets.PrevStart + Entry->LinkOffsets.PrevEnd + Entry->LinkOffsets.NextStart + Entry->LinkOffsets.NextEnd),
1,
HTML.Handle);
fclose(HTML.Handle);
Entry->LinkOffsets.PrevEnd = 0;
Entry->LinkOffsets.NextEnd = 0;
if(N->PrevIndex >= 0)
{
N->PrevOffsetModifier = N->Prev.LinkOffsets.PrevEnd
+ N->Prev.LinkOffsets.NextStart
+ N->Prev.LinkOffsets.NextEnd
- N->PreLinkPrevOffsetTotal;
}
else
{
N->NextOffsetModifier = N->Next.LinkOffsets.PrevEnd
+ N->Next.LinkOffsets.NextStart
+ N->Next.LinkOffsets.NextEnd
- N->PreLinkNextOffsetTotal;
}
FreeBuffer(&HTML.Buffer);
SnipeEntryIntoMetadataBuffer(Entry, EntryIndex);
}
return RC_SUCCESS;
}
void
LinkToNewEntry(neighbourhood *N)
{
N->PreLinkThisOffsetTotal = N->This.LinkOffsets.PrevEnd
+ N->This.LinkOffsets.NextStart
+ N->This.LinkOffsets.NextEnd;
if(N->PrevIndex >= 0)
{
N->PreLinkPrevOffsetTotal = N->Prev.LinkOffsets.PrevEnd
+ N->Prev.LinkOffsets.NextStart
+ N->Prev.LinkOffsets.NextEnd;
InsertNeighbourLink(&N->Prev, N->PrevIndex, &N->This, LINK_FORWARDS, N->FormerIsFirst);
N->PrevOffsetModifier = N->Prev.LinkOffsets.PrevEnd
+ N->Prev.LinkOffsets.NextStart
+ N->Prev.LinkOffsets.NextEnd
- N->PreLinkPrevOffsetTotal;
}
if(N->NextIndex >= 0)
{
N->PreLinkNextOffsetTotal = N->Next.LinkOffsets.PrevEnd
+ N->Next.LinkOffsets.NextStart
+ N->Next.LinkOffsets.NextEnd;
InsertNeighbourLink(&N->Next, N->NextIndex, &N->This, LINK_BACKWARDS, N->LatterIsFinal);
N->NextOffsetModifier = N->Next.LinkOffsets.PrevEnd
+ N->Next.LinkOffsets.NextStart
+ N->Next.LinkOffsets.NextEnd
- N->PreLinkNextOffsetTotal;
}
}
void
MarkNextAsFirst(neighbourhood *N)
{
file_buffer HTML;
ReadPlayerPageIntoBuffer(&HTML, &N->Next);
buffer Link;
ClaimBuffer(&Link, "Link", Kilobytes(4));
HTML.Handle = fopen(HTML.Path, "w");
fwrite(HTML.Buffer.Location, N->Next.LinkOffsets.PrevStart, 1, HTML.Handle);
CopyStringToBuffer(&Link,
" <div class=\"episodeMarker first\"><div>&#8226;</div><div>Welcome to <cite>%s</cite></div><div>&#8226;</div></div>\n", DB.EntriesHeader.ProjectName);
fwrite(Link.Location, (Link.Ptr - Link.Location), 1, HTML.Handle);
fwrite(HTML.Buffer.Location + N->Next.LinkOffsets.PrevStart + N->Next.LinkOffsets.PrevEnd,
HTML.FileSize - (N->Next.LinkOffsets.PrevStart + N->Next.LinkOffsets.PrevEnd),
1,
HTML.Handle);
N->Next.LinkOffsets.PrevEnd = Link.Ptr - Link.Location;
DeclaimBuffer(&Link);
fclose(HTML.Handle);
FreeBuffer(&HTML.Buffer);
SnipeEntryIntoMetadataBuffer(&N->Next, N->NextIndex);
}
void
MarkPrevAsFinal(neighbourhood *N)
{
file_buffer File;
ReadPlayerPageIntoBuffer(&File, &N->Prev);
File.Handle = fopen(File.Path, "w");
buffer Link;
ClaimBuffer(&Link, "Link", Kilobytes(4));
fwrite(File.Buffer.Location, N->Prev.LinkOffsets.PrevStart + N->Prev.LinkOffsets.PrevEnd + N->Prev.LinkOffsets.NextStart, 1, File.Handle);
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", DB.EntriesHeader.ProjectName);
fwrite(Link.Location, (Link.Ptr - Link.Location), 1, File.Handle);
fwrite(File.Buffer.Location + N->Prev.LinkOffsets.PrevStart + N->Prev.LinkOffsets.PrevEnd + N->Prev.LinkOffsets.NextStart + N->Prev.LinkOffsets.NextEnd,
File.FileSize - (N->Prev.LinkOffsets.PrevStart + N->Prev.LinkOffsets.PrevEnd + N->Prev.LinkOffsets.NextStart + N->Prev.LinkOffsets.NextEnd),
1,
File.Handle);
N->Prev.LinkOffsets.NextEnd = Link.Ptr - Link.Location;
DeclaimBuffer(&Link);
fclose(File.Handle);
FreeBuffer(&File.Buffer);
SnipeEntryIntoMetadataBuffer(&N->Prev, N->PrevIndex);
}
void
LinkOverDeletedEntry(neighbourhood *N)
{
if(DB.EntriesHeader.Count == 1)
{
DeleteNeighbourLinks(N);
}
else
{
if(N->DeletedEntryWasFirst)
{
N->PreLinkNextOffsetTotal = N->Next.LinkOffsets.PrevEnd
+ N->Next.LinkOffsets.NextStart
+ N->Next.LinkOffsets.NextEnd;
MarkNextAsFirst(N);
N->NextOffsetModifier = N->Next.LinkOffsets.PrevEnd
+ N->Next.LinkOffsets.NextStart
+ N->Next.LinkOffsets.NextEnd
- N->PreLinkNextOffsetTotal;
return;
}
else if(N->DeletedEntryWasFinal)
{
N->PreLinkPrevOffsetTotal = N->Prev.LinkOffsets.PrevEnd
+ N->Prev.LinkOffsets.NextStart
+ N->Prev.LinkOffsets.NextEnd;
MarkPrevAsFinal(N);
N->PrevOffsetModifier = N->Prev.LinkOffsets.PrevEnd
+ N->Prev.LinkOffsets.NextStart
+ N->Prev.LinkOffsets.NextEnd
- N->PreLinkPrevOffsetTotal;
return;
}
else
{
// Assert(N->PrevIndex >= 0 && N->NextIndex >= 0)
N->PreLinkPrevOffsetTotal = N->Prev.LinkOffsets.PrevEnd
+ N->Prev.LinkOffsets.NextStart
+ N->Prev.LinkOffsets.NextEnd;
N->PreLinkNextOffsetTotal = N->Next.LinkOffsets.PrevEnd
+ N->Next.LinkOffsets.NextStart
+ N->Next.LinkOffsets.NextEnd;
}
InsertNeighbourLink(&N->Prev, N->PrevIndex, &N->Next, LINK_FORWARDS, N->FormerIsFirst);
InsertNeighbourLink(&N->Next, N->NextIndex, &N->Prev, LINK_BACKWARDS, N->LatterIsFinal);
N->PrevOffsetModifier = N->Prev.LinkOffsets.PrevEnd
+ N->Prev.LinkOffsets.NextStart
+ N->Prev.LinkOffsets.NextEnd
- N->PreLinkPrevOffsetTotal;
N->NextOffsetModifier = N->Next.LinkOffsets.PrevEnd
+ N->Next.LinkOffsets.NextStart
+ N->Next.LinkOffsets.NextEnd
- N->PreLinkNextOffsetTotal;
}
}
void
LinkNeighbours(neighbourhood *N, enum8(link_types) LinkType)
{
if(LinkType == LINK_INCLUDE)
{
LinkToNewEntry(N);
}
else
{
LinkOverDeletedEntry(N);
}
}
void
DeleteSearchPageFromFilesystem() // NOTE(matt): Do we need to handle relocating, like the PlayerPage function?
{
buffer SearchDirectory;
ClaimBuffer(&SearchDirectory, "SearchDirectory", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + 10);
ConstructDirectoryPath(&SearchDirectory, PAGE_SEARCH, Config.SearchLocation, "");
char SearchPagePath[1024];
CopyString(SearchPagePath, sizeof(SearchPagePath), "%s/index.html", SearchDirectory.Location);
remove(SearchPagePath);
remove(SearchDirectory.Location);
DeclaimBuffer(&SearchDirectory);
}
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", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10);
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, "%sMostly deleted%s %s. %sUnable to remove directory%s %s: %s", ColourStrings[EditTypes[EDIT_DELETION].Colour], ColourStrings[CS_END], BaseFilename, ColourStrings[CS_ERROR], ColourStrings[CS_END], OutputDirectoryPath.Location, strerror(errno));
}
else
{
if(!Relocating)
{
LogError(LOG_INFORMATIONAL, "Deleted %s", BaseFilename);
fprintf(stderr, "%sDeleted%s %s\n", ColourStrings[EditTypes[EDIT_DELETION].Colour], ColourStrings[CS_END], BaseFilename);
}
}
}
DeclaimBuffer(&OutputDirectoryPath);
return RC_SUCCESS;
}
int
DeleteFromDB(neighbourhood *N, char *BaseFilename)
{
// TODO(matt): LogError()
DB.Header = *(db_header *)DB.Metadata.Buffer.Location;
DB.EntriesHeader = *(db_header_entries *)(DB.Metadata.Buffer.Location + sizeof(DB.Header));
db_entry *Entry = { 0 };
int EntryIndex = BinarySearchForMetadataEntry(&Entry, BaseFilename);
if(Entry)
{
int DeleteFileFrom = AccumulateDBEntryInsertionOffset(EntryIndex);
int DeleteFileTo = DeleteFileFrom + Entry->Size;
N->ThisIndex = EntryIndex;
GetNeighbourhood(N, EDIT_DELETION);
--DB.EntriesHeader.Count;
if(DB.EntriesHeader.Count == 0)
{
// TODO(matt): Handle this differently, allowing 0 entries but > 0 assets?
DeleteSearchPageFromFilesystem();
remove(DB.Metadata.Path);
DB.Metadata.FileSize = 0;
FreeBuffer(&DB.Metadata.Buffer);
remove(DB.File.Path);
DB.File.FileSize = 0;
FreeBuffer(&DB.File.Buffer);
}
else
{
if(!(DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"))) { FreeBuffer(&DB.Metadata.Buffer); return RC_ERROR_FILE; }
if(!(DB.File.Handle = fopen(DB.File.Path, "w"))) { FreeBuffer(&DB.File.Buffer); return RC_ERROR_FILE; }
fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle);
fwrite(&DB.EntriesHeader, sizeof(DB.EntriesHeader), 1, DB.Metadata.Handle);
fwrite(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader), sizeof(DB.Entry) * EntryIndex, 1, DB.Metadata.Handle);
fwrite(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * (EntryIndex + 1),
DB.Metadata.FileSize - sizeof(DB.Header) - sizeof(DB.EntriesHeader) - sizeof(DB.Entry) * (EntryIndex + 1),
1, DB.Metadata.Handle);
CycleFile(&DB.Metadata);
fwrite(DB.File.Buffer.Location, DeleteFileFrom, 1, DB.File.Handle);
fwrite(DB.File.Buffer.Location + DeleteFileTo, DB.File.FileSize - DeleteFileTo, 1, DB.File.Handle);
CycleFile(&DB.File);
}
}
return Entry ? RC_SUCCESS : RC_NOOP;
}
int
SearchToBuffer(buffers *CollationBuffers) // NOTE(matt): This guy malloc's CollationBuffers->Search
{
if(DB.Metadata.Buffer.Location)
{
DB.Header = *(db_header *)DB.Metadata.Buffer.Location;
DB.EntriesHeader = *(db_header_entries *)(DB.Metadata.Buffer.Location + sizeof(DB.Header));
if(DB.EntriesHeader.Count > 0)
{
RewindBuffer(&CollationBuffers->IncludesSearch);
buffer URL;
ConstructResolvedAssetURL(&URL, ASSET_CSS_CINERA, PAGE_SEARCH);
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
"<meta charset=\"UTF-8\">\n"
" <meta name=\"generator\" content=\"Cinera %d.%d.%d\">\n"
"\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
CINERA_APP_VERSION.Major,
CINERA_APP_VERSION.Minor,
CINERA_APP_VERSION.Patch,
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesSearch, ASSET_CSS_CINERA, PAGE_SEARCH);
ConstructResolvedAssetURL(&URL, ASSET_CSS_THEME, PAGE_SEARCH);
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
"\">\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesSearch, ASSET_CSS_THEME, PAGE_SEARCH);
ConstructResolvedAssetURL(&URL, ASSET_CSS_TOPICS, PAGE_SEARCH);
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
"\">\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesSearch, ASSET_CSS_TOPICS, PAGE_SEARCH);
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
"\">\n");
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);
ConstructResolvedAssetURL(&URL, ASSET_JS_SEARCH, PAGE_SEARCH);
buffer Script;
ClaimBuffer(&Script, "Script", (117 + URL.Ptr - URL.Location) * 2); // NOTE(matt): Update the size if changing the string
CopyStringToBuffer(&Script,
" </div>\n"
" </div>\n"
" <script type=\"text/javascript\" src=\"%s", URL.Location);
// NOTE(matt): DeclaimBuffer(&URL) happens later, after Script is declaimed because Script was only claimed here
PushAssetLandmark(&Script, ASSET_JS_SEARCH, PAGE_SEARCH);
CopyStringToBuffer(&Script,
"\"></script>");
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];
db_entry *This;
char Text[(ProjectUnit ? StringLength(ProjectUnit) : 0) + sizeof(Number) + sizeof(This->Title) + 3];
int EntryLength = StringLength(PlayerURL.Location) + sizeof(Text) + 82;
CollationBuffers->Search.Size = StringLength(queryContainer) + (DB.EntriesHeader.Count * EntryLength) + Script.Ptr - Script.Location;
if(!(CollationBuffers->Search.Location = malloc(CollationBuffers->Search.Size))) { return RC_ERROR_MEMORY; }
CollationBuffers->Search.ID = "Search";
CollationBuffers->Search.Ptr = CollationBuffers->Search.Location;
CopyStringToBuffer(&CollationBuffers->Search, "%s", queryContainer);
int ProjectIDLength = StringLength(Config.ProjectID);
bool SearchRequired = FALSE;
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader);
for(int EntryIndex = 0; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex, DB.Metadata.Buffer.Ptr += sizeof(DB.Entry))
{
This = (db_entry *)DB.Metadata.Buffer.Ptr;
if(This->Size > 0)
{
SearchRequired = 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->Search,
" <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->Search, Text);
CopyStringToBuffer(&CollationBuffers->Search,
"</a>\n"
" </div>\n");
}
else
{
CopyStringToBuffer(&CollationBuffers->Search,
" <div>\n"
" <a href=\"%s\">", PlayerURL.Location);
CopyStringToBufferHTMLSafe(&CollationBuffers->Search, This->Title);
CopyStringToBuffer(&CollationBuffers->Search,
"</a>\n"
" </div>\n");
}
}
}
OffsetLandmarks(&CollationBuffers->Search, ASSET_JS_SEARCH, PAGE_SEARCH);
CopyBuffer(&CollationBuffers->Search, &Script);
DeclaimBuffer(&PlayerURL);
DeclaimBuffer(&Script);
DeclaimBuffer(&URL);
if(!SearchRequired) { return RC_NOOP; }
else { return RC_SUCCESS; }
}
}
return RC_NOOP;
}
int
GeneratePlayerPage(neighbourhood *N, buffers *CollationBuffers, template *PlayerTemplate, char *BaseFilename, bool Reinserting)
{
buffer OutputDirectoryPath;
ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10);
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 SearchInTemplate = FALSE;
for(int TagIndex = 0; TagIndex < PlayerTemplate->Metadata.TagCount; ++TagIndex)
{
if(PlayerTemplate->Metadata.Tags[TagIndex].TagCode == TAG_SEARCH)
{
SearchInTemplate = TRUE;
SearchToBuffer(CollationBuffers);
break;
}
}
BuffersToHTML(CollationBuffers, PlayerTemplate, PlayerPagePath, PAGE_PLAYER, &N->This.LinkOffsets.PrevStart);
// NOTE(matt): A previous InsertNeighbourLink() call will have done SnipeEntryIntoMetadataBuffer(), but we must do it here now
// that PrevStart has been adjusted by BuffersToHTML()
SnipeEntryIntoMetadataBuffer(&N->This, N->ThisIndex);
UpdateLandmarksForNeighbourhood(N, Reinserting ? EDIT_REINSERTION : EDIT_ADDITION);
if(SearchInTemplate)
{
FreeBuffer(&CollationBuffers->Search);
}
return RC_SUCCESS;
}
int
GenerateSearchPage(buffers *CollationBuffers, template *SearchTemplate)
{
buffer OutputDirectoryPath;
ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + 10);
ConstructDirectoryPath(&OutputDirectoryPath, PAGE_SEARCH, Config.SearchLocation, 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 SearchPagePath[1024];
CopyString(SearchPagePath, sizeof(SearchPagePath), "%s/index.html", OutputDirectoryPath.Location);
DeclaimBuffer(&OutputDirectoryPath);
switch(SearchToBuffer(CollationBuffers))
{
case RC_SUCCESS:
{
BuffersToHTML(CollationBuffers, SearchTemplate, SearchPagePath, PAGE_SEARCH, 0);
UpdateLandmarksForSearch();
break;
}
case RC_NOOP:
{
DeleteSearchPageFromFilesystem();
DeleteLandmarksForSearch();
break;
}
}
FreeBuffer(&CollationBuffers->Search);
return RC_SUCCESS;
}
int
DeleteEntry(neighbourhood *Neighbourhood, char *BaseFilename)
{
if(DeleteFromDB(Neighbourhood, BaseFilename) == RC_SUCCESS)
{
LinkNeighbours(Neighbourhood, LINK_EXCLUDE);
DeletePlayerPageFromFilesystem(BaseFilename, Config.PlayerLocation, FALSE);
UpdateLandmarksForNeighbourhood(Neighbourhood, EDIT_DELETION);
return RC_SUCCESS;
}
return RC_NOOP;
}
int
InsertEntry(neighbourhood *Neighbourhood, buffers *CollationBuffers, template *PlayerTemplate, template *BespokeTemplate, char *BaseFilename, bool RecheckingPrivacy)
{
bool Reinserting = FALSE;
if(InsertIntoDB(Neighbourhood, CollationBuffers, BespokeTemplate, BaseFilename, RecheckingPrivacy, &Reinserting) == RC_SUCCESS)
{
LinkNeighbours(Neighbourhood, LINK_INCLUDE);
if(BespokeTemplate->File.Buffer.Location)
{
GeneratePlayerPage(Neighbourhood, CollationBuffers, BespokeTemplate, BaseFilename, Reinserting);
FreeTemplate(BespokeTemplate);
}
else
{
GeneratePlayerPage(Neighbourhood, CollationBuffers, PlayerTemplate, BaseFilename, Reinserting);
}
return RC_SUCCESS;
}
return RC_NOOP;
}
int
RecheckPrivacy(buffers *CollationBuffers, template *SearchTemplate, template *PlayerTemplate, template *BespokeTemplate)
{
if(DB.Metadata.FileSize > 0)
{
DB.Header = *(db_header *)DB.Metadata.Buffer.Location;
DB.EntriesHeader = *(db_header_entries *)(DB.Metadata.Buffer.Location + sizeof(DB.Header));
db_entry Entry = { };
int PrivateEntryIndex = 0;
db_entry PrivateEntries[DB.EntriesHeader.Count];
bool Inserted = FALSE;
for(int IndexEntry = 0; IndexEntry < DB.EntriesHeader.Count; ++IndexEntry)
{
Entry = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * IndexEntry);
if(Entry.Size == 0)
{
PrivateEntries[PrivateEntryIndex] = Entry;
++PrivateEntryIndex;
}
}
for(int i = 0; i < PrivateEntryIndex; ++i)
{
neighbourhood Neighbourhood = { };
InitNeighbourhood(&Neighbourhood);
ResetAssetLandmarks();
Inserted = (InsertEntry(&Neighbourhood, CollationBuffers, PlayerTemplate, BespokeTemplate, PrivateEntries[i].BaseFilename, TRUE) == RC_SUCCESS);
}
if(Inserted)
{
GenerateSearchPage(CollationBuffers, SearchTemplate);
DeleteStaleAssets();
}
LastPrivacyCheck = time(0);
}
return RC_SUCCESS;
}
#define DEBUG_LANDMARKS 0
#if DEBUG_LANDMARKS
void
VerifyLandmarks(void)
{
printf("\n"
"VerifyLandmarks()\n");
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location;
DB.Header = *(db_header *)DB.Metadata.Buffer.Ptr;
DB.Metadata.Buffer.Ptr += sizeof(DB.Header);
DB.EntriesHeader = *(db_header_entries *)DB.Metadata.Buffer.Ptr;
DB.Metadata.Buffer.Ptr += sizeof(DB.EntriesHeader);
char *FirstEntry = DB.Metadata.Buffer.Ptr;
DB.Metadata.Buffer.Ptr += sizeof(DB.Entry) * DB.EntriesHeader.Count;
DB.AssetsHeader = *(db_header_assets *)DB.Metadata.Buffer.Ptr;
DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader);
char *FirstAsset = DB.Metadata.Buffer.Ptr;
buffer OutputDirectoryPath;
ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1 + 10);
bool UniversalSuccess = TRUE;
// Search
{
file_buffer HTML;
ReadSearchPageIntoBuffer(&HTML);
char *Cursor = FirstAsset;
for(int j = 0; j < DB.AssetsHeader.Count; ++j)
{
DB.Asset = *(db_asset *)Cursor;
Cursor += sizeof(DB.Asset);
landmark_range Target = BinarySearchForMetadataLandmark(Cursor, PAGE_TYPE_SEARCH, DB.Asset.LandmarkCount);
for(int k = 0; k < Target.Length; ++k)
{
DB.Landmark = *(db_landmark *)(Cursor + sizeof(db_landmark) * (Target.First + k));
bool Found = FALSE;
for(int l = 0; l < Assets.Count; ++l)
{
// TODO(matt): StringToInt (base16)
char HashString[9];
sprintf(HashString, "%08x", Assets.Asset[l].Hash);
if(HTML.Buffer.Size >= DB.Landmark.Position + 8 && !StringsDifferT(HashString, HTML.Buffer.Location + DB.Landmark.Position, 0))
{
Found = TRUE;
break;
}
}
if(!Found)
{
printf("Failure!!!\n");
printf(" %s\n", HTML.Path);
printf(" %.*s [at byte %d] is not a known asset hash\n", 8, HTML.Buffer.Location + DB.Landmark.Position, DB.Landmark.Position);
UniversalSuccess = FALSE;
}
}
Cursor += sizeof(DB.Landmark) * DB.Asset.LandmarkCount;
}
FreeBuffer(&HTML.Buffer);
}
for(int i = 0; i < DB.EntriesHeader.Count; ++i)
{
DB.Entry = *(db_entry *)(FirstEntry + sizeof(DB.Entry) * i);
ConstructDirectoryPath(&OutputDirectoryPath, PAGE_PLAYER, Config.PlayerLocation, DB.Entry.BaseFilename);
file_buffer HTML;
CopyString(HTML.Path, sizeof(HTML.Path), "%s/index.html", OutputDirectoryPath.Location);
if(ReadFileIntoBuffer(&HTML, 0) == RC_SUCCESS)
{
char *Cursor = FirstAsset;
for(int j = 0; j < DB.AssetsHeader.Count; ++j)
{
DB.Asset = *(db_asset *)Cursor;
Cursor += sizeof(DB.Asset);
landmark_range Target = BinarySearchForMetadataLandmark(Cursor, i, DB.Asset.LandmarkCount);
for(int k = 0; k < Target.Length; ++k)
{
DB.Landmark = *(db_landmark *)(Cursor + sizeof(db_landmark) * (Target.First + k));
bool Found = FALSE;
for(int l = 0; l < Assets.Count; ++l)
{
// TODO(matt): StringToInt (base16)
char HashString[9];
sprintf(HashString, "%08x", Assets.Asset[l].Hash);
if((HTML.Buffer.Size >= DB.Landmark.Position + 8) && !StringsDifferT(HashString, HTML.Buffer.Location + DB.Landmark.Position, 0))
{
Found = TRUE;
break;
}
}
if(!Found)
{
printf("%sFailure ↓%s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END]);
printf(" %s\n", HTML.Path);
printf(" %.*s [at byte %d] is not a known asset hash\n", 8, HTML.Buffer.Location + DB.Landmark.Position, DB.Landmark.Position);
UniversalSuccess = FALSE;
}
}
Cursor += sizeof(DB.Landmark) * DB.Asset.LandmarkCount;
}
FreeBuffer(&HTML.Buffer);
}
else
{
printf("%sFailed to open%s %s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], HTML.Path);
}
}
Assert(UniversalSuccess);
printf("Success! All landmarks correspond to asset hashes\n");
DeclaimBuffer(&OutputDirectoryPath);
}
#endif
void
UpdateDeferredAssetChecksums(void)
{
for(int i = 0; i < Assets.Count; ++i)
{
if(Assets.Asset[i].DeferredUpdate)
{
UpdateAssetInDB(i);
}
}
}
#define DEBUG_EVENTS 0
#if DEBUG_EVENTS
void
PrintEvent(struct inotify_event *Event, int EventIndex)
{
printf("\nEvent[%d]\n"
" wd: %d\n"
" mask: %d\n",
EventIndex,
Event->wd,
Event->mask);
if(Event->mask & IN_ACCESS) { printf(" IN_ACCESS\n"); }
if(Event->mask & IN_ATTRIB) { printf(" IN_ATTRIB\n"); }
if(Event->mask & IN_CLOSE_WRITE) { printf(" IN_CLOSE_WRITE\n"); }
if(Event->mask & IN_CLOSE_NOWRITE) { printf(" IN_CLOSE_NOWRITE\n"); }
if(Event->mask & IN_CREATE) { printf(" IN_CREATE\n"); }
if(Event->mask & IN_DELETE) { printf(" IN_DELETE\n"); }
if(Event->mask & IN_DELETE_SELF) { printf(" IN_DELETE_SELF\n"); }
if(Event->mask & IN_MODIFY) { printf(" IN_MODIFY\n"); }
if(Event->mask & IN_MOVE_SELF) { printf(" IN_MOVE_SELF\n"); }
if(Event->mask & IN_MOVED_FROM) { printf(" IN_MOVED_FROM\n"); }
if(Event->mask & IN_MOVED_TO) { printf(" IN_MOVED_TO\n"); }
if(Event->mask & IN_OPEN) { printf(" IN_OPEN\n"); }
if(Event->mask & IN_MOVE) { printf(" IN_MOVE\n"); }
if(Event->mask & IN_CLOSE) { printf(" IN_CLOSE\n"); }
if(Event->mask & IN_DONT_FOLLOW) { printf(" IN_DONT_FOLLOW\n"); }
if(Event->mask & IN_EXCL_UNLINK) { printf(" IN_EXCL_UNLINK\n"); }
if(Event->mask & IN_MASK_ADD) { printf(" IN_MASK_ADD\n"); }
if(Event->mask & IN_ONESHOT) { printf(" IN_ONESHOT\n"); }
if(Event->mask & IN_ONLYDIR) { printf(" IN_ONLYDIR\n"); }
if(Event->mask & IN_IGNORED) { printf(" IN_IGNORED\n"); }
if(Event->mask & IN_ISDIR) { printf(" IN_ISDIR\n"); }
if(Event->mask & IN_Q_OVERFLOW) { printf(" IN_Q_OVERFLOW\n"); }
if(Event->mask & IN_UNMOUNT) { printf(" IN_UNMOUNT\n"); }
printf( " cookie: %d\n"
" len: %d\n"
" name: %s\n",
Event->cookie,
Event->len,
Event->name);
}
#endif
int
MonitorFilesystem(buffers *CollationBuffers, template *SearchTemplate, template *PlayerTemplate, template *BespokeTemplate)
{
buffer Events;
if(ClaimBuffer(&Events, "inotify Events", Kilobytes(4)) == RC_ARENA_FULL) { return RC_ARENA_FULL; };
Clear(Events.Location, Events.Size);
struct inotify_event *Event;
int BytesRead = read(inotifyInstance, Events.Location, Events.Size); // TODO(matt): Handle error EINVAL
if(inotifyInstance < 0) { perror("MonitorFilesystem()"); }
// TODO(matt): Test this with longer update intervals, and combinations of events...
#if DEBUG_EVENTS
int i = 0;
#endif
bool Deleted = FALSE;
bool Inserted = FALSE;
bool UpdatedAsset = FALSE;
for(Events.Ptr = Events.Location;
Events.Ptr - Events.Location < BytesRead && Events.Ptr - Events.Location < Events.Size;
Events.Ptr += sizeof(struct inotify_event) + Event->len
#if DEBUG_EVENTS
, ++i
#endif
)
{
Event = (struct inotify_event *)Events.Ptr;
#if DEBUG_EVENTS
PrintEvent(Event, i);
#endif
//PrintWatchHandles();
for(int WatchHandleIndex = 0; WatchHandleIndex < WatchHandles.Count; ++WatchHandleIndex)
{
if(Event->wd == WatchHandles.Handle[WatchHandleIndex].Descriptor)
{
switch(WatchHandles.Handle[WatchHandleIndex].Type)
{
case WT_HMML:
{
char *Ptr = Event->name;
Ptr += (StringLength(Event->name) - StringLength(".hmml"));
if(!(StringsDiffer(Ptr, ".hmml")))
{
*Ptr = '\0';
neighbourhood Neighbourhood = { };
InitNeighbourhood(&Neighbourhood);
ResetAssetLandmarks();
if(Event->mask & IN_DELETE)
{
Deleted |= (DeleteEntry(&Neighbourhood, Event->name) == RC_SUCCESS);
}
else if(Event->mask & IN_CLOSE_WRITE)
{
Inserted |= (InsertEntry(&Neighbourhood, CollationBuffers, PlayerTemplate, BespokeTemplate, Event->name, 0) == RC_SUCCESS);
}
}
} break;
case WT_ASSET:
{
for(int i = 0; i < Assets.Count; ++i)
{
if(!StringsDiffer(Event->name, Assets.Asset[i].Filename + Assets.Asset[i].FilenameAt))
{
UpdateAsset(i, 0);
UpdatedAsset = TRUE;
break;
}
}
} break;
}
break;
}
}
}
if(Deleted || Inserted)
{
UpdateDeferredAssetChecksums();
GenerateSearchPage(CollationBuffers, SearchTemplate);
DeleteStaleAssets();
#if DEBUG_LANDMARKS
VerifyLandmarks();
#endif
}
if(UpdatedAsset)
{
#if DEBUG_LANDMARKS
VerifyLandmarks();
#endif
}
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, "%sUnable to remove directory%s %s: %s\n", ColourStrings[CS_WARNING], ColourStrings[CS_END], Path, strerror(errno));
return RC_ERROR_DIRECTORY;
}
else
{
LogError(LOG_INFORMATIONAL, "Deleted %s", Path);
//fprintf(stderr, "%sDeleted%s %s\n", ColourStrings[CS_DELETION], ColourStrings[CS_END], 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(int OriginalDBVersion)
{
// NOTE(matt): For each new DB version, we must declare and initialise one instance of each preceding version, only cast the
// incoming DB to the type of the OriginalDBVersion, and move into the final case all operations on that incoming DB
bool OnlyHeaderChanged = TRUE;
int OriginalHeaderSize = 0;
database1 DB1 = { };
database2 DB2 = { };
database3 DB3 = { };
switch(OriginalDBVersion)
{
case 1:
{
OriginalHeaderSize = sizeof(db_header1);
DB1.Header = *(db_header1 *)DB.Metadata.Buffer.Location;
DB2.Header.DBVersion = CINERA_DB_VERSION;
DB2.Header.AppVersion = CINERA_APP_VERSION;
DB2.Header.HMMLVersion.Major = hmml_version.Major;
DB2.Header.HMMLVersion.Minor = hmml_version.Minor;
DB2.Header.HMMLVersion.Patch = hmml_version.Patch;
DB2.Header.EntryCount = DB1.Header.EntryCount;
Clear(DB2.Header.SearchLocation, sizeof(DB2.Header.SearchLocation));
Clear(DB2.Header.PlayerLocation, sizeof(DB2.Header.PlayerLocation));
}
case 2:
{
if(OriginalDBVersion == 2)
{
OriginalHeaderSize = sizeof(db_header2);
DB2.Header = *(db_header2 *)DB.Metadata.Buffer.Location;
DB.Header.InitialDBVersion = DB2.Header.DBVersion;
DB.Header.InitialAppVersion = DB2.Header.AppVersion;
DB.Header.InitialHMMLVersion.Major = DB2.Header.HMMLVersion.Major;
DB.Header.InitialHMMLVersion.Minor = DB2.Header.HMMLVersion.Minor;
DB.Header.InitialHMMLVersion.Patch = DB2.Header.HMMLVersion.Patch;
}
DB.EntriesHeader.Count = DB2.Header.EntryCount;
ClearCopyStringNoFormat(DB.EntriesHeader.ProjectID, sizeof(DB.EntriesHeader.ProjectID), Config.ProjectID);
Clear(DB.EntriesHeader.ProjectName, sizeof(DB.EntriesHeader.ProjectName));
for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex)
{
if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID))
{
CopyString(DB.EntriesHeader.ProjectName, sizeof(DB.EntriesHeader.ProjectName), "%s", ProjectInfo[ProjectIndex].FullName);
break;
}
}
ClearCopyStringNoFormat(DB.EntriesHeader.BaseURL, sizeof(DB.EntriesHeader.BaseURL), Config.BaseURL);
ClearCopyStringNoFormat(DB.EntriesHeader.SearchLocation, sizeof(DB.EntriesHeader.SearchLocation), DB2.Header.SearchLocation);
ClearCopyStringNoFormat(DB.EntriesHeader.PlayerLocation, sizeof(DB.EntriesHeader.PlayerLocation), DB2.Header.PlayerLocation);
ClearCopyStringNoFormat(DB.EntriesHeader.PlayerURLPrefix, sizeof(DB.EntriesHeader.PlayerURLPrefix), Config.PlayerURLPrefix);
OnlyHeaderChanged = FALSE;
if(!(DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"))) { FreeBuffer(&DB.Metadata.Buffer); return RC_ERROR_FILE; }
fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle);
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + OriginalHeaderSize;
DB.File.Buffer.Ptr += StringLength("---\n");
for(int EntryIndex = 0; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex)
{
// NOTE(matt): We can use either db_entry1 or 2 here because they are the same
db_entry2 This = *(db_entry2 *)DB.Metadata.Buffer.Ptr;
DB.Entry.LinkOffsets.PrevStart = 0;
DB.Entry.LinkOffsets.NextStart = 0;
DB.Entry.LinkOffsets.PrevEnd = 0;
DB.Entry.LinkOffsets.NextEnd = 0;
DB.Entry.Size = This.Size;
ClearCopyStringNoFormat(DB.Entry.BaseFilename, sizeof(DB.Entry.BaseFilename), This.BaseFilename);
char *EntryStart = DB.File.Buffer.Ptr;
SeekBufferForString(&DB.File.Buffer, "title: \"", C_SEEK_FORWARDS, C_SEEK_AFTER);
Clear(DB.Entry.Title, sizeof(DB.Entry.Title));
CopyStringNoFormatT(DB.Entry.Title, sizeof(DB.Entry.Title), DB.File.Buffer.Ptr, '\n');
DB.Entry.Title[StringLength(DB.Entry.Title) - 1] = '\0';
fwrite(&DB.Entry, sizeof(DB.Entry), 1, DB.Metadata.Handle);
DB.Metadata.Buffer.Ptr += sizeof(This);
EntryStart += This.Size;
DB.File.Buffer.Ptr = EntryStart;
}
CycleFile(&DB.Metadata);
}
case 3:
{
OriginalHeaderSize = sizeof(db_header3);
DB3.Header = *(db_header3 *)DB.Metadata.Buffer.Location;
DB.Header.HexSignature = FOURCC("CNRA");
DB.Header.CurrentDBVersion = CINERA_DB_VERSION;
DB.Header.CurrentAppVersion = CINERA_APP_VERSION;
DB.Header.CurrentHMMLVersion.Major = hmml_version.Major;
DB.Header.CurrentHMMLVersion.Minor = hmml_version.Minor;
DB.Header.CurrentHMMLVersion.Patch = hmml_version.Patch;
DB.Header.InitialDBVersion = DB3.Header.InitialDBVersion;
DB.Header.InitialAppVersion = DB3.Header.InitialAppVersion;
DB.Header.InitialHMMLVersion.Major = DB3.Header.InitialHMMLVersion.Major;
DB.Header.InitialHMMLVersion.Minor = DB3.Header.InitialHMMLVersion.Minor;
DB.Header.InitialHMMLVersion.Patch = DB3.Header.InitialHMMLVersion.Patch;
DB.Header.BlockCount = 0;
// TODO(matt): Consider allowing zero NTRY or ASET blocks in a future version
DB.EntriesHeader.BlockID = FOURCC("NTRY");
DB.EntriesHeader.Count = DB3.Header.EntryCount;
ClearCopyStringNoFormat(DB.EntriesHeader.ProjectID, sizeof(DB.EntriesHeader.ProjectID), DB3.Header.ProjectID);
ClearCopyStringNoFormat(DB.EntriesHeader.ProjectName, sizeof(DB.EntriesHeader.ProjectName), DB3.Header.ProjectName);
ClearCopyStringNoFormat(DB.EntriesHeader.BaseURL, sizeof(DB.EntriesHeader.BaseURL), DB3.Header.BaseURL);
ClearCopyStringNoFormat(DB.EntriesHeader.SearchLocation, sizeof(DB.EntriesHeader.SearchLocation), DB3.Header.SearchLocation);
ClearCopyStringNoFormat(DB.EntriesHeader.PlayerLocation, sizeof(DB.EntriesHeader.PlayerLocation), DB3.Header.PlayerLocation);
ClearCopyStringNoFormat(DB.EntriesHeader.PlayerURLPrefix, sizeof(DB.EntriesHeader.PlayerURLPrefix), DB3.Header.PlayerURLPrefix);
++DB.Header.BlockCount;
DB.AssetsHeader.BlockID = FOURCC("ASET");
DB.AssetsHeader.Count = 0;
ClearCopyStringNoFormat(DB.AssetsHeader.RootDir, sizeof(DB.AssetsHeader.RootDir), Config.RootDir);
ClearCopyStringNoFormat(DB.AssetsHeader.RootURL, sizeof(DB.AssetsHeader.RootURL), Config.RootURL);
ClearCopyStringNoFormat(DB.AssetsHeader.CSSDir, sizeof(DB.AssetsHeader.CSSDir), Config.CSSDir);
ClearCopyStringNoFormat(DB.AssetsHeader.ImagesDir, sizeof(DB.AssetsHeader.ImagesDir), Config.ImagesDir);
ClearCopyStringNoFormat(DB.AssetsHeader.JSDir, sizeof(DB.AssetsHeader.JSDir), Config.JSDir);
++DB.Header.BlockCount;
OnlyHeaderChanged = FALSE;
if(!(DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"))) { FreeBuffer(&DB.Metadata.Buffer); return RC_ERROR_FILE; }
fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle);
fwrite(&DB.EntriesHeader, sizeof(DB.EntriesHeader), 1, DB.Metadata.Handle);
fwrite(DB.Metadata.Buffer.Location + OriginalHeaderSize,
sizeof(DB.Entry),
DB.EntriesHeader.Count,
DB.Metadata.Handle);
fwrite(&DB.AssetsHeader, sizeof(DB.AssetsHeader), 1, DB.Metadata.Handle);
CycleFile(&DB.Metadata);
}
// TODO(matt); DBVersion 4 is the first that uses HexSignatures
// We should try and deprecate earlier versions and just enforce a clean rebuild for invalid DB files
// Perhaps do this either the next time we have to bump the version, or when we scrap Single Edition
}
if(OnlyHeaderChanged)
{
if(!(DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"))) { FreeBuffer(&DB.Metadata.Buffer); return RC_ERROR_FILE; }
fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle);
fwrite(DB.Metadata.Buffer.Location + OriginalHeaderSize, DB.Metadata.FileSize - OriginalHeaderSize, 1, DB.Metadata.Handle);
CycleFile(&DB.Metadata);
}
fprintf(stderr, "\n%sUpgraded Cinera DB from %d to %d!%s\n\n", ColourStrings[CS_SUCCESS], OriginalDBVersion, DB.Header.CurrentDBVersion, ColourStrings[CS_END]);
return RC_SUCCESS;
}
typedef struct
{
bool Present;
char ID[32];
} entry_presence_id; // 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
DeleteDeadDBEntries(void)
{
bool NewPlayerLocation = FALSE;
bool NewSearchLocation = FALSE;
if(StringsDiffer(DB.EntriesHeader.PlayerLocation, Config.PlayerLocation))
{
buffer OldPlayerDirectory;
ClaimBuffer(&OldPlayerDirectory, "OldPlayerDirectory",
MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1);
ConstructDirectoryPath(&OldPlayerDirectory, PAGE_PLAYER, DB.EntriesHeader.PlayerLocation, 0);
buffer NewPlayerDirectory;
ClaimBuffer(&NewPlayerDirectory, "NewPlayerDirectory",
MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_PLAYER_URL_PREFIX_LENGTH + MAX_BASE_FILENAME_LENGTH + 1);
ConstructDirectoryPath(&NewPlayerDirectory, PAGE_PLAYER, Config.PlayerLocation, 0);
printf("%sRelocating Player Page%s from %s to %s%s\n",
ColourStrings[CS_REINSERTION], DB.EntriesHeader.Count > 1 ? "s" : "",
OldPlayerDirectory.Location, NewPlayerDirectory.Location, ColourStrings[CS_END]);
DeclaimBuffer(&NewPlayerDirectory);
for(int EntryIndex = 0; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex)
{
db_entry This = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * EntryIndex);
ConstructDirectoryPath(&OldPlayerDirectory, PAGE_PLAYER, DB.EntriesHeader.PlayerLocation, This.BaseFilename);
DeletePlayerPageFromFilesystem(This.BaseFilename, DB.EntriesHeader.PlayerLocation, TRUE);
}
ConstructDirectoryPath(&OldPlayerDirectory, PAGE_PLAYER, DB.EntriesHeader.PlayerLocation, 0);
if(StringLength(DB.EntriesHeader.PlayerLocation) > 0)
{
RemoveChildDirectories(OldPlayerDirectory, Config.BaseDir);
}
DeclaimBuffer(&OldPlayerDirectory);
ClearCopyStringNoFormat(DB.EntriesHeader.PlayerLocation, sizeof(DB.EntriesHeader.PlayerLocation), Config.PlayerLocation);
*(db_header *)DB.Metadata.Buffer.Location = DB.Header;
NewPlayerLocation = TRUE;
}
if(StringsDiffer(DB.EntriesHeader.SearchLocation, Config.SearchLocation))
{
buffer OldSearchDirectory;
ClaimBuffer(&OldSearchDirectory, "OldSearchDirectory", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1);
ConstructDirectoryPath(&OldSearchDirectory, PAGE_SEARCH, DB.EntriesHeader.SearchLocation, 0);
buffer NewSearchDirectory;
ClaimBuffer(&NewSearchDirectory, "NewSearchDirectory", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1);
ConstructDirectoryPath(&NewSearchDirectory, PAGE_SEARCH, Config.SearchLocation, 0);
printf("%sRelocating Search Page from %s to %s%s\n",
ColourStrings[CS_REINSERTION], OldSearchDirectory.Location, NewSearchDirectory.Location, ColourStrings[CS_END]);
DeclaimBuffer(&NewSearchDirectory);
char SearchPagePath[2048] = { 0 };
CopyString(SearchPagePath, sizeof(SearchPagePath), "%s/index.html", OldSearchDirectory.Location);
remove(SearchPagePath);
if(StringLength(DB.EntriesHeader.SearchLocation) > 0)
{
RemoveChildDirectories(OldSearchDirectory, Config.BaseDir);
}
DeclaimBuffer(&OldSearchDirectory);
ClearCopyStringNoFormat(DB.EntriesHeader.SearchLocation, sizeof(DB.EntriesHeader.SearchLocation), Config.SearchLocation);
*(db_header *)DB.Metadata.Buffer.Location = DB.Header;
NewSearchLocation = TRUE;
}
if(NewPlayerLocation || NewSearchLocation)
{
if(!(DB.Metadata.Handle = fopen(DB.Metadata.Path, "w"))) { FreeBuffer(&DB.Metadata.Buffer); return RC_ERROR_FILE; }
fwrite(DB.Metadata.Buffer.Location, DB.Metadata.FileSize, 1, DB.Metadata.Handle);
CycleFile(&DB.Metadata);
}
entry_presence_id Entries[DB.EntriesHeader.Count];
for(int EntryIndex = 0; EntryIndex < DB.EntriesHeader.Count; ++EntryIndex)
{
db_entry This = *(db_entry *)(DB.Metadata.Buffer.Location + sizeof(DB.Header) + sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * 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 < DB.EntriesHeader.Count; ++i)
{
if(!StringsDiffer(Entries[i].ID, ProjectFiles->d_name))
{
Entries[i].Present = TRUE;
break;
}
}
}
}
closedir(ProjectDirHandle);
bool Deleted = FALSE;
for(int i = 0; i < DB.EntriesHeader.Count; ++i)
{
if(Entries[i].Present == FALSE)
{
Deleted = TRUE;
neighbourhood Neighbourhood = { };
InitNeighbourhood(&Neighbourhood);
ResetAssetLandmarks();
DeleteEntry(&Neighbourhood, Entries[i].ID);
}
}
return Deleted ? RC_SUCCESS : RC_NOOP;
}
int
SyncDBWithInput(buffers *CollationBuffers, template *SearchTemplate, template *PlayerTemplate, template *BespokeTemplate)
{
if(DB.Metadata.FileSize > 0 && Config.Mode & MODE_NOREVVEDRESOURCE)
{
DeleteStaleLandmarks();
}
bool Deleted = FALSE;
Deleted = (DB.Metadata.FileSize > 0 && DB.File.FileSize > 0 && DeleteDeadDBEntries() == 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 = { };
InitNeighbourhood(&Neighbourhood);
ResetAssetLandmarks();
Inserted |= (InsertEntry(&Neighbourhood, CollationBuffers, PlayerTemplate, BespokeTemplate, ProjectFiles->d_name, 0) == RC_SUCCESS);
}
}
closedir(ProjectDirHandle);
UpdateDeferredAssetChecksums();
if(Deleted || Inserted)
{
GenerateSearchPage(CollationBuffers, SearchTemplate);
DeleteStaleAssets();
#if DEBUG_LANDMARKS
VerifyLandmarks();
#endif
}
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
InitDB(void)
{
DB.Metadata.Buffer.ID = "DBMetadata";
CopyString(DB.Metadata.Path, sizeof(DB.Metadata.Path), "%s/%s.metadata", Config.BaseDir, Config.ProjectID);
ReadFileIntoBuffer(&DB.Metadata, 0); // NOTE(matt): Could we actually catch errors (permissions?) here and bail?
DB.File.Buffer.ID = "DBFile";
CopyString(DB.File.Path, sizeof(DB.File.Path), "%s/%s.index", Config.BaseDir, Config.ProjectID);
ReadFileIntoBuffer(&DB.File, 0); // NOTE(matt): Could we actually catch errors (permissions?) here and bail?
if(DB.Metadata.Buffer.Location)
{
// TODO(matt): Handle this gracefully (it'll be an invalid file)
Assert(DB.Metadata.FileSize >= sizeof(DB.Header));
uint32_t OriginalDBVersion = 0;
uint32_t FirstInt = *(uint32_t *)DB.Metadata.Buffer.Location;
if(FirstInt != FOURCC("CNRA"))
{
// TODO(matt): More checking, somehow? Ideally this should be able to report "Invalid .metadata file" rather than
// trying to upgrade a file that isn't even valid
// TODO(matt): We should try and deprecate < CINERA_DB_VERSION 4 and enforce a clean rebuild for invalid DB files
// Perhaps do this either the next time we have to bump the version, or when we scrap Single Edition
OriginalDBVersion = FirstInt;
}
else
{
OriginalDBVersion = *(uint32_t *)(DB.Metadata.Buffer.Location + sizeof(DB.Header.HexSignature));
}
if(OriginalDBVersion < CINERA_DB_VERSION)
{
if(CINERA_DB_VERSION == 5)
{
fprintf(stderr, "\n%sHandle conversion from CINERA_DB_VERSION %d to %d!%s\n\n", ColourStrings[CS_ERROR], OriginalDBVersion, CINERA_DB_VERSION, ColourStrings[CS_END]);
exit(RC_ERROR_FATAL);
}
if(UpgradeDB(OriginalDBVersion) == RC_ERROR_FILE) { return RC_NOOP; }
}
else if(OriginalDBVersion > CINERA_DB_VERSION)
{
fprintf(stderr, "%sUnsupported DB Version (%d). Please upgrade Cinera%s\n", ColourStrings[CS_ERROR], OriginalDBVersion, ColourStrings[CS_END]);
exit(RC_ERROR_FATAL);
}
DB.Header = *(db_header *)DB.Metadata.Buffer.Location;
DB.Header.CurrentAppVersion = CINERA_APP_VERSION;
DB.Header.CurrentHMMLVersion.Major = hmml_version.Major;
DB.Header.CurrentHMMLVersion.Minor = hmml_version.Minor;
DB.Header.CurrentHMMLVersion.Patch = hmml_version.Patch;
DB.Metadata.Handle = fopen(DB.Metadata.Path, "w");
fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle);
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location + sizeof(DB.Header);
// NOTE(matt): In the future we may want to support multiple occurrences of a block type, at which point this code
// should continue to work
for(int BlockIndex = 0; BlockIndex < DB.Header.BlockCount; ++BlockIndex)
{
uint32_t BlockID = *(uint32_t *)DB.Metadata.Buffer.Ptr;
if(BlockID == FOURCC("NTRY"))
{
DB.EntriesHeader = *(db_header_entries *)DB.Metadata.Buffer.Ptr;
fwrite(DB.Metadata.Buffer.Ptr,
sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * DB.EntriesHeader.Count,
1,
DB.Metadata.Handle);
DB.Metadata.Buffer.Ptr += sizeof(DB.EntriesHeader) + sizeof(DB.Entry) * DB.EntriesHeader.Count;
}
else if(BlockID == FOURCC("ASET"))
{
DB.AssetsHeader = *(db_header_assets *)DB.Metadata.Buffer.Ptr;
ClearCopyStringNoFormat(DB.AssetsHeader.RootDir, sizeof(DB.AssetsHeader.RootDir), Config.RootDir);
ClearCopyStringNoFormat(DB.AssetsHeader.RootURL, sizeof(DB.AssetsHeader.RootURL), Config.RootURL);
ClearCopyStringNoFormat(DB.AssetsHeader.CSSDir, sizeof(DB.AssetsHeader.CSSDir), Config.CSSDir);
ClearCopyStringNoFormat(DB.AssetsHeader.ImagesDir, sizeof(DB.AssetsHeader.ImagesDir), Config.ImagesDir);
ClearCopyStringNoFormat(DB.AssetsHeader.JSDir, sizeof(DB.AssetsHeader.JSDir), Config.JSDir);
fwrite(&DB.AssetsHeader, sizeof(DB.AssetsHeader), 1, DB.Metadata.Handle);
DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader);
for(int AssetIndex = 0; AssetIndex < DB.AssetsHeader.Count; ++AssetIndex)
{
DB.Asset = *(db_asset *)DB.Metadata.Buffer.Ptr;
fwrite(DB.Metadata.Buffer.Ptr,
sizeof(DB.Asset) + sizeof(DB.Landmark) * DB.Asset.LandmarkCount,
1,
DB.Metadata.Handle);
DB.Metadata.Buffer.Ptr += sizeof(DB.Asset) + sizeof(DB.Landmark) * DB.Asset.LandmarkCount;
}
}
else
{
printf("%sMalformed metadata file%s: %s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], DB.Metadata.Path);
}
}
CycleFile(&DB.Metadata);
}
else
{
// NOTE(matt): Initialising new db_header
DB.Header.HexSignature = FOURCC("CNRA");
DB.Header.InitialDBVersion = DB.Header.CurrentDBVersion = CINERA_DB_VERSION;
DB.Header.InitialAppVersion = DB.Header.CurrentAppVersion = CINERA_APP_VERSION;
DB.Header.InitialHMMLVersion.Major = DB.Header.CurrentHMMLVersion.Major = hmml_version.Major;
DB.Header.InitialHMMLVersion.Minor = DB.Header.CurrentHMMLVersion.Minor = hmml_version.Minor;
DB.Header.InitialHMMLVersion.Patch = DB.Header.CurrentHMMLVersion.Patch = hmml_version.Patch;
DB.Header.BlockCount = 0;
CopyStringNoFormat(DB.EntriesHeader.ProjectID, sizeof(DB.EntriesHeader.ProjectID), Config.ProjectID);
for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex)
{
if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID))
{
CopyStringNoFormat(DB.EntriesHeader.ProjectName, sizeof(DB.EntriesHeader.ProjectName), ProjectInfo[ProjectIndex].FullName);
break;
}
}
// TODO(matt): Consider allowing zero NTRY or ASET blocks in a future version
DB.EntriesHeader.BlockID = FOURCC("NTRY");
DB.EntriesHeader.Count = 0;
CopyStringNoFormat(DB.EntriesHeader.BaseURL, sizeof(DB.EntriesHeader.BaseURL), Config.BaseURL);
CopyStringNoFormat(DB.EntriesHeader.SearchLocation, sizeof(DB.EntriesHeader.SearchLocation), Config.SearchLocation);
CopyStringNoFormat(DB.EntriesHeader.PlayerLocation, sizeof(DB.EntriesHeader.PlayerLocation), Config.PlayerLocation);
CopyStringNoFormat(DB.EntriesHeader.PlayerURLPrefix, sizeof(DB.EntriesHeader.PlayerURLPrefix), Config.PlayerURLPrefix);
++DB.Header.BlockCount;
DB.AssetsHeader.BlockID = FOURCC("ASET");
DB.AssetsHeader.Count = 0;
CopyStringNoFormat(DB.AssetsHeader.RootDir, sizeof(DB.AssetsHeader.RootDir), Config.RootDir);
CopyStringNoFormat(DB.AssetsHeader.RootURL, sizeof(DB.AssetsHeader.RootURL), Config.RootURL);
CopyStringNoFormat(DB.AssetsHeader.CSSDir, sizeof(DB.AssetsHeader.CSSDir), Config.CSSDir);
CopyStringNoFormat(DB.AssetsHeader.ImagesDir, sizeof(DB.AssetsHeader.ImagesDir), Config.ImagesDir);
CopyStringNoFormat(DB.AssetsHeader.JSDir, sizeof(DB.AssetsHeader.JSDir), Config.JSDir);
++DB.Header.BlockCount;
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);
DB.Metadata.Handle = fopen(DB.Metadata.Path, "w");
fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.Handle);
fwrite(&DB.EntriesHeader, sizeof(DB.EntriesHeader), 1, DB.Metadata.Handle);
fwrite(&DB.AssetsHeader, sizeof(DB.AssetsHeader), 1, DB.Metadata.Handle);
fclose(DB.Metadata.Handle);
ReadFileIntoBuffer(&DB.Metadata, 0);
DB.File.Handle = fopen(DB.File.Path, "w");
fprintf(DB.File.Handle, "---\n");
fclose(DB.File.Handle);
ReadFileIntoBuffer(&DB.File, 0);
}
return RC_SUCCESS;
}
void
SetCacheDirectory(config *DefaultConfig)
{
int Flags = WRDE_NOCMD | WRDE_UNDEF | WRDE_APPEND;
wordexp_t Expansions = {};
wordexp("$XDG_CACHE_HOME/cinera", &Expansions, Flags);
wordexp("$HOME/.cache/cinera", &Expansions, Flags);
if(Expansions.we_wordc > 0 ) { CopyString(DefaultConfig->CacheDir, sizeof(DefaultConfig->CacheDir), Expansions.we_wordv[0]); }
wordfree(&Expansions);
}
void
InitMemoryArena(arena *Arena, int Size)
{
Arena->Size = Size;
if(!(Arena->Location = calloc(1, Arena->Size)))
{
exit(RC_ERROR_MEMORY);
}
Arena->Ptr = Arena->Location;
}
int
main(int ArgC, char **Args)
{
// TODO(matt): Read all defaults from the config
config DefaultConfig = {
.RootDir = ".",
.RootURL = "",
.CSSDir = "",
.ImagesDir = "",
.JSDir = "",
.QueryString = "r",
.TemplatesDir = ".",
.TemplateSearchLocation = "",
.TemplatePlayerLocation = "",
.BaseDir = ".",
.BaseURL = "",
.SearchLocation = "",
.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 = ""
};
SetCacheDirectory(&DefaultConfig);
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:qQ:r: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.SearchLocation = 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 'Q':
Config.QueryString = optarg;
if(!StringsDiffer(Config.QueryString, ""))
{
Config.Mode |= MODE_NOREVVEDRESOURCE;
}
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.TemplateSearchLocation = optarg;
break;
case 'y':
Config.TemplatePlayerLocation = optarg;
break;
case 'h':
default:
PrintUsage(Args[0], &DefaultConfig);
return RC_SUCCESS;
}
}
if(Config.Mode & MODE_EXAMINE)
{
// TODO(matt): Allow optionally passing a .metadata file as an argument?
ExamineDB();
exit(RC_SUCCESS);
}
// NOTE(matt): Init MemoryArenas (they are global)
InitMemoryArena(&MemoryArena, Megabytes(4));
#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
//
// IncludesSearch
// Search
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.IncludesSearch, "IncludesSearch", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; };
if(ClaimBuffer(&CollationBuffers.SearchEntry, "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, "%sProjectID \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], Config.ProjectID, StringLength(Config.ProjectID), MAX_PROJECT_ID_LENGTH, ColourStrings[CS_END]);
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, "%sPlayer URL Prefix \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], ProjectInfo[ProjectInfoIndex].AltURLPrefix, StringLength(ProjectInfo[ProjectInfoIndex].AltURLPrefix), MAX_PLAYER_URL_PREFIX_LENGTH, ColourStrings[CS_END]);
HaveConfigErrors = TRUE;
}
else
{
Config.PlayerURLPrefix = ProjectInfo[ProjectInfoIndex].AltURLPrefix;
}
}
if(StringLength(ProjectInfo[ProjectInfoIndex].FullName) > MAX_PROJECT_NAME_LENGTH)
{
fprintf(stderr, "%sProject Name \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], ProjectInfo[ProjectInfoIndex].FullName, StringLength(ProjectInfo[ProjectInfoIndex].FullName), MAX_PROJECT_NAME_LENGTH, ColourStrings[CS_END]);
HaveConfigErrors = TRUE;
}
break;
}
}
if(!KnownProject)
{
fprintf(stderr, "%sMissing Project Info for %s%s %s(-p)%s\n", ColourStrings[CS_ERROR], Config.ProjectID, ColourStrings[CS_END], ColourStrings[CS_COMMENT], ColourStrings[CS_END]);
HaveConfigErrors = TRUE;
}
}
if(StringsDiffer(Config.BaseURL, "") && StringLength(Config.BaseURL) > MAX_BASE_URL_LENGTH)
{
fprintf(stderr, "%sBase URL \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], Config.BaseURL, StringLength(Config.BaseURL), MAX_BASE_URL_LENGTH, ColourStrings[CS_END]);
HaveConfigErrors = TRUE;
}
if(StringsDiffer(Config.SearchLocation, "") && StringLength(Config.SearchLocation) > MAX_RELATIVE_PAGE_LOCATION_LENGTH)
{
fprintf(stderr, "%sRelative Search Page Location \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], Config.SearchLocation, StringLength(Config.SearchLocation), MAX_RELATIVE_PAGE_LOCATION_LENGTH, ColourStrings[CS_END]);
HaveConfigErrors = TRUE;
}
if(StringsDiffer(Config.PlayerLocation, "") && StringLength(Config.PlayerLocation) > MAX_RELATIVE_PAGE_LOCATION_LENGTH)
{
fprintf(stderr, "%sRelative Player Page Location \"%s\" is too long (%d/%d characters)%s\n", ColourStrings[CS_ERROR], Config.PlayerLocation, StringLength(Config.PlayerLocation), MAX_RELATIVE_PAGE_LOCATION_LENGTH, ColourStrings[CS_END]);
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 Search and Player templates if their locations are set
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: %s(XDG_CACHE_HOME)%s\t%s\n"
" Update Interval: %s(-u)%s\t\t%d second%s\n"
"\n"
" Assets Root\n"
" Directory: %s(-r)%s\t\t\t%s\n"
" URL: %s(-R)%s\t\t\t%s\n"
" Paths relative to root\n"
" CSS: %s(-c)%s\t\t\t%s\n"
" Images: %s(-i)%s\t\t\t%s\n"
" JS: %s(-j)%s\t\t\t%s\n"
" Revved resources query string: %s(-Q)%s\t%s\n"
"\n"
"Project\n"
" ID: %s(-p)%s\t\t\t\t%s\n"
" Default Medium: %s(-m)%s\t\t%s\n"
" Style / Theme: %s(-s)%s\t\t\t%s\n"
"\n"
"Input Paths\n"
" Annotations Directory: %s(-d)%s\t\t%s\n"
" Templates Directory: %s(-t)%s\t\t%s\n"
" Search Template: %s(-x)%s\t\t%s\n"
" Player Template: %s(-y)%s\t\t%s\n"
"\n"
"Output Paths\n"
" Base\n"
" Directory: %s(-b)%s\t\t\t%s\n"
" URL: %s(-B)%s\t\t\t%s\n"
" Paths relative to base\n"
" Search Page: %s(-n)%s\t\t%s\n"
/* NOTE(matt): Here, I think, is where we'll split into sub-projects (...really?...) */
" Player Page(s): %s(-a)%s\t\t%s\n"
" Player Page Prefix: %s(hardcoded)%s\t%s\n"
"\n"
"Modes\n"
" Single browser tab: %s(-1)%s\t\t%s\n"
" Force template integration: %s(-f)%s\t%s\n"
" Ignore video privacy status: %s(-g)%s\t%s\n"
" Quit after sync: %s(-q)%s\t\t%s\n"
" Force quote cache rebuild: %s(-w)%s\t%s\n"
"\n",
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.CacheDir,
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.UpdateInterval,
Config.UpdateInterval == 1 ? "" : "s",
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.RootDir,
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.RootURL, "") ? Config.RootURL : "[empty]",
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.CSSDir, "") ? Config.CSSDir : "(same as root)",
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.ImagesDir, "") ? Config.ImagesDir : "(same as root)",
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.JSDir, "") ? Config.JSDir : "(same as root)",
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Mode & MODE_NOREVVEDRESOURCE ? "[disabled]" : Config.QueryString,
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.ProjectID,
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.DefaultMedium,
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Theme,
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.ProjectDir,
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.TemplatesDir,
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.TemplateSearchLocation, "") ? Config.TemplateSearchLocation : "[none set]",
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.TemplatePlayerLocation, "") ? Config.TemplatePlayerLocation : "[none set]",
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.BaseDir,
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.BaseURL, "") ? Config.BaseURL : "[empty]",
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.SearchLocation, "") ? Config.SearchLocation : "(same as base)",
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.PlayerLocation, "") ? Config.PlayerLocation : "(directly descended from base)",
ColourStrings[CS_COMMENT], ColourStrings[CS_END], StringsDiffer(Config.PlayerURLPrefix, "") ? Config.PlayerURLPrefix : Config.ProjectID,
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Mode & MODE_SINGLETAB ? "on" : "off",
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Mode & MODE_FORCEINTEGRATION ? "on" : "off",
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Mode & MODE_NOPRIVACY ? "on" : "off",
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Mode & MODE_ONESHOT ? "on" : "off",
ColourStrings[CS_COMMENT], ColourStrings[CS_END], Config.Mode & MODE_NOCACHE ? "on" : "off");
if((StringsDiffer(Config.SearchLocation, "") || StringsDiffer(Config.PlayerLocation, ""))
&& StringLength(Config.BaseURL) == 0)
{
printf("%sPlease set a Project Base URL (-B) so we can output the Search / Player pages to\n"
"locations other than the defaults%s\n",
ColourStrings[CS_ERROR], ColourStrings[CS_END]);
return(RC_SUCCESS);
}
#if 0
for(int i = 0; i < Assets.Count; ++i)
{
printf("%08x - %s\n", Assets.Asset[i].Hash, Assets.Asset[i].Filename);
}
#endif
InitDB();
inotifyInstance = inotify_init1(IN_NONBLOCK);
printf("┌╼ Hashing assets ╾┐\n");
// NOTE(matt): This had to happen before PackTemplate() because those guys may need to do PushAsset() and we must
// ensure that the builtin assets get placed correctly
InitAssets();
}
printf("┌╼ Packing templates ╾┐\n");
template SearchTemplate;
template PlayerTemplate;
template BespokeTemplate;
if(StringsDiffer(Config.TemplatePlayerLocation, ""))
{
switch(PackTemplate(&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.TemplateSearchLocation, ""))
{
switch(PackTemplate(&SearchTemplate, Config.TemplateSearchLocation, TEMPLATE_SEARCH))
{
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)
{
printf("\n┌╼ Synchronising with Annotations Directory ╾┐\n");
SyncDBWithInput(&CollationBuffers, &SearchTemplate, &PlayerTemplate, &BespokeTemplate);
if(Config.Mode & MODE_ONESHOT)
{
goto RIP;
}
printf("\n┌╼ Monitoring Annotations Directory for %snew%s, %sedited%s and %sdeleted%s .hmml files ╾┐\n",
ColourStrings[EditTypes[EDIT_ADDITION].Colour], ColourStrings[CS_END],
ColourStrings[EditTypes[EDIT_REINSERTION].Colour], ColourStrings[CS_END],
ColourStrings[EditTypes[EDIT_DELETION].Colour], ColourStrings[CS_END]);
// NOTE(matt): Do we want to also watch IN_DELETE_SELF events?
PushHMMLWatchHandle();
while(MonitorFilesystem(&CollationBuffers, &SearchTemplate, &PlayerTemplate, &BespokeTemplate) != RC_ARENA_FULL)
{
// TODO(matt): Refetch the quotes and rebuild player pages if needed
//
// Every sixty mins, redownload the quotes and, I suppose, SyncDBWithInput(). 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(&CollationBuffers, &SearchTemplate, &PlayerTemplate, &BespokeTemplate);
}
sleep(Config.UpdateInterval);
}
}
else
{
// TODO(matt): Get rid of Single Edition once and for all, probably for v1.0.0
if(optind == ArgC)
{
fprintf(stderr, "%s: requires at least one input .hmml file\n", Args[0]);
PrintUsage(Args[0], &DefaultConfig);
goto RIP;
}
inotifyInstance = inotify_init1(IN_NONBLOCK);
printf("┌╼ Hashing assets ╾┐\n");
InitBuiltinAssets();
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 = BespokeTemplate.File.Buffer.Location != NULL;
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) { FreeTemplate(&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, "%sWritten%s %s\n", HasBespokeTemplate ? Config.OutIntegratedLocation : Config.OutLocation);
#endif
if(HasBespokeTemplate) { FreeTemplate(&BespokeTemplate); }
break;
};
}
}
}
if(PlayerTemplate.File.Buffer.Location)
{
FreeTemplate(&PlayerTemplate);
}
if(Config.Edition == EDITION_PROJECT && SearchTemplate.File.Buffer.Location)
{
FreeTemplate(&SearchTemplate);
}
DeclaimBuffer(&CollationBuffers.SearchEntry);
DeclaimBuffer(&CollationBuffers.IncludesSearch);
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
}