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