6345 lines
250 KiB
C
6345 lines
250 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
|
|
ctime -end ${0%.*}.ctm
|
|
exit
|
|
#endif
|
|
|
|
typedef struct
|
|
{
|
|
unsigned int Major, Minor, Patch;
|
|
} version;
|
|
|
|
version CINERA_APP_VERSION = {
|
|
.Major = 0,
|
|
.Minor = 5,
|
|
.Patch = 36
|
|
};
|
|
|
|
// TODO(matt): Copy in the DB 3 stuff from cinera_working.c
|
|
#define CINERA_DB_VERSION 3
|
|
|
|
#define DEBUG 0
|
|
#define DEBUG_MEM 0
|
|
|
|
typedef unsigned int bool;
|
|
|
|
#define TRUE 1
|
|
#define FALSE 0
|
|
|
|
#include <stdarg.h> // NOTE(matt): varargs
|
|
#include <stdio.h> // NOTE(matt): printf, sprintf, vsprintf, fprintf, perror
|
|
#include <stdlib.h> // NOTE(matt): calloc, malloc, free
|
|
#include "hmmlib.h"
|
|
#include <getopt.h> // NOTE(matt): getopts
|
|
//#include "config.h" // TODO(matt): Implement config.h
|
|
#include <curl/curl.h>
|
|
#include <time.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <dirent.h>
|
|
#include <string.h> // NOTE(matt): strerror
|
|
#include <errno.h> //NOTE(matt): errno
|
|
#include <sys/inotify.h> // NOTE(matt): inotify
|
|
#include <unistd.h> // NOTE(matt): sleep()
|
|
|
|
#define Kilobytes(Bytes) Bytes << 10
|
|
#define Megabytes(Bytes) Bytes << 20
|
|
|
|
#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_ONESHOT = 1 << 0,
|
|
MODE_EXAMINE = 1 << 1,
|
|
MODE_NOCACHE = 1 << 2,
|
|
} 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_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;
|
|
bool ForceIntegration;
|
|
|
|
// 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 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];
|
|
} 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 ProjectName[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_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_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/", "", ""},
|
|
{ "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/", "", ""},
|
|
{ "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"},
|
|
{ "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[] =
|
|
{
|
|
{ "book", "Book Club", "Day", NS_LINEAR, "research", "" },
|
|
{ "pcalc", "pcalc", "Day", NS_LINEAR, "programming", "" },
|
|
{ "riscy", "RISCY BUSINESS", "Day", NS_LINEAR, "programming", "" },
|
|
|
|
{ "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))
|
|
|
|
__attribute__ ((format (printf, 2, 3)))
|
|
int
|
|
CopyString(char Dest[], char *Format, ...)
|
|
{
|
|
int Length = 0;
|
|
va_list Args;
|
|
va_start(Args, Format);
|
|
Length = vsprintf(Dest, Format, Args);
|
|
va_end(Args);
|
|
return Length;
|
|
}
|
|
|
|
int
|
|
StringLength(char *String)
|
|
{
|
|
int i = 0;
|
|
while(String[i])
|
|
{
|
|
++i;
|
|
}
|
|
return i;
|
|
}
|
|
|
|
void
|
|
CopyBuffer(buffer *Dest, buffer *Src)
|
|
{
|
|
Src->Ptr = Src->Location;
|
|
while(*Src->Ptr)
|
|
{
|
|
// TODO(matt)
|
|
{
|
|
if(Dest->Ptr - Dest->Location >= Dest->Size)
|
|
{
|
|
fprintf(stderr, "CopyBuffer: %s cannot accommodate %s\n", Dest->ID, Src->ID);
|
|
__asm__("int3");
|
|
}
|
|
}
|
|
*Dest->Ptr++ = *Src->Ptr++;
|
|
}
|
|
*Dest->Ptr = '\0';
|
|
}
|
|
|
|
void
|
|
Clear(char *String, int Size)
|
|
{
|
|
for(int i = 0; i < Size; ++i)
|
|
{
|
|
String[i] = 0;
|
|
}
|
|
}
|
|
|
|
int
|
|
CopyStringNoFormat(char *Dest, char *String)
|
|
{
|
|
int Length = 0;
|
|
while(*String)
|
|
{
|
|
*Dest++ = *String++;
|
|
++Length;
|
|
}
|
|
*Dest = '\0';
|
|
return Length;
|
|
}
|
|
|
|
int
|
|
ClearCopyStringNoFormat(char *Dest, int DestSize, char *String)
|
|
{
|
|
Clear(Dest, DestSize);
|
|
return(CopyStringNoFormat(Dest, String));
|
|
}
|
|
|
|
// TODO(matt): Maybe do a version of this that takes a string as a Terminator
|
|
int
|
|
CopyStringNoFormatT(char *Dest, char *String, char Terminator)
|
|
{
|
|
int Length = 0;
|
|
while(*String != Terminator)
|
|
{
|
|
*Dest++ = *String++;
|
|
++Length;
|
|
}
|
|
*Dest = '\0';
|
|
return Length;
|
|
}
|
|
|
|
__attribute__ ((format (printf, 2, 3)))
|
|
void
|
|
CopyStringToBuffer(buffer *Dest, char *Format, ...)
|
|
{
|
|
va_list Args;
|
|
va_start(Args, Format);
|
|
int Length = vsnprintf(Dest->Ptr, Dest->Size - (Dest->Ptr - Dest->Location), Format, Args);
|
|
va_end(Args);
|
|
// TODO(matt)
|
|
{
|
|
if(Length + (Dest->Ptr - Dest->Location) >= Dest->Size)
|
|
{
|
|
fprintf(stderr, "CopyStringToBuffer: %s cannot accommodate %d-character string:\n"
|
|
"\n"
|
|
"%s\n", Dest->ID, Length, Format);
|
|
__asm__("int3");
|
|
}
|
|
}
|
|
Dest->Ptr += Length;
|
|
}
|
|
|
|
void
|
|
CopyStringToBufferNoFormat(buffer *Dest, char *String)
|
|
{
|
|
while(*String)
|
|
{
|
|
*Dest->Ptr++ = *String++;
|
|
}
|
|
*Dest->Ptr = '\0';
|
|
}
|
|
|
|
void
|
|
CopyStringToBufferHTMLSafe(buffer *Dest, char *String)
|
|
{
|
|
while(*String)
|
|
{
|
|
if(Dest->Ptr - Dest->Location >= Dest->Size)
|
|
{
|
|
fprintf(stderr, "CopyStringToBufferHTMLSafe: %s cannot accommodate %d-character string\n", Dest->ID, StringLength(String));
|
|
__asm__("int3");
|
|
}
|
|
switch(*String)
|
|
{
|
|
case '<':
|
|
CopyStringToBuffer(Dest, "<");
|
|
String++;
|
|
break;
|
|
case '>':
|
|
CopyStringToBuffer(Dest, ">");
|
|
String++;
|
|
break;
|
|
case '&':
|
|
CopyStringToBuffer(Dest, "&");
|
|
String++;
|
|
break;
|
|
case '\"':
|
|
CopyStringToBuffer(Dest, """);
|
|
String++;
|
|
break;
|
|
case '\'':
|
|
CopyStringToBuffer(Dest, "'");
|
|
String++;
|
|
break;
|
|
default:
|
|
*Dest->Ptr++ = *String++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
CopyStringToBufferJSONSafe(buffer *Dest, char *String)
|
|
{
|
|
while(*String)
|
|
{
|
|
if(Dest->Ptr - Dest->Location >= Dest->Size)
|
|
{
|
|
fprintf(stderr, "CopyStringToBufferHTMLSafe: %s cannot accommodate %d-character string\n", Dest->ID, StringLength(String));
|
|
__asm__("int3");
|
|
}
|
|
switch(*String)
|
|
{
|
|
case '\\':
|
|
case '\"':
|
|
*Dest->Ptr++ = '\\';
|
|
default:
|
|
*Dest->Ptr++ = *String++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
int
|
|
StringsDiffer(char *A, char *B) // NOTE(matt): Two null-terminated strings
|
|
{
|
|
while(*A && *B && *A == *B)
|
|
{
|
|
++A, ++B;
|
|
}
|
|
return *A - *B;
|
|
}
|
|
|
|
bool
|
|
StringsDifferT(char *A, // NOTE(matt): Null-terminated string
|
|
char *B, // NOTE(matt): Not null-terminated string (e.g. one mid-buffer)
|
|
char Terminator // NOTE(matt): Caller definable terminator. Pass 0 to only match on the extent of A
|
|
)
|
|
{
|
|
int ALength = StringLength(A);
|
|
int i = 0;
|
|
while(i < ALength && A[i] && A[i] == B[i])
|
|
{
|
|
++i;
|
|
}
|
|
if((!Terminator && !A[i] && ALength == i) ||
|
|
(!A[i] && ALength == i && (B[i] == Terminator)))
|
|
{
|
|
return FALSE;
|
|
}
|
|
else
|
|
{
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
int
|
|
MakeDir(char *Path)
|
|
{
|
|
// TODO(matt): Correctly check for permissions
|
|
int i = StringLength(Path);
|
|
int Ancestors = 0;
|
|
while(mkdir(Path, 00755) == -1)
|
|
{
|
|
if(errno == EACCES)
|
|
{
|
|
return RC_ERROR_DIRECTORY;
|
|
}
|
|
while(Path[i] != '/' && i > 0)
|
|
{
|
|
--i;
|
|
}
|
|
++Ancestors;
|
|
Path[i] = '\0';
|
|
if(i == 0) { return RC_ERROR_DIRECTORY; }
|
|
}
|
|
while(Ancestors > 0)
|
|
{
|
|
while(Path[i] != '\0')
|
|
{
|
|
++i;
|
|
}
|
|
Path[i] = '/';
|
|
--Ancestors;
|
|
if((mkdir(Path, 00755)) == -1)
|
|
{
|
|
return RC_ERROR_DIRECTORY;
|
|
}
|
|
}
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
void
|
|
LogUsage(buffer *Buffer)
|
|
{
|
|
#if DEBUG
|
|
char LogPath[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, "%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 >= 80.0f)
|
|
{
|
|
// TODO(matt): Implement either dynamically growing buffers, or phoning home to matt@handmadedev.org
|
|
LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", Buffer->ID, PercentageUsed);
|
|
fprintf(stderr, "Warning: %s used %.2f%% of its allotted memory\n", Buffer->ID, PercentageUsed);
|
|
}
|
|
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, Template->Metadata.Filename);
|
|
char *Ptr = Template->Metadata.Filename;
|
|
if(TemplateType == TEMPLATE_BESPOKE)
|
|
{
|
|
if(Config.Edition == EDITION_SINGLE)
|
|
{
|
|
Ptr += CopyString(Ptr, "%s/", GetDirectoryPath(Config.SingleHMMLFilePath));
|
|
}
|
|
else
|
|
{
|
|
Ptr += CopyString(Ptr, "%s/", Config.ProjectDir);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Ptr += CopyString(Ptr, "%s/", Config.TemplatesDir);
|
|
}
|
|
CopyString(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, 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;
|
|
}
|
|
|
|
hsl_colour *
|
|
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;
|
|
return(Colour);
|
|
}
|
|
|
|
char *
|
|
SanitisePunctuation(char *String)
|
|
{
|
|
char *Ptr = String;
|
|
while(*Ptr)
|
|
{
|
|
if(*Ptr == ' ')
|
|
{
|
|
*Ptr = '_';
|
|
}
|
|
if((*Ptr < '0' || *Ptr > '9') &&
|
|
(*Ptr < 'a' || *Ptr > 'z') &&
|
|
(*Ptr < 'A' || *Ptr > 'Z'))
|
|
{
|
|
*Ptr = '-';
|
|
}
|
|
++Ptr;
|
|
}
|
|
return String;
|
|
}
|
|
|
|
enum
|
|
{
|
|
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, ""))
|
|
{
|
|
URLPrefix->Ptr += CopyString(URLPrefix->Ptr, "%s/", Config.RootURL);
|
|
}
|
|
else
|
|
{
|
|
if(Config.Edition == EDITION_PROJECT)
|
|
{
|
|
if(PageType == PAGE_PLAYER)
|
|
{
|
|
URLPrefix->Ptr += CopyString(URLPrefix->Ptr, "../");
|
|
}
|
|
URLPrefix->Ptr += CopyString(URLPrefix->Ptr, "../");
|
|
}
|
|
}
|
|
|
|
switch(IncludeType)
|
|
{
|
|
case INCLUDE_CSS:
|
|
if(StringsDiffer(Config.CSSDir, ""))
|
|
{
|
|
URLPrefix->Ptr += CopyString(URLPrefix->Ptr, "%s/", Config.CSSDir);
|
|
}
|
|
break;
|
|
case INCLUDE_Images:
|
|
if(StringsDiffer(Config.ImagesDir, ""))
|
|
{
|
|
URLPrefix->Ptr += CopyString(URLPrefix->Ptr, "%s/", Config.ImagesDir);
|
|
}
|
|
break;
|
|
case INCLUDE_JS:
|
|
if(StringsDiffer(Config.JSDir, ""))
|
|
{
|
|
URLPrefix->Ptr += CopyString(URLPrefix->Ptr, "%s/", Config.JSDir);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
enum
|
|
{
|
|
CreditsError_NoHost,
|
|
CreditsError_NoAnnotator,
|
|
CreditsError_NoCredentials
|
|
} credits_errors;
|
|
|
|
int
|
|
SearchCredentials(buffer *CreditsMenu, bool *HasCreditsMenu, char *Person, char *Role)
|
|
{
|
|
bool Found = FALSE;
|
|
for(int CredentialIndex = 0; CredentialIndex < ArrayCount(Credentials); ++CredentialIndex)
|
|
{
|
|
if(!StringsDiffer(Person, Credentials[CredentialIndex].Username))
|
|
{
|
|
Found = TRUE;
|
|
if(*HasCreditsMenu == FALSE)
|
|
{
|
|
CopyStringToBuffer(CreditsMenu,
|
|
" <div class=\"menu credits\">\n"
|
|
" <div class=\"mouse_catcher\"></div>\n"
|
|
" <span>Credits</span>\n"
|
|
" <div class=\"credits_container\">\n");
|
|
|
|
*HasCreditsMenu = TRUE;
|
|
}
|
|
|
|
CopyStringToBuffer(CreditsMenu,
|
|
" <span class=\"credit\">\n");
|
|
if(*Credentials[CredentialIndex].HomepageURL)
|
|
{
|
|
CopyStringToBuffer(CreditsMenu,
|
|
" <a class=\"person\" href=\"%s\" target=\"_blank\">\n"
|
|
" <div class=\"role\">%s</div>\n"
|
|
" <div class=\"name\">%s</div>\n"
|
|
" </a>\n",
|
|
Credentials[CredentialIndex].HomepageURL,
|
|
Role,
|
|
Credentials[CredentialIndex].CreditedName);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(CreditsMenu,
|
|
" <div class=\"person\">\n"
|
|
" <div class=\"role\">%s</div>\n"
|
|
" <div class=\"name\">%s</div>\n"
|
|
" </div>\n",
|
|
Role,
|
|
Credentials[CredentialIndex].CreditedName);
|
|
}
|
|
|
|
if(*Credentials[CredentialIndex].SupportIcon && *Credentials[CredentialIndex].SupportURL)
|
|
{
|
|
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\" style=\"background-image: url(%s%s);\"></div></a>\n",
|
|
Credentials[CredentialIndex].SupportURL,
|
|
URLPrefix.Location,
|
|
Credentials[CredentialIndex].SupportIcon);
|
|
DeclaimBuffer(&URLPrefix);
|
|
}
|
|
|
|
CopyStringToBuffer(CreditsMenu,
|
|
" </span>\n");
|
|
}
|
|
}
|
|
return Found ? 0 : CreditsError_NoCredentials;
|
|
}
|
|
|
|
int
|
|
BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Metadata)
|
|
// TODO(matt): Make this take the Credentials, once we are parsing them from a config
|
|
{
|
|
if(Metadata->member)
|
|
{
|
|
if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->member, "Host"))
|
|
{
|
|
printf("No credentials for 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;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(*HasCreditsMenu == TRUE)
|
|
{
|
|
CopyStringToBuffer(CreditsMenu,
|
|
" </div>\n"
|
|
" </div>\n");
|
|
}
|
|
fprintf(stderr, "Missing \"member\" in the [video] node\n");
|
|
return CreditsError_NoHost;
|
|
}
|
|
|
|
if(Metadata->co_host_count > 0)
|
|
{
|
|
for(int i = 0; i < Metadata->co_host_count; ++i)
|
|
{
|
|
if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->co_hosts[i], "Co-host"))
|
|
{
|
|
printf("No credentials for 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"))
|
|
{
|
|
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(Metadata->annotator_count > 0)
|
|
{
|
|
for(int i = 0; i < Metadata->annotator_count; ++i)
|
|
{
|
|
if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->annotators[i], "Annotator"))
|
|
{
|
|
printf("No credentials for 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;
|
|
}
|
|
|
|
int
|
|
BuildReference(ref_info *ReferencesArray, int RefIdentifier, int UniqueRefs, HMML_Reference *Ref, HMML_Annotation *Anno)
|
|
{
|
|
|
|
#define REF_SITE (1 << 0)
|
|
#define REF_PAGE (1 << 1)
|
|
#define REF_URL (1 << 2)
|
|
#define REF_TITLE (1 << 3)
|
|
#define REF_ARTICLE (1 << 4)
|
|
#define REF_AUTHOR (1 << 5)
|
|
#define REF_EDITOR (1 << 6)
|
|
#define REF_PUBLISHER (1 << 7)
|
|
#define REF_ISBN (1 << 8)
|
|
|
|
int Mask = 0;
|
|
|
|
if(Ref->site) { Mask |= REF_SITE; }
|
|
if(Ref->page) { Mask |= REF_PAGE; }
|
|
if(Ref->url) { Mask |= REF_URL; }
|
|
if(Ref->title) { Mask |= REF_TITLE; }
|
|
if(Ref->article) { Mask |= REF_ARTICLE; }
|
|
if(Ref->author) { Mask |= REF_AUTHOR; }
|
|
if(Ref->editor) { Mask |= REF_EDITOR; }
|
|
if(Ref->publisher) { Mask |= REF_PUBLISHER; }
|
|
if(Ref->isbn) { Mask |= REF_ISBN; }
|
|
|
|
if((REF_URL | REF_TITLE | REF_AUTHOR | REF_PUBLISHER | REF_ISBN) == Mask)
|
|
{
|
|
CopyString(ReferencesArray[UniqueRefs].ID, Ref->isbn);
|
|
CopyString(ReferencesArray[UniqueRefs].Source, "%s (%s)", Ref->author, Ref->publisher);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref->title);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref->url);
|
|
}
|
|
else if((REF_AUTHOR | REF_SITE | REF_PAGE | REF_URL) == Mask)
|
|
{
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref->url);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, Ref->site);
|
|
CopyString(ReferencesArray[UniqueRefs].RefTitle, "%s: \"%s\"", Ref->author, Ref->page);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref->url);
|
|
}
|
|
else if((REF_PAGE | REF_URL | REF_TITLE) == Mask)
|
|
{
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref->url);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, Ref->title);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref->page);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref->url);
|
|
}
|
|
else if((REF_SITE | REF_PAGE | REF_URL) == Mask)
|
|
{
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref->url);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, Ref->site);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref->page);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref->url);
|
|
}
|
|
else if((REF_SITE | REF_URL | REF_TITLE) == Mask)
|
|
{
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref->url);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, Ref->site);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref->title);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref->url);
|
|
}
|
|
else if((REF_TITLE | REF_AUTHOR | REF_ISBN) == Mask)
|
|
{
|
|
CopyString(ReferencesArray[UniqueRefs].ID, Ref->isbn);
|
|
CopyString(ReferencesArray[UniqueRefs].Source, Ref->author);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref->title);
|
|
CopyString(ReferencesArray[UniqueRefs].URL, "http://www.openisbn.com/isbn/%s", Ref->isbn);
|
|
}
|
|
else if((REF_URL | REF_ARTICLE | REF_AUTHOR) == Mask)
|
|
{
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref->url);
|
|
CopyString(ReferencesArray[UniqueRefs].Source, Ref->author);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref->article);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref->url);
|
|
}
|
|
else if((REF_URL | REF_TITLE | REF_AUTHOR) == Mask)
|
|
{
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref->url);
|
|
CopyString(ReferencesArray[UniqueRefs].Source, Ref->author);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref->title);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref->url);
|
|
}
|
|
else if((REF_URL | REF_TITLE) == Mask)
|
|
{
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref->url);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref->title);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref->url);
|
|
}
|
|
else if((REF_SITE | REF_URL) == Mask)
|
|
{
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, Ref->url);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, Ref->site);
|
|
CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, Ref->url);
|
|
}
|
|
else
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
CopyString(ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Timecode, Anno->time);
|
|
ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Identifier = RefIdentifier;
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
InsertCategory(categories *GlobalTopics, categories *LocalTopics, categories *GlobalMedia, categories *LocalMedia, char *Marker)
|
|
{
|
|
bool IsMedium = FALSE;
|
|
|
|
int CategoryMediumIndex;
|
|
for(CategoryMediumIndex = 0; CategoryMediumIndex < ArrayCount(CategoryMedium); ++CategoryMediumIndex)
|
|
{
|
|
if(!StringsDiffer(CategoryMedium[CategoryMediumIndex].Medium, Marker))
|
|
{
|
|
IsMedium = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
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, LocalMedia->Category[CategoryCount-1].Marker);
|
|
CopyString(LocalMedia->Category[CategoryCount].WrittenText, LocalMedia->Category[CategoryCount-1].WrittenText);
|
|
}
|
|
|
|
CopyString(LocalMedia->Category[CategoryCount].Marker, CategoryMedium[CategoryMediumIndex].Medium);
|
|
CopyString(LocalMedia->Category[CategoryCount].WrittenText, CategoryMedium[CategoryMediumIndex].WrittenName);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(MediumIndex == LocalMedia->Count)
|
|
{
|
|
CopyString(LocalMedia->Category[MediumIndex].Marker, CategoryMedium[CategoryMediumIndex].Medium);
|
|
CopyString(LocalMedia->Category[MediumIndex].WrittenText, 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, GlobalMedia->Category[CategoryCount-1].Marker);
|
|
CopyString(GlobalMedia->Category[CategoryCount].WrittenText, GlobalMedia->Category[CategoryCount-1].WrittenText);
|
|
}
|
|
|
|
CopyString(GlobalMedia->Category[CategoryCount].Marker, CategoryMedium[CategoryMediumIndex].Medium);
|
|
CopyString(GlobalMedia->Category[CategoryCount].WrittenText, CategoryMedium[CategoryMediumIndex].WrittenName);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(MediumIndex == GlobalMedia->Count)
|
|
{
|
|
CopyString(GlobalMedia->Category[MediumIndex].Marker, CategoryMedium[CategoryMediumIndex].Medium);
|
|
CopyString(GlobalMedia->Category[MediumIndex].WrittenText, 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, LocalTopics->Category[CategoryCount-1].Marker);
|
|
}
|
|
|
|
CopyString(LocalTopics->Category[CategoryCount].Marker, Marker);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(TopicIndex == LocalTopics->Count)
|
|
{
|
|
CopyString(LocalTopics->Category[TopicIndex].Marker, 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, GlobalTopics->Category[CategoryCount-1].Marker);
|
|
}
|
|
|
|
CopyString(GlobalTopics->Category[CategoryCount].Marker, Marker);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(TopicIndex == GlobalTopics->Count)
|
|
{
|
|
CopyString(GlobalTopics->Category[TopicIndex].Marker, 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)];
|
|
CopyString(SanitisedMarker, 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)];
|
|
CopyString(SanitisedMarker, 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)];
|
|
CopyString(SanitisedMarker, 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)];
|
|
CopyString(SanitisedMarker, 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 res;
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &QuoteStaging->Ptr);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlIntoBuffer);
|
|
curl_easy_setopt(curl, CURLOPT_URL, QuotesURL);
|
|
if((res = curl_easy_perform(curl)))
|
|
{
|
|
fprintf(stderr, "%s", curl_easy_strerror(res));
|
|
}
|
|
curl_easy_cleanup(curl);
|
|
}
|
|
}
|
|
|
|
int
|
|
SearchQuotes(buffer *QuoteStaging, int CacheSize, quote_info *Info, int ID)
|
|
{
|
|
QuoteStaging->Ptr = QuoteStaging->Location;
|
|
while(QuoteStaging->Ptr - QuoteStaging->Location < CacheSize)
|
|
{
|
|
char InID[4] = { 0 };
|
|
char InTime[16] = { 0 };
|
|
char *OutPtr = InID;
|
|
QuoteStaging->Ptr += CopyStringNoFormatT(OutPtr, QuoteStaging->Ptr, ',');
|
|
|
|
if(StringToInt(InID) == ID)
|
|
{
|
|
QuoteStaging->Ptr += 1;
|
|
OutPtr = InTime;
|
|
QuoteStaging->Ptr += CopyStringNoFormatT(OutPtr, QuoteStaging->Ptr, ',');
|
|
|
|
long int Time = StringToInt(InTime);
|
|
char DayString[3];
|
|
strftime(DayString, 3, "%d", gmtime(&Time));
|
|
int Day = StringToInt(DayString);
|
|
|
|
char DaySuffix[3]; if(DayString[1] == '1' && Day != 11) { CopyString(DaySuffix, "st"); }
|
|
else if(DayString[1] == '2' && Day != 12) { CopyString(DaySuffix, "nd"); }
|
|
else if(DayString[1] == '3' && Day != 13) { CopyString(DaySuffix, "rd"); }
|
|
else { CopyString(DaySuffix, "th"); }
|
|
|
|
char MonthYear[32];
|
|
strftime(MonthYear, 32, "%B, %Y", gmtime(&Time));
|
|
CopyString(Info->Date, "%d%s %s", Day, DaySuffix, MonthYear);
|
|
|
|
QuoteStaging->Ptr += 1;
|
|
OutPtr = Info->Text;
|
|
QuoteStaging->Ptr += CopyStringNoFormatT(OutPtr, QuoteStaging->Ptr, '\n');
|
|
|
|
FreeBuffer(QuoteStaging);
|
|
return RC_FOUND;
|
|
}
|
|
else
|
|
{
|
|
while(*QuoteStaging->Ptr != '\n')
|
|
{
|
|
++QuoteStaging->Ptr;
|
|
}
|
|
++QuoteStaging->Ptr;
|
|
}
|
|
}
|
|
return RC_UNFOUND;
|
|
}
|
|
|
|
int
|
|
BuildQuote(quote_info *Info, char *Speaker, int ID, bool ShouldFetchQuotes)
|
|
{
|
|
char QuoteCacheDir[256];
|
|
CopyString(QuoteCacheDir, "%s/quotes", Config.CacheDir);
|
|
char QuoteCachePath[256];
|
|
CopyString(QuoteCachePath, "%s/%s", QuoteCacheDir, Speaker);
|
|
|
|
if(ShouldFetchQuotes)
|
|
{
|
|
remove(QuoteCachePath);
|
|
}
|
|
|
|
FILE *QuoteCache;
|
|
char QuotesURL[256];
|
|
// TODO(matt): Make the URL configurable
|
|
CopyString(QuotesURL, "https://dev.abaines.me.uk/quotes/%s.raw", Speaker);
|
|
bool CacheAvailable = FALSE;
|
|
|
|
if(!(QuoteCache = fopen(QuoteCachePath, "a+")))
|
|
{
|
|
if(MakeDir(QuoteCacheDir) == RC_SUCCESS)
|
|
{
|
|
CacheAvailable = TRUE;
|
|
}
|
|
if(!(QuoteCache = fopen(QuoteCachePath, "a+")))
|
|
{
|
|
fprintf(stderr, "Unable to open quote cache %s: %s\n", QuoteCachePath, strerror(errno));
|
|
}
|
|
else
|
|
{
|
|
CacheAvailable = TRUE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CacheAvailable = TRUE;
|
|
}
|
|
|
|
buffer QuoteStaging;
|
|
QuoteStaging.ID = "QuoteStaging";
|
|
QuoteStaging.Size = Kilobytes(256);
|
|
if(!(QuoteStaging.Location = malloc(QuoteStaging.Size)))
|
|
{
|
|
fclose(QuoteCache);
|
|
return RC_ERROR_MEMORY;
|
|
}
|
|
|
|
#if DEBUG_MEM
|
|
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
|
|
fprintf(MemLog, " Allocated QuoteStaging (%d)\n", QuoteStaging.Size);
|
|
fclose(MemLog);
|
|
printf(" Allocated QuoteStaging (%d)\n", QuoteStaging.Size);
|
|
#endif
|
|
|
|
QuoteStaging.Ptr = QuoteStaging.Location;
|
|
|
|
if(CacheAvailable)
|
|
{
|
|
fseek(QuoteCache, 0, SEEK_END);
|
|
int FileSize = ftell(QuoteCache);
|
|
fseek(QuoteCache, 0, SEEK_SET);
|
|
|
|
fread(QuoteStaging.Location, FileSize, 1, QuoteCache);
|
|
fclose(QuoteCache);
|
|
|
|
if(SearchQuotes(&QuoteStaging, FileSize, Info, ID) == RC_UNFOUND)
|
|
{
|
|
CurlQuotes(&QuoteStaging, QuotesURL);
|
|
|
|
if(!(QuoteCache = fopen(QuoteCachePath, "w")))
|
|
{
|
|
perror(QuoteCachePath);
|
|
}
|
|
fwrite(QuoteStaging.Location, QuoteStaging.Ptr - QuoteStaging.Location, 1, QuoteCache);
|
|
fclose(QuoteCache);
|
|
|
|
int CacheSize = QuoteStaging.Ptr - QuoteStaging.Location;
|
|
QuoteStaging.Ptr = QuoteStaging.Location;
|
|
if(SearchQuotes(&QuoteStaging, CacheSize, Info, ID) == 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)];
|
|
CopyString(SanitisedTopic, 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, "%s/%s/cinera_topics.css", Config.RootDir, Config.CSSDir);
|
|
}
|
|
else
|
|
{
|
|
CopyString(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"
|
|
" -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_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)
|
|
{
|
|
#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.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.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.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.ForceIntegration && FoundPlayer == FALSE)
|
|
{
|
|
CopyStringToBuffer(&Errors, "<!-- %s --> must come after <!-- __CINERA_PLAYER__ -->\n", Tags[i].Tag);
|
|
HaveErrors = TRUE;
|
|
}
|
|
if(!Config.ForceIntegration && FoundScript == TRUE)
|
|
{
|
|
CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", Tags[i].Tag);
|
|
HaveErrors = TRUE;
|
|
}
|
|
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.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")))
|
|
{
|
|
return RC_ERROR_FILE;
|
|
}
|
|
|
|
fseek(File->Handle, 0, SEEK_END);
|
|
File->FileSize = ftell(File->Handle);
|
|
File->Buffer.Size = File->FileSize + 1 + BufferPadding; // NOTE(matt): +1 to accommodate a NULL terminator
|
|
fseek(File->Handle, 0, SEEK_SET);
|
|
|
|
// TODO(matt): Consider using the MemoryArena? Maybe have separate ReadFileIntoMemory() and ReadFileIntoArena()
|
|
if(!(File->Buffer.Location = malloc(File->Buffer.Size)))
|
|
{
|
|
fclose(File->Handle);
|
|
return RC_ERROR_MEMORY;
|
|
}
|
|
File->Buffer.Ptr = File->Buffer.Location;
|
|
|
|
fread(File->Buffer.Location, File->FileSize, 1, File->Handle);
|
|
File->Buffer.Location[File->FileSize] = '\0';
|
|
fclose(File->Handle);
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
// NOTE(matt): Currently unused
|
|
int
|
|
WriteBufferToFile(file_buffer *File, buffer *Buffer, int BytesToWrite, bool KeepFileHandleOpen)
|
|
{
|
|
if(!(File->Handle = fopen(File->Path, "w")))
|
|
{
|
|
return RC_ERROR_FILE;
|
|
}
|
|
|
|
fwrite(Buffer->Location, BytesToWrite, 1, File->Handle);
|
|
if(!KeepFileHandleOpen)
|
|
{
|
|
fclose(File->Handle);
|
|
}
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
#define HMMLCleanup() \
|
|
DeclaimBuffer(&FilterState); \
|
|
DeclaimBuffer(&CreditsMenu); \
|
|
DeclaimBuffer(&FilterMedia); \
|
|
DeclaimBuffer(&FilterTopics); \
|
|
DeclaimBuffer(&FilterMenu); \
|
|
DeclaimBuffer(&ReferenceMenu); \
|
|
DeclaimBuffer(&QuoteMenu); \
|
|
hmml_free(&HMML);
|
|
|
|
int
|
|
HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filename, link_insertion_offsets *LinkOffsets, neighbours *Neighbours)
|
|
{
|
|
RewindBuffer(&CollationBuffers->IncludesPlayer);
|
|
RewindBuffer(&CollationBuffers->Menus);
|
|
RewindBuffer(&CollationBuffers->Player);
|
|
RewindBuffer(&CollationBuffers->ScriptPlayer);
|
|
RewindBuffer(&CollationBuffers->IncludesIndex);
|
|
*CollationBuffers->Custom0 = '\0';
|
|
*CollationBuffers->Custom1 = '\0';
|
|
*CollationBuffers->Custom2 = '\0';
|
|
*CollationBuffers->Custom3 = '\0';
|
|
*CollationBuffers->Custom4 = '\0';
|
|
*CollationBuffers->Custom5 = '\0';
|
|
*CollationBuffers->Custom6 = '\0';
|
|
*CollationBuffers->Custom7 = '\0';
|
|
*CollationBuffers->Custom8 = '\0';
|
|
*CollationBuffers->Custom9 = '\0';
|
|
*CollationBuffers->Custom10 = '\0';
|
|
*CollationBuffers->Custom11 = '\0';
|
|
*CollationBuffers->Custom12 = '\0';
|
|
*CollationBuffers->Custom13 = '\0';
|
|
*CollationBuffers->Custom14 = '\0';
|
|
*CollationBuffers->Custom15 = '\0';
|
|
*CollationBuffers->Title = '\0';
|
|
*CollationBuffers->ProjectName = '\0';
|
|
|
|
char Filepath[256];
|
|
if(Config.Edition == EDITION_PROJECT)
|
|
{
|
|
CopyString(Filepath, "%s/%s", Config.ProjectDir, Filename);
|
|
}
|
|
else
|
|
{
|
|
CopyString(Filepath, "%s", Filename);
|
|
}
|
|
|
|
FILE *InFile;
|
|
if(!(InFile = fopen(Filepath, "r")))
|
|
{
|
|
LogError(LOG_ERROR, "Unable to open (annotations file) %s: %s", Filename, strerror(errno));
|
|
fprintf(stderr, "Unable to open (annotations file) %s: %s\n", Filename, strerror(errno));
|
|
return RC_ERROR_FILE;
|
|
}
|
|
|
|
HMML_Output HMML = hmml_parse_file(InFile);
|
|
fclose(InFile);
|
|
|
|
if(HMML.well_formed)
|
|
{
|
|
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, 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(!HMML.metadata.project && !StringsDiffer(Config.Theme, ""))
|
|
{
|
|
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, 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, URLPlayer.Location);
|
|
DeclaimBuffer(&URLPlayer);
|
|
|
|
for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex)
|
|
{
|
|
if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID,
|
|
Config.Edition == EDITION_SINGLE && HMML.metadata.project ?
|
|
HMML.metadata.project :
|
|
Config.ProjectID))
|
|
{
|
|
CopyString(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, HMML.metadata.custom[CustomIndex]); break;
|
|
case 1: CopyStringNoFormat(CollationBuffers->Custom1, HMML.metadata.custom[CustomIndex]); break;
|
|
case 2: CopyStringNoFormat(CollationBuffers->Custom2, HMML.metadata.custom[CustomIndex]); break;
|
|
case 3: CopyStringNoFormat(CollationBuffers->Custom3, HMML.metadata.custom[CustomIndex]); break;
|
|
case 4: CopyStringNoFormat(CollationBuffers->Custom4, HMML.metadata.custom[CustomIndex]); break;
|
|
case 5: CopyStringNoFormat(CollationBuffers->Custom5, HMML.metadata.custom[CustomIndex]); break;
|
|
case 6: CopyStringNoFormat(CollationBuffers->Custom6, HMML.metadata.custom[CustomIndex]); break;
|
|
case 7: CopyStringNoFormat(CollationBuffers->Custom7, HMML.metadata.custom[CustomIndex]); break;
|
|
case 8: CopyStringNoFormat(CollationBuffers->Custom8, HMML.metadata.custom[CustomIndex]); break;
|
|
case 9: CopyStringNoFormat(CollationBuffers->Custom9, HMML.metadata.custom[CustomIndex]); break;
|
|
case 10: CopyStringNoFormat(CollationBuffers->Custom10, HMML.metadata.custom[CustomIndex]); break;
|
|
case 11: CopyStringNoFormat(CollationBuffers->Custom11, HMML.metadata.custom[CustomIndex]); break;
|
|
case 12: CopyStringNoFormat(CollationBuffers->Custom12, HMML.metadata.custom[CustomIndex]); break;
|
|
case 13: CopyStringNoFormat(CollationBuffers->Custom13, HMML.metadata.custom[CustomIndex]); break;
|
|
case 14: CopyStringNoFormat(CollationBuffers->Custom14, HMML.metadata.custom[CustomIndex]); break;
|
|
case 15: CopyStringNoFormat(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(LinkOffsets)
|
|
{
|
|
LinkOffsets->PrevStart = 0;
|
|
LinkOffsets->NextStart = 0;
|
|
LinkOffsets->PrevEnd = 0;
|
|
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
|
|
// FilterState
|
|
|
|
buffer QuoteMenu;
|
|
buffer ReferenceMenu;
|
|
buffer FilterMenu;
|
|
buffer FilterTopics;
|
|
buffer FilterMedia;
|
|
buffer CreditsMenu;
|
|
|
|
buffer Annotation;
|
|
buffer AnnotationHeader;
|
|
buffer AnnotationClass;
|
|
buffer AnnotationData;
|
|
buffer Text;
|
|
buffer CategoryIcons;
|
|
|
|
buffer FilterState;
|
|
|
|
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; };
|
|
if(ClaimBuffer(&FilterState, "FilterState", Kilobytes(4)) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
|
|
|
|
ref_info ReferencesArray[200] = { 0 };
|
|
categories Topics = { 0 };
|
|
categories Media = { 0 };
|
|
|
|
bool HasQuoteMenu = FALSE;
|
|
bool HasReferenceMenu = FALSE;
|
|
bool HasFilterMenu = FALSE;
|
|
bool HasCreditsMenu = FALSE;
|
|
|
|
int QuoteIdentifier = 0x3b1;
|
|
int RefIdentifier = 1;
|
|
int UniqueRefs = 0;
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
"<div class=\"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"
|
|
" <span id=\"focus-warn\">⚠ Click here to regain focus ⚠</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(LinkOffsets)
|
|
{
|
|
LinkOffsets->PrevStart = (CollationBuffers->Player.Ptr - CollationBuffers->Player.Location);
|
|
if(Neighbours->Prev.BaseFilename || Neighbours->Next.BaseFilename)
|
|
{
|
|
if(Neighbours->Prev.BaseFilename)
|
|
{
|
|
// 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, Neighbours->Prev.BaseFilename);
|
|
CopyStringToBuffer(&CollationBuffers->Player,
|
|
" <a class=\"episodeMarker prev\" href=\"%s\"><div>⏫</div><div>Previous: '%s'</div><div>⏫</div></a>\n",
|
|
PreviousPlayerURL.Location,
|
|
Neighbours->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);
|
|
}
|
|
}
|
|
LinkOffsets->PrevEnd = (CollationBuffers->Player.Ptr - CollationBuffers->Player.Location - LinkOffsets->PrevStart);
|
|
}
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Player,
|
|
" <div class=\"markers\">\n");
|
|
|
|
switch(BuildCredits(&CreditsMenu, &HasCreditsMenu, &HMML.metadata))
|
|
{
|
|
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;
|
|
}
|
|
|
|
if(Config.Edition != EDITION_SINGLE)
|
|
{
|
|
RewindBuffer(&CollationBuffers->Search);
|
|
CopyStringToBuffer(&CollationBuffers->Search, "name: \"%s\"\n"
|
|
"title: \"", BaseFilename);
|
|
CopyStringToBufferJSONSafe(&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);
|
|
|
|
if(Config.Edition != EDITION_SINGLE)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Search, "\"%d\": \"", TimecodeToSeconds(Anno->time));
|
|
CopyStringToBufferJSONSafe(&CollationBuffers->Search, Anno->text);
|
|
CopyStringToBuffer(&CollationBuffers->Search, "\"\n");
|
|
}
|
|
|
|
categories LocalTopics = { 0 };
|
|
categories LocalMedia = { 0 };
|
|
bool HasQuote = FALSE;
|
|
bool HasReference = FALSE;
|
|
|
|
quote_info QuoteInfo = { 0 };
|
|
|
|
// NOTE(matt): Tree structure of "annotation local" buffer dependencies
|
|
// Annotation
|
|
// AnnotationHeader
|
|
// AnnotationClass
|
|
// AnnotationData
|
|
// Text
|
|
// 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", 512) == RC_ARENA_FULL) { hmml_free(&HMML); return RC_ARENA_FULL; };
|
|
|
|
CopyStringToBuffer(&AnnotationHeader,
|
|
" <div data-timestamp=\"%d\"",
|
|
TimecodeToSeconds(Anno->time));
|
|
|
|
CopyStringToBuffer(&AnnotationClass,
|
|
" class=\"marker");
|
|
|
|
if(Anno->author)
|
|
{
|
|
if(!HasFilterMenu)
|
|
{
|
|
HasFilterMenu = TRUE;
|
|
}
|
|
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, "authored");
|
|
hsl_colour AuthorColour;
|
|
StringToColourHash(&AuthorColour, Anno->author);
|
|
if(Config.Edition == EDITION_NETWORK)
|
|
{
|
|
fprintf(stderr, "%s:%d - TODO(matt): Implement author hoverbox\n", __FILE__, __LINE__);
|
|
// NOTE(matt): We should get instructions on how to get this info in the config
|
|
CopyStringToBuffer(&Text,
|
|
"<a class=\"author\" href=\"https://handmade.network/m/%s\" target=\"blank\" style=\"color: hsl(%d, %d%%, %d%%); text-decoration: none\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</a> ",
|
|
Anno->author,
|
|
AuthorColour.Hue, AuthorColour.Saturation, AuthorColour.Lightness,
|
|
AuthorColour.Hue, AuthorColour.Saturation,
|
|
Anno->author);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&Text,
|
|
"<span class=\"author\" style=\"color: hsl(%d, %d%%, %d%%);\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</span> ",
|
|
AuthorColour.Hue, AuthorColour.Saturation, AuthorColour.Lightness,
|
|
AuthorColour.Hue, AuthorColour.Saturation,
|
|
Anno->author);
|
|
}
|
|
|
|
}
|
|
|
|
char *InPtr = Anno->text;
|
|
|
|
int MarkerIndex = 0, RefIndex = 0;
|
|
while(*InPtr || RefIndex < Anno->reference_count)
|
|
{
|
|
if(MarkerIndex < Anno->marker_count &&
|
|
InPtr - Anno->text == Anno->markers[MarkerIndex].offset)
|
|
{
|
|
// TODO(matt): Consider switching on the Anno->markers[MarkerIndex].type and 100% ensuring this is all correct
|
|
// I wonder if HMML_CATEGORY should do InPtr += StringLength(Readable); like the others, and also whether HMML_MEMBER and HMML_PROJECT could be
|
|
// identical, except only for their class ("member" and "project" respectively)
|
|
// Pretty goddamn sure we can totally compress these cases, but let's do it tomorrow when we're fresh
|
|
char *Readable = Anno->markers[MarkerIndex].parameter
|
|
? Anno->markers[MarkerIndex].parameter
|
|
: Anno->markers[MarkerIndex].marker;
|
|
if(Anno->markers[MarkerIndex].type == HMML_MEMBER)
|
|
{
|
|
hsl_colour MemberColour;
|
|
StringToColourHash(&MemberColour, Anno->markers[MarkerIndex].marker);
|
|
if(Config.Edition == EDITION_NETWORK)
|
|
{
|
|
fprintf(stderr, "%s:%d - TODO(matt): Implement member hoverbox\n", __FILE__, __LINE__);
|
|
// NOTE(matt): We should get instructions on how to get this info in the config
|
|
CopyStringToBuffer(&Text,
|
|
"<a class=\"member\" href=\"https://handmade.network/m/%s\" target=\"blank\" style=\"color: hsl(%d, %d%%, %d%%); text-decoration: none\" data-hue=\"%d\" data-saturation=\"%d%%\">%.*s</a>",
|
|
Anno->markers[MarkerIndex].marker,
|
|
MemberColour.Hue, MemberColour.Saturation, MemberColour.Lightness,
|
|
MemberColour.Hue, MemberColour.Saturation,
|
|
StringLength(Readable), InPtr);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&Text,
|
|
"<span class=\"member\" style=\"color: hsl(%d, %d%%, %d%%);\" data-hue=\"%d\" data-saturation=\"%d%%\">%.*s</span>",
|
|
MemberColour.Hue, MemberColour.Saturation, MemberColour.Lightness,
|
|
MemberColour.Hue, MemberColour.Saturation,
|
|
StringLength(Readable), InPtr);
|
|
}
|
|
|
|
InPtr += StringLength(Readable);
|
|
++MarkerIndex;
|
|
}
|
|
else if(Anno->markers[MarkerIndex].type == HMML_PROJECT)
|
|
{
|
|
hsl_colour ProjectColour;
|
|
StringToColourHash(&ProjectColour, Anno->markers[MarkerIndex].marker);
|
|
if(Config.Edition == EDITION_NETWORK)
|
|
{
|
|
fprintf(stderr, "%s:%d - TODO(matt): Implement project hoverbox\n", __FILE__, __LINE__);
|
|
// NOTE(matt): We should get instructions on how to get this info in the config
|
|
CopyStringToBuffer(&Text,
|
|
"<a class=\"project\" href=\"https://%s.handmade.network/\" target=\"blank\" style=\"color: hsl(%d, %d%%, %d%%); text-decoration: none\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</a>",
|
|
Anno->markers[MarkerIndex].marker,
|
|
ProjectColour.Hue, ProjectColour.Saturation, ProjectColour.Lightness,
|
|
ProjectColour.Hue, ProjectColour.Saturation,
|
|
Readable);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&Text,
|
|
"<span class=\"project\" style=\"color: hsl(%d, %d%%, %d%%);\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</span>",
|
|
ProjectColour.Hue, ProjectColour.Saturation, ProjectColour.Lightness,
|
|
ProjectColour.Hue, ProjectColour.Saturation,
|
|
Readable);
|
|
}
|
|
InPtr += StringLength(Readable);
|
|
++MarkerIndex;
|
|
}
|
|
else if(Anno->markers[MarkerIndex].type == HMML_CATEGORY)
|
|
{
|
|
switch(GenerateTopicColours(Anno->markers[MarkerIndex].marker))
|
|
{
|
|
case RC_SUCCESS:
|
|
case RC_NOOP:
|
|
break;
|
|
case RC_ERROR_FILE:
|
|
case RC_ERROR_MEMORY:
|
|
hmml_free(&HMML);
|
|
return RC_ERROR_FATAL;
|
|
};
|
|
if(!HasFilterMenu)
|
|
{
|
|
HasFilterMenu = TRUE;
|
|
}
|
|
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, Anno->markers[MarkerIndex].marker);
|
|
++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=\"mouse_catcher\"></div>\n"
|
|
" <div class=\"refs references_container\">\n");
|
|
|
|
if(BuildReference(ReferencesArray, RefIdentifier, UniqueRefs, CurrentRef, Anno) == 1)
|
|
{
|
|
LogError(LOG_ERROR, "Reference combination processing failed: %s:%d", Filename, Anno->line);
|
|
fprintf(stderr, "%s:%d: Cannot process new combination of reference info\n"
|
|
"\n"
|
|
"Either tweak your annotation, or contact matt@handmadedev.org\n"
|
|
"mentioning the ref node you want to write and how you want it to\n"
|
|
"appear in the references menu\n", Filename, Anno->line);
|
|
hmml_free(&HMML);
|
|
return RC_INVALID_REFERENCE;
|
|
}
|
|
++ReferencesArray[RefIdentifier - 1].IdentifierCount;
|
|
++UniqueRefs;
|
|
|
|
HasReferenceMenu = TRUE;
|
|
}
|
|
else
|
|
{
|
|
for(int i = 0; i < UniqueRefs; ++i)
|
|
{
|
|
if(ReferencesArray[i].IdentifierCount == REF_MAX_IDENTIFIER)
|
|
{
|
|
LogError(LOG_EMERGENCY, "REF_MAX_IDENTIFIER (%d) reached. Contact matt@handmadedev.org", REF_MAX_IDENTIFIER);
|
|
fprintf(stderr, "%s:%d: Too many timecodes associated with one reference (increase REF_MAX_IDENTIFIER)\n", Filename, Anno->line);
|
|
hmml_free(&HMML);
|
|
return RC_ERROR_MAX_REFS;
|
|
}
|
|
if(CurrentRef->isbn)
|
|
{
|
|
if(!StringsDiffer(CurrentRef->isbn, ReferencesArray[i].ID))
|
|
{
|
|
CopyString(ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Timecode, Anno->time);
|
|
ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Identifier = RefIdentifier;
|
|
++ReferencesArray[i].IdentifierCount;
|
|
goto AppendedIdentifier;
|
|
}
|
|
}
|
|
else if(CurrentRef->url)
|
|
{
|
|
if(!StringsDiffer(CurrentRef->url, ReferencesArray[i].ID))
|
|
{
|
|
CopyString(ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Timecode, Anno->time);
|
|
ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Identifier = RefIdentifier;
|
|
++ReferencesArray[i].IdentifierCount;
|
|
goto AppendedIdentifier;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LogError(LOG_ERROR, "Reference missing ISBN or URL: %s:%d", Filename, Anno->line);
|
|
fprintf(stderr, "%s:%d: Reference must have an ISBN or URL\n", Filename, Anno->line);
|
|
hmml_free(&HMML);
|
|
return RC_INVALID_REFERENCE;
|
|
}
|
|
}
|
|
|
|
if(BuildReference(ReferencesArray, RefIdentifier, UniqueRefs, CurrentRef, Anno) == 1)
|
|
{
|
|
LogError(LOG_ERROR, "Reference combination processing failed: %s:%d", Filename, Anno->line);
|
|
fprintf(stderr, "%s:%d: Cannot process new combination of reference info\n"
|
|
"\n"
|
|
"Either tweak your annotation, or contact matt@handmadedev.org\n"
|
|
"mentioning the ref node you want to write and how you want it to\n"
|
|
"appear in the references menu\n", Filename, Anno->line);
|
|
hmml_free(&HMML);
|
|
return RC_INVALID_REFERENCE;
|
|
}
|
|
++ReferencesArray[UniqueRefs].IdentifierCount;
|
|
++UniqueRefs;
|
|
}
|
|
AppendedIdentifier:
|
|
if(!HasReference)
|
|
{
|
|
if(CurrentRef->isbn)
|
|
{
|
|
CopyStringToBuffer(&AnnotationData, " data-ref=\"%s", CurrentRef->isbn);
|
|
}
|
|
else if(CurrentRef->url)
|
|
{
|
|
CopyStringToBuffer(&AnnotationData, " data-ref=\"%s", CurrentRef->url);
|
|
}
|
|
else
|
|
{
|
|
LogError(LOG_ERROR, "Reference missing ISBN or URL: %s:%d", Filename, Anno->line);
|
|
fprintf(stderr, "%s:%d: Reference must have an ISBN or URL\n", Filename, Anno->line);
|
|
hmml_free(&HMML);
|
|
return RC_INVALID_REFERENCE;
|
|
}
|
|
|
|
HasReference = TRUE;
|
|
}
|
|
else
|
|
{
|
|
if(CurrentRef->isbn)
|
|
{
|
|
CopyStringToBuffer(&AnnotationData, ",%s", CurrentRef->isbn);
|
|
}
|
|
else if(CurrentRef->url)
|
|
{
|
|
CopyStringToBuffer(&AnnotationData, ",%s", CurrentRef->url);
|
|
}
|
|
else
|
|
{
|
|
LogError(LOG_ERROR, "Reference missing ISBN or URL: %s:%d", Filename, Anno->line);
|
|
fprintf(stderr, "%s:%d: Reference must have an ISBN or URL", Filename, Anno->line);
|
|
hmml_free(&HMML);
|
|
return RC_INVALID_REFERENCE;
|
|
}
|
|
}
|
|
|
|
if(Anno->references[RefIndex].offset == Anno->references[RefIndex-1].offset)
|
|
{
|
|
CopyStringToBuffer(&Text, "<sup style=\"vertical-align: super;\">,%d</sup>", RefIdentifier);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&Text, "<sup style=\"vertical-align: super;\">%d</sup>", RefIdentifier);
|
|
}
|
|
|
|
++RefIndex;
|
|
++RefIdentifier;
|
|
}
|
|
|
|
if(*InPtr)
|
|
{
|
|
switch(*InPtr)
|
|
{
|
|
case '<':
|
|
CopyStringToBuffer(&Text, "<");
|
|
InPtr++;
|
|
break;
|
|
case '>':
|
|
CopyStringToBuffer(&Text, ">");
|
|
InPtr++;
|
|
break;
|
|
case '&':
|
|
CopyStringToBuffer(&Text, "&");
|
|
InPtr++;
|
|
break;
|
|
case '\"':
|
|
CopyStringToBuffer(&Text, """);
|
|
InPtr++;
|
|
break;
|
|
case '\'':
|
|
CopyStringToBuffer(&Text, "'");
|
|
InPtr++;
|
|
break;
|
|
default:
|
|
*Text.Ptr++ = *InPtr++;
|
|
*Text.Ptr = '\0';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(Anno->is_quote)
|
|
{
|
|
if(!HasQuoteMenu)
|
|
{
|
|
CopyStringToBuffer(&QuoteMenu,
|
|
" <div class=\"menu quotes\">\n"
|
|
" <span>Quotes ▼</span>\n"
|
|
" <div class=\"mouse_catcher\"></div>\n"
|
|
" <div class=\"refs quotes_container\">\n");
|
|
|
|
HasQuoteMenu = TRUE;
|
|
}
|
|
|
|
if(!HasReference)
|
|
{
|
|
CopyStringToBuffer(&AnnotationData, " data-ref=\"&#%d;", QuoteIdentifier);
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&AnnotationData, ",&#%d;", QuoteIdentifier);
|
|
}
|
|
|
|
HasQuote = TRUE;
|
|
|
|
char *Speaker = Anno->quote.author ? Anno->quote.author : HMML.metadata.stream_username ? HMML.metadata.stream_username : HMML.metadata.member;
|
|
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 style=\"vertical-align: super;\">&#%d;</sup>", QuoteIdentifier);
|
|
++QuoteIdentifier;
|
|
}
|
|
|
|
while(MarkerIndex < Anno->marker_count)
|
|
{
|
|
switch(GenerateTopicColours(Anno->markers[MarkerIndex].marker))
|
|
{
|
|
case RC_SUCCESS:
|
|
case RC_NOOP:
|
|
break;
|
|
case RC_ERROR_FILE:
|
|
case RC_ERROR_MEMORY:
|
|
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)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Search, "---\n");
|
|
}
|
|
|
|
#if DEBUG
|
|
printf("\n\n --- End of Annotations Loop ---\n\n\n\n");
|
|
#endif
|
|
if(HasQuoteMenu)
|
|
{
|
|
CopyStringToBuffer(&QuoteMenu,
|
|
" </div>\n"
|
|
" </div>\n");
|
|
CopyBuffer(&CollationBuffers->Menus, &QuoteMenu);
|
|
}
|
|
|
|
if(HasReferenceMenu)
|
|
{
|
|
for(int i = 0; i < UniqueRefs; ++i)
|
|
{
|
|
CopyStringToBuffer(&ReferenceMenu,
|
|
" <a data-id=\"%s\" href=\"%s\" target=\"_blank\" class=\"ref\">\n"
|
|
" <span class=\"ref_content\">\n",
|
|
ReferencesArray[i].ID,
|
|
ReferencesArray[i].URL);
|
|
|
|
if(*ReferencesArray[i].Source)
|
|
{
|
|
CopyStringToBuffer(&ReferenceMenu,
|
|
" <div class=\"source\">");
|
|
CopyStringToBufferHTMLSafe(&ReferenceMenu, ReferencesArray[i].Source);
|
|
CopyStringToBuffer(&ReferenceMenu, "</div>\n"
|
|
" <div class=\"ref_title\">");
|
|
CopyStringToBufferHTMLSafe(&ReferenceMenu, ReferencesArray[i].RefTitle);
|
|
CopyStringToBuffer(&ReferenceMenu, "</div>\n");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&ReferenceMenu,
|
|
" <div class=\"ref_title\">");
|
|
CopyStringToBufferHTMLSafe(&ReferenceMenu, ReferencesArray[i].RefTitle);
|
|
CopyStringToBuffer(&ReferenceMenu, "</div>\n");
|
|
}
|
|
CopyStringToBuffer(&ReferenceMenu,
|
|
" </span>\n");
|
|
|
|
for(int j = 0; j < ReferencesArray[i].IdentifierCount;)
|
|
{
|
|
CopyStringToBuffer(&ReferenceMenu,
|
|
" <div class=\"ref_indices\">\n ");
|
|
for(int k = 0; k < 3 && j < ReferencesArray[i].IdentifierCount; ++k, ++j)
|
|
{
|
|
CopyStringToBuffer(&ReferenceMenu,
|
|
"<span data-timestamp=\"%d\" class=\"timecode\"><span class=\"ref_index\">[%d]</span><span class=\"time\">%s</span></span>",
|
|
TimecodeToSeconds(ReferencesArray[i].Identifier[j].Timecode),
|
|
ReferencesArray[i].Identifier[j].Identifier,
|
|
ReferencesArray[i].Identifier[j].Timecode);
|
|
}
|
|
CopyStringToBuffer(&ReferenceMenu, "\n"
|
|
" </div>\n");
|
|
}
|
|
|
|
CopyStringToBuffer(&ReferenceMenu,
|
|
" </a>\n");
|
|
}
|
|
|
|
CopyStringToBuffer(&ReferenceMenu,
|
|
" </div>\n"
|
|
" </div>\n");
|
|
CopyBuffer(&CollationBuffers->Menus, &ReferenceMenu);
|
|
}
|
|
|
|
if(HasFilterMenu)
|
|
{
|
|
// NOTE(matt): Two loops, one for each JS "object"
|
|
CopyStringToBuffer(&FilterState, "<script>\n"
|
|
" var filterInitState = {\n");
|
|
for(int i = 0; i < Topics.Count; ++i)
|
|
{
|
|
char SanitisedMarker[StringLength(Topics.Category[i].Marker)];
|
|
CopyString(SanitisedMarker, Topics.Category[i].Marker);
|
|
SanitisePunctuation(SanitisedMarker);
|
|
|
|
CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": false },\n",
|
|
SanitisedMarker, "topic");
|
|
}
|
|
for(int i = 0; i < Media.Count; ++i)
|
|
{
|
|
char SanitisedMarker[StringLength(Media.Category[i].Marker)];
|
|
CopyString(SanitisedMarker, Media.Category[i].Marker);
|
|
SanitisePunctuation(SanitisedMarker);
|
|
|
|
if(!StringsDiffer(Media.Category[i].Marker, "afk")) // TODO(matt): Make this configurable?
|
|
{
|
|
CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": true },\n",
|
|
SanitisedMarker, "medium");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": false },\n",
|
|
SanitisedMarker, "medium");
|
|
}
|
|
}
|
|
CopyStringToBuffer(&FilterState,
|
|
" };\n"
|
|
"\n"
|
|
" var filterState = {\n");
|
|
for(int i = 0; i < Topics.Count; ++i)
|
|
{
|
|
char SanitisedMarker[StringLength(Topics.Category[i].Marker)];
|
|
CopyString(SanitisedMarker, Topics.Category[i].Marker);
|
|
SanitisePunctuation(SanitisedMarker);
|
|
|
|
CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": false },\n",
|
|
SanitisedMarker, "topic");
|
|
}
|
|
for(int i = 0; i < Media.Count; ++i)
|
|
{
|
|
char SanitisedMarker[StringLength(Media.Category[i].Marker)];
|
|
CopyString(SanitisedMarker, Media.Category[i].Marker);
|
|
SanitisePunctuation(SanitisedMarker);
|
|
|
|
if(!StringsDiffer(Media.Category[i].Marker, "afk")) // TODO(matt): Make this configurable?
|
|
{
|
|
CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": true },\n",
|
|
SanitisedMarker, "medium");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": false },\n",
|
|
SanitisedMarker, "medium");
|
|
}
|
|
}
|
|
|
|
CopyStringToBuffer(&FilterState,
|
|
" };\n"
|
|
"</script>\n");
|
|
|
|
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)];
|
|
CopyString(SanitisedMarker, 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)];
|
|
CopyString(SanitisedMarker, 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");
|
|
|
|
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\">z</span> <span class=\"help_text\">Toggle filter mode</span> <span class=\"help_key\">V</span> <span class=\"help_text\">Revert filter to original state</span>\n");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <span class=\"help_key unavailable\">z</span> <span class=\"help_text unavailable\">Toggle filter mode</span> <span class=\"help_key unavailable\">V</span> <span class=\"help_text unavailable\">Revert filter to original state</span>\n");
|
|
}
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
"\n"
|
|
" <h2>Menu toggling</h2>\n");
|
|
|
|
if(HasQuoteMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <span class=\"help_key\">q</span> <span class=\"help_text\">Quotes</span>\n");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <span class=\"help_key unavailable\">q</span> <span class=\"help_text unavailable\">Quotes</span>\n");
|
|
}
|
|
|
|
if(HasReferenceMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <span class=\"help_key\">r</span> <span class=\"help_text\">References</span>\n");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <span class=\"help_key unavailable\">r</span> <span class=\"help_text unavailable\">References</span>\n");
|
|
}
|
|
|
|
if(HasFilterMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <span class=\"help_key\">f</span> <span class=\"help_text\">Filter</span>\n");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <span class=\"help_key unavailable\">f</span> <span class=\"help_text unavailable\">Filter</span>\n");
|
|
}
|
|
|
|
if(HasCreditsMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <span class=\"help_key\">c</span> <span class=\"help_text\">Credits</span>\n");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <span class=\"help_key unavailable\">c</span> <span class=\"help_text unavailable\">Credits</span>\n");
|
|
}
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
"\n"
|
|
" <h2>In-Menu Movement</h2>\n"
|
|
" <div class=\"help_paragraph\">\n"
|
|
" <div class=\"key_block\">\n"
|
|
" <div class=\"key_column\" style=\"flex-grow: 1\">\n"
|
|
" <span class=\"help_key\">a</span>\n"
|
|
" </div>\n"
|
|
" <div class=\"key_column\" style=\"flex-grow: 2\">\n"
|
|
" <span class=\"help_key\">w</span><br>\n"
|
|
" <span class=\"help_key\">s</span>\n"
|
|
" </div>\n"
|
|
" <div class=\"key_column\" style=\"flex-grow: 1\">\n"
|
|
" <span class=\"help_key\">d</span>\n"
|
|
" </div>\n"
|
|
" </div>\n"
|
|
" <div class=\"key_block\">\n"
|
|
" <span class=\"help_key\">h</span>\n"
|
|
" <span class=\"help_key\">j</span>\n"
|
|
" <span class=\"help_key\">k</span>\n"
|
|
" <span class=\"help_key\">l</span>\n"
|
|
" </div>\n"
|
|
" <div class=\"key_block\">\n"
|
|
" <div style=\"flex-grow: 1\">\n"
|
|
" <span class=\"help_key\">←</span>\n"
|
|
" </div>\n"
|
|
" <div style=\"flex-grow: 2\">\n"
|
|
" <span class=\"help_key\">↑</span><br>\n"
|
|
" <span class=\"help_key\">↓</span>\n"
|
|
" </div>\n"
|
|
" <div style=\"flex-grow: 1\">\n"
|
|
" <span class=\"help_key\">→</span>\n"
|
|
" </div>\n"
|
|
" </div>\n"
|
|
" </div>\n"
|
|
" <br>\n");
|
|
|
|
if(HasQuoteMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <h2>Quotes ");
|
|
if(HasReferenceMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus, "and References Menus</h2>\n");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus, "<span class=\"unavailable\">and References</span> Menus</h2>\n");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <h2><span class=\"unavailable\">Quotes");
|
|
if(HasReferenceMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus, " and</span> References Menus</h2>\n");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus, " and References Menus</span></h2>\n");
|
|
}
|
|
}
|
|
|
|
if(HasQuoteMenu || HasReferenceMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <span style=\"width: auto\" class=\"help_key\">Enter</span> <span class=\"help_text\">Jump to timecode</span><br>\n");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <span style=\"width: auto\" class=\"help_key unavailable\">Enter</span> <span class=\"help_text unavailable\">Jump to timecode</span><br>\n");
|
|
}
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Menus, "\n");
|
|
|
|
if(HasQuoteMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <h2>Quotes");
|
|
if(HasReferenceMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus, ", References ");
|
|
if(HasCreditsMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus, "and Credits Menus</h2>");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus, "<span class=\"unavailable\">and Credits</span> Menus</h2>");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus, "<span class=\"unavailable\">, References ");
|
|
if(HasCreditsMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus, "</span>and Credits Menus</h2>");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus, "and Credits</span> Menus</h2>");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <h2><span class=\"unavailable\">Quotes");
|
|
if(HasReferenceMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus, ", </span>References ");
|
|
if(HasCreditsMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus, "and Credits Menus</h2>");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus, "<span class=\"unavailable\">and Credits</span> Menus</h2>");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus, ", References ");
|
|
if(HasCreditsMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus, "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 style=\"width: auto\" class=\"help_key\">Space</span> <span class=\"help_text\">Toggle category and focus next</span><br>\n"
|
|
" <span class=\"help_key\">X</span>, <span style=\"width: auto; margin-right: 0px\" class=\"help_key\">Shift</span><span style=\"width: auto\" class=\"help_key\">Space</span> <span class=\"help_text\">Toggle category and focus previous</span><br>\n"
|
|
" <span class=\"help_key\">v</span> <span class=\"help_text\">Invert topics / media as per focus</span>\n");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <h2><span class=\"unavailable\">Filter Menu</span></h2>\n"
|
|
" <span class=\"help_key unavailable\">x</span>, <span style=\"width: auto\" class=\"help_key unavailable\">Space</span> <span class=\"help_text unavailable\">Toggle category and focus next</span><br>\n"
|
|
" <span class=\"help_key unavailable\">X</span>, <span style=\"width: auto; margin-right: 0px\" class=\"help_key unavailable\">Shift</span><span style=\"width: auto\" class=\"help_key unavailable\">Space</span> <span class=\"help_text unavailable\">Toggle category and focus previous</span><br>\n"
|
|
" <span class=\"help_key unavailable\">v</span> <span class=\"help_text unavailable\">Invert topics / media as per focus</span>\n");
|
|
}
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Menus, "\n");
|
|
|
|
if(HasCreditsMenu)
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <h2>Credits Menu</h2>\n"
|
|
" <span style=\"width: auto\" class=\"help_key\">Enter</span> <span class=\"help_text\">Open URL (in new tab)</span><br>\n");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" <h2><span class=\"unavailable\">Credits Menu</span></h2>\n"
|
|
" <span style=\"width: auto\" class=\"help_key unavailable\">Enter</span> <span class=\"help_text unavailable\">Open URL (in new tab)</span><br>\n");
|
|
}
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Menus,
|
|
" </div>\n"
|
|
" </div>\n"
|
|
|
|
" </div>");
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Player,
|
|
" </div>\n");
|
|
|
|
if(LinkOffsets)
|
|
{
|
|
LinkOffsets->NextStart = (CollationBuffers->Player.Ptr - CollationBuffers->Player.Location - (LinkOffsets->PrevStart + LinkOffsets->PrevEnd));
|
|
if(Neighbours->Prev.BaseFilename || Neighbours->Next.BaseFilename)
|
|
{
|
|
if(Neighbours->Next.BaseFilename)
|
|
{
|
|
// 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, Neighbours->Next.BaseFilename);
|
|
CopyStringToBuffer(&CollationBuffers->Player,
|
|
" <a class=\"episodeMarker next\" href=\"%s\"><div>⏬</div><div>Next: '%s'</div><div>⏬</div></a>\n",
|
|
NextPlayerURL.Location,
|
|
Neighbours->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);
|
|
}
|
|
}
|
|
LinkOffsets->NextEnd = (CollationBuffers->Player.Ptr - CollationBuffers->Player.Location - (LinkOffsets->PrevStart + LinkOffsets->PrevEnd + 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, URLIndex.Location);
|
|
DeclaimBuffer(&URLIndex);
|
|
|
|
buffer URLPrefix;
|
|
ClaimBuffer(&URLPrefix, "URLPrefix", 1024);
|
|
ConstructURLPrefix(&URLPrefix, INCLUDE_CSS, PAGE_INDEX);
|
|
CopyStringToBuffer(&CollationBuffers->IncludesIndex,
|
|
"<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"
|
|
" <style>body { overflow-y: scroll; }</style>\n"
|
|
"\n"
|
|
" <meta charset=\"UTF-8\">\n"
|
|
" <meta name=\"generator\" content=\"Cinera %d.%d.%d\">\n",
|
|
URLPrefix.Location,
|
|
URLPrefix.Location, StringsDiffer(Config.Theme, "") ? Config.Theme : HMML.metadata.project,
|
|
URLPrefix.Location,
|
|
|
|
CINERA_APP_VERSION.Major,
|
|
CINERA_APP_VERSION.Minor,
|
|
CINERA_APP_VERSION.Patch);
|
|
|
|
ConstructURLPrefix(&URLPrefix, INCLUDE_CSS, PAGE_PLAYER);
|
|
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
|
|
"<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"
|
|
"\n"
|
|
" <meta charset=\"UTF-8\">\n"
|
|
" <meta name=\"generator\" content=\"Cinera %d.%d.%d\">\n",
|
|
URLPrefix.Location,
|
|
URLPrefix.Location, StringsDiffer(Config.Theme, "") ? Config.Theme : HMML.metadata.project,
|
|
URLPrefix.Location,
|
|
|
|
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\n");
|
|
}
|
|
|
|
ConstructURLPrefix(&URLPrefix, INCLUDE_JS, PAGE_PLAYER);
|
|
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
|
|
" <script type=\"text/javascript\" src=\"%scinera_player_pre.js\"></script>",
|
|
URLPrefix.Location);
|
|
|
|
if(HasFilterMenu)
|
|
{
|
|
CopyBuffer(&CollationBuffers->ScriptPlayer, &FilterState);
|
|
}
|
|
|
|
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
|
|
// FilterState
|
|
// CreditsMenu
|
|
// FilterMedia
|
|
// FilterTopics
|
|
// FilterMenu
|
|
// ReferenceMenu
|
|
// QuoteMenu
|
|
|
|
DeclaimBuffer(&FilterState);
|
|
|
|
DeclaimBuffer(&CreditsMenu);
|
|
DeclaimBuffer(&FilterMedia);
|
|
DeclaimBuffer(&FilterTopics);
|
|
DeclaimBuffer(&FilterMenu);
|
|
DeclaimBuffer(&ReferenceMenu);
|
|
DeclaimBuffer(&QuoteMenu);
|
|
}
|
|
else
|
|
{
|
|
LogError(LOG_ERROR, "%s:%d: %s", Filename, HMML.error.line, HMML.error.message);
|
|
fprintf(stderr, "\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, int *PlayerOffset)
|
|
{
|
|
#if DEBUG
|
|
printf("\n\n --- Buffer Collation ---\n"
|
|
" %s\n\n\n", OutputPath ? OutputPath : Config.OutLocation);
|
|
#endif
|
|
|
|
#if DEBUG_MEM
|
|
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
|
|
fprintf(MemLog, "\nEntered BuffersToHTML(%s)\n", OutputPath ? OutputPath : Config.OutLocation);
|
|
fclose(MemLog);
|
|
#endif
|
|
|
|
if(Template->Metadata.Filename && StringsDiffer(Template->Metadata.Filename, ""))
|
|
{
|
|
if((Template->Metadata.Filename && StringsDiffer(Template->Metadata.Filename, ""))
|
|
&& ((Template->Metadata.Validity & PageType) || Config.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;
|
|
}
|
|
|
|
switch(Template->Metadata.Tag[i].TagCode)
|
|
{
|
|
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
|
|
{
|
|
CopyStringToBufferNoFormat(&Output, CollationBuffers->ProjectName);
|
|
}
|
|
break;
|
|
case TAG_TITLE:
|
|
CopyStringToBufferNoFormat(&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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
int
|
|
InsertIntoIndex(index *Index, buffers *CollationBuffers, template **BespokeTemplate, char *BaseFilename)
|
|
{
|
|
int IndexMetadataFileReadCode = ReadFileIntoBuffer(&Index->Metadata, 0);
|
|
switch(IndexMetadataFileReadCode)
|
|
{
|
|
case RC_ERROR_MEMORY:
|
|
return RC_ERROR_MEMORY;
|
|
case RC_ERROR_FILE:
|
|
case RC_SUCCESS:
|
|
break;
|
|
}
|
|
|
|
int IndexFileReadCode = ReadFileIntoBuffer(&Index->File, 0);
|
|
switch(IndexFileReadCode)
|
|
{
|
|
case RC_ERROR_MEMORY:
|
|
return RC_ERROR_MEMORY;
|
|
case RC_ERROR_FILE:
|
|
case RC_SUCCESS:
|
|
break;
|
|
}
|
|
|
|
int MetadataInsertionOffset = -1;
|
|
int IndexEntryInsertionStart = -1;
|
|
int IndexEntryInsertionEnd = -1;
|
|
Index->Header.EntryCount = 0;
|
|
char *IndexEntryStart;
|
|
bool Found = FALSE;
|
|
|
|
int EntryIndex;
|
|
neighbours Neighbours = { 0 };
|
|
index_metadata *This, *Next;
|
|
if(IndexMetadataFileReadCode == RC_SUCCESS && IndexFileReadCode == RC_SUCCESS)
|
|
{
|
|
// 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->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location;
|
|
Index->Header = *(index_header *)Index->Metadata.Buffer.Ptr;
|
|
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->Metadata.Buffer.Ptr += sizeof(Index->Header);
|
|
Index->File.Buffer.Ptr += StringLength("---\n");
|
|
IndexEntryStart = Index->File.Buffer.Ptr;
|
|
|
|
for(EntryIndex = 0; EntryIndex < Index->Header.EntryCount; ++EntryIndex)
|
|
{
|
|
This = (index_metadata *)Index->Metadata.Buffer.Ptr;
|
|
if(!StringsDiffer(This->BaseFilename, BaseFilename))
|
|
{
|
|
// Reinsert
|
|
if(EntryIndex < (Index->Header.EntryCount - 1))
|
|
{
|
|
Next = (index_metadata *)(Index->Metadata.Buffer.Ptr + sizeof(Index->Entry));
|
|
Neighbours.Next.BaseFilename = Next->BaseFilename;
|
|
Neighbours.Next.Title = Next->Title;
|
|
}
|
|
|
|
MetadataInsertionOffset = Index->Metadata.Buffer.Ptr - Index->Metadata.Buffer.Location;
|
|
IndexEntryInsertionStart = IndexEntryStart - Index->File.Buffer.Location;
|
|
IndexEntryInsertionEnd = IndexEntryInsertionStart + This->Size;
|
|
Found = TRUE;
|
|
break;
|
|
}
|
|
else if(StringsDiffer(This->BaseFilename, BaseFilename) > 0)
|
|
{
|
|
// Insert
|
|
Neighbours.Next.BaseFilename = This->BaseFilename;
|
|
Neighbours.Next.Title = This->Title;
|
|
|
|
MetadataInsertionOffset = Index->Metadata.Buffer.Ptr - Index->Metadata.Buffer.Location;
|
|
IndexEntryInsertionStart = IndexEntryStart - Index->File.Buffer.Location;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
Index->Metadata.Buffer.Ptr += sizeof(Index->Entry);
|
|
|
|
IndexEntryStart += This->Size;
|
|
Index->File.Buffer.Ptr = IndexEntryStart;
|
|
|
|
Neighbours.Prev.BaseFilename = This->BaseFilename;
|
|
Neighbours.Prev.Title = This->Title;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// NOTE(matt): Initialising new index_header
|
|
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.InitialDBVersion = CINERA_DB_VERSION;
|
|
Index->Header.InitialAppVersion = CINERA_APP_VERSION;
|
|
Index->Header.InitialHMMLVersion.Major = hmml_version.Major;
|
|
Index->Header.InitialHMMLVersion.Minor = hmml_version.Minor;
|
|
Index->Header.InitialHMMLVersion.Patch = hmml_version.Patch;
|
|
|
|
CopyStringNoFormat(Index->Header.ProjectID, Config.ProjectID);
|
|
|
|
for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex)
|
|
{
|
|
if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID))
|
|
{
|
|
CopyStringNoFormat(Index->Header.ProjectName, ProjectInfo[ProjectIndex].FullName);
|
|
break;
|
|
}
|
|
}
|
|
|
|
CopyStringNoFormat(Index->Header.BaseURL, Config.BaseURL);
|
|
CopyStringNoFormat(Index->Header.IndexLocation, Config.IndexLocation);
|
|
CopyStringNoFormat(Index->Header.PlayerLocation, Config.PlayerLocation);
|
|
CopyStringNoFormat(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")];
|
|
CopyString(InputFile, "%s.hmml", BaseFilename);
|
|
switch(HMMLToBuffers(CollationBuffers, BespokeTemplate, InputFile, &Index->Entry.LinkOffsets, &Neighbours))
|
|
{
|
|
// TODO(matt): Actually sort out the fatality of these cases, once we are always-on
|
|
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_SUCCESS:
|
|
break;
|
|
};
|
|
|
|
Index->Entry.Size = CollationBuffers->Search.Ptr - CollationBuffers->Search.Location;
|
|
ClearCopyStringNoFormat(Index->Entry.BaseFilename, sizeof(Index->Entry.BaseFilename), BaseFilename);
|
|
ClearCopyStringNoFormat(Index->Entry.Title, sizeof(Index->Entry.Title), CollationBuffers->Title);
|
|
|
|
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; }
|
|
|
|
if(!Found) { ++Index->Header.EntryCount; }
|
|
|
|
fwrite(&Index->Header, sizeof(Index->Header), 1, Index->Metadata.Handle);
|
|
|
|
if(IndexMetadataFileReadCode == RC_SUCCESS)
|
|
{
|
|
Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location + sizeof(Index->Header);
|
|
}
|
|
|
|
if(Found)
|
|
{
|
|
// NOTE(matt): We hit this during the start-up sync and when copying in a .hmml file over an already existing one, but
|
|
// would need to fool about with the inotify event processing to get to this branch in the case that saving
|
|
// a file triggers an IN_DELETE followed by an IN_CLOSE_WRITE event
|
|
|
|
// Reinsert
|
|
fwrite(Index->Metadata.Buffer.Ptr, MetadataInsertionOffset - sizeof(Index->Header), 1, Index->Metadata.Handle);
|
|
fwrite(&Index->Entry, sizeof(Index->Entry), 1, Index->Metadata.Handle);
|
|
fwrite(Index->Metadata.Buffer.Ptr - sizeof(Index->Header) + MetadataInsertionOffset + sizeof(Index->Entry), Index->Metadata.FileSize - MetadataInsertionOffset - sizeof(Index->Entry), 1, Index->Metadata.Handle);
|
|
|
|
fwrite(Index->File.Buffer.Location, IndexEntryInsertionStart, 1, Index->File.Handle);
|
|
fwrite(CollationBuffers->Search.Location, Index->Entry.Size, 1, Index->File.Handle);
|
|
fwrite(Index->File.Buffer.Location + IndexEntryInsertionEnd, Index->File.FileSize - IndexEntryInsertionEnd, 1, Index->File.Handle);
|
|
|
|
LogError(LOG_NOTICE, "Reinserted %s - %s", BaseFilename, CollationBuffers->Title);
|
|
fprintf(stderr, "\e[1;33mReinserted\e[0m %s - %s\n", BaseFilename, CollationBuffers->Title);
|
|
}
|
|
else if(MetadataInsertionOffset >= 0 && IndexEntryInsertionStart >= 0)
|
|
{
|
|
// Insert new
|
|
fwrite(Index->Metadata.Buffer.Ptr, MetadataInsertionOffset - sizeof(Index->Header), 1, Index->Metadata.Handle);
|
|
fwrite(&Index->Entry, sizeof(Index->Entry), 1, Index->Metadata.Handle);
|
|
fwrite(Index->Metadata.Buffer.Ptr - sizeof(Index->Header) + MetadataInsertionOffset, Index->Metadata.FileSize - MetadataInsertionOffset, 1, Index->Metadata.Handle);
|
|
|
|
fwrite(Index->File.Buffer.Location, IndexEntryInsertionStart, 1, Index->File.Handle);
|
|
fwrite(CollationBuffers->Search.Location, Index->Entry.Size, 1, Index->File.Handle);
|
|
fwrite(Index->File.Buffer.Location + IndexEntryInsertionStart, Index->File.FileSize - IndexEntryInsertionStart, 1, Index->File.Handle);
|
|
|
|
LogError(LOG_NOTICE, "Inserted %s - %s", BaseFilename, CollationBuffers->Title);
|
|
fprintf(stderr, "\e[1;32mInserted\e[0m %s - %s\n", BaseFilename, CollationBuffers->Title);
|
|
}
|
|
else
|
|
{
|
|
// Append new
|
|
if(IndexMetadataFileReadCode == RC_SUCCESS)
|
|
{
|
|
fwrite(Index->Metadata.Buffer.Ptr, 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(&Index->Entry, sizeof(Index->Entry), 1, Index->Metadata.Handle);
|
|
fwrite(CollationBuffers->Search.Location, Index->Entry.Size, 1, Index->File.Handle);
|
|
LogError(LOG_NOTICE, "Appended %s - %s", BaseFilename, CollationBuffers->Title);
|
|
fprintf(stderr, "\e[1;32mAppended\e[0m %s - %s\n", BaseFilename, CollationBuffers->Title);
|
|
}
|
|
|
|
fclose(Index->Metadata.Handle);
|
|
fclose(Index->File.Handle);
|
|
FreeBuffer(&Index->Metadata.Buffer);
|
|
FreeBuffer(&Index->File.Buffer);
|
|
return Found ? RC_SUCCESS : RC_UNFOUND;
|
|
}
|
|
|
|
void
|
|
ConstructDirectoryPath(buffer *DirectoryPath, int PageType, char *PageLocation, char *BaseFilename)
|
|
{
|
|
RewindBuffer(DirectoryPath);
|
|
CopyStringToBuffer(DirectoryPath, 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(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, char *BaseFilename, int LinkType)
|
|
{
|
|
switch(ReadFileIntoBuffer(&Index->Metadata, 0))
|
|
{
|
|
case RC_ERROR_FILE:
|
|
return RC_ERROR_FILE;
|
|
case RC_ERROR_MEMORY:
|
|
LogError(LOG_ERROR, "DeleteFromIndex(): %s", strerror(errno));
|
|
return RC_ERROR_MEMORY;
|
|
case RC_SUCCESS:
|
|
break;
|
|
}
|
|
|
|
Index->Header = *(index_header *)Index->Metadata.Buffer.Ptr;
|
|
Index->Metadata.Buffer.Ptr += sizeof(Index->Header);
|
|
index_metadata *Prev = { 0 };
|
|
index_metadata *This = { 0 };
|
|
index_metadata *Next = { 0 };
|
|
int EntryIndex = 0;
|
|
|
|
switch(LinkType)
|
|
{
|
|
case LINK_INCLUDE:
|
|
{
|
|
for(EntryIndex = 0; EntryIndex < Index->Header.EntryCount; ++EntryIndex)
|
|
{
|
|
This = (index_metadata *)Index->Metadata.Buffer.Ptr;
|
|
if(!StringsDiffer(This->BaseFilename, BaseFilename))
|
|
{
|
|
if(EntryIndex < (Index->Header.EntryCount - 1))
|
|
{
|
|
Next = (index_metadata *)(Index->Metadata.Buffer.Ptr + sizeof(index_metadata));
|
|
}
|
|
break;
|
|
}
|
|
Prev = This;
|
|
Index->Metadata.Buffer.Ptr += sizeof(index_metadata);
|
|
}
|
|
} break;
|
|
case LINK_EXCLUDE:
|
|
{
|
|
This = (index_metadata *)Index->Metadata.Buffer.Ptr;
|
|
if(Index->Header.EntryCount != 1)
|
|
{
|
|
Index->Metadata.Buffer.Ptr += sizeof(index_metadata);
|
|
Next = (index_metadata *)Index->Metadata.Buffer.Ptr;
|
|
|
|
for(EntryIndex = 1; EntryIndex < Index->Header.EntryCount; ++EntryIndex)
|
|
{
|
|
Prev = This;
|
|
This = Next;
|
|
if(EntryIndex < (Index->Header.EntryCount - 1))
|
|
{
|
|
Index->Metadata.Buffer.Ptr += sizeof(index_metadata);
|
|
Next = (index_metadata *)Index->Metadata.Buffer.Ptr;
|
|
}
|
|
else { Next = 0; }
|
|
if(StringsDiffer(BaseFilename, This->BaseFilename) < 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} break;
|
|
}
|
|
|
|
if(!Prev && !Next)
|
|
{
|
|
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, This->BaseFilename);
|
|
CopyStringToBuffer(&ThisPlayerPagePath, "/index.html");
|
|
|
|
file_buffer ThisPlayerPage;
|
|
CopyStringNoFormat(ThisPlayerPage.Path, ThisPlayerPagePath.Location);
|
|
|
|
DeleteNeighbourLinks(&ThisPlayerPage, This);
|
|
|
|
DeclaimBuffer(&ThisPlayerPagePath);
|
|
|
|
Index->Metadata.Handle = fopen(Index->Metadata.Path, "w");
|
|
fwrite(Index->Metadata.Buffer.Location, ((char *)This - Index->Metadata.Buffer.Location), 1, Index->Metadata.Handle);
|
|
fwrite(This, sizeof(index_metadata), 1, Index->Metadata.Handle);
|
|
fwrite(Index->Metadata.Buffer.Location + ((char *)This - Index->Metadata.Buffer.Location) + sizeof(index_metadata),
|
|
Index->Metadata.FileSize - (((char *)This - Index->Metadata.Buffer.Location) + sizeof(index_metadata)),
|
|
1, Index->Metadata.Handle);
|
|
fclose(Index->Metadata.Handle);
|
|
}
|
|
|
|
if(Prev)
|
|
{
|
|
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, Prev->BaseFilename);
|
|
CopyStringToBuffer(&PreviousPlayerPagePath, "/index.html");
|
|
|
|
file_buffer PreviousPlayerPage;
|
|
CopyStringNoFormat(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, This->BaseFilename);
|
|
CopyStringToBuffer(&ThisPlayerPagePath, "/index.html");
|
|
|
|
file_buffer ThisPlayerPage;
|
|
CopyStringNoFormat(ThisPlayerPage.Path, ThisPlayerPagePath.Location);
|
|
|
|
InsertNeighbourLink(&ThisPlayerPage, This, Prev, LINK_PREV, Index->Header.ProjectName, Next ? FALSE : TRUE);
|
|
|
|
DeclaimBuffer(&ThisPlayerPagePath);
|
|
|
|
Index->Metadata.Handle = fopen(Index->Metadata.Path, "w");
|
|
fwrite(Index->Metadata.Buffer.Location, ((char* )This - Index->Metadata.Buffer.Location), 1, Index->Metadata.Handle);
|
|
fwrite(This, sizeof(index_metadata), 1, Index->Metadata.Handle);
|
|
fwrite(Index->Metadata.Buffer.Location + ((char* )This - Index->Metadata.Buffer.Location) + sizeof(index_metadata),
|
|
Index->Metadata.FileSize - (((char* )This - Index->Metadata.Buffer.Location) + sizeof(index_metadata)),
|
|
1, Index->Metadata.Handle);
|
|
fclose(Index->Metadata.Handle);
|
|
|
|
if(EntryIndex == Index->Header.EntryCount) { break; }
|
|
}
|
|
case LINK_INCLUDE:
|
|
{
|
|
InsertNeighbourLink(&PreviousPlayerPage, Prev, This, LINK_NEXT, Index->Header.ProjectName, EntryIndex == 1 ? TRUE : FALSE);
|
|
} break;
|
|
}
|
|
|
|
DeclaimBuffer(&PreviousPlayerPagePath);
|
|
|
|
Index->Metadata.Handle = fopen(Index->Metadata.Path, "w");
|
|
fwrite(Index->Metadata.Buffer.Location, ((char *)Prev - Index->Metadata.Buffer.Location), 1, Index->Metadata.Handle);
|
|
fwrite(Prev, sizeof(index_metadata), 1, Index->Metadata.Handle);
|
|
fwrite(Index->Metadata.Buffer.Location + ((char *)Prev - Index->Metadata.Buffer.Location) + sizeof(index_metadata),
|
|
Index->Metadata.FileSize - (((char *)Prev - Index->Metadata.Buffer.Location) + sizeof(index_metadata)),
|
|
1, Index->Metadata.Handle);
|
|
fclose(Index->Metadata.Handle);
|
|
}
|
|
|
|
if(Next && LinkType == LINK_INCLUDE)
|
|
{
|
|
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, Next->BaseFilename);
|
|
CopyStringToBuffer(&NextPlayerPagePath, "/index.html");
|
|
|
|
file_buffer NextPlayerPage;
|
|
CopyStringNoFormat(NextPlayerPage.Path, NextPlayerPagePath.Location);
|
|
|
|
InsertNeighbourLink(&NextPlayerPage, Next, This, LINK_PREV, Index->Header.ProjectName, EntryIndex == (Index->Header.EntryCount - 2) ? TRUE : FALSE);
|
|
|
|
DeclaimBuffer(&NextPlayerPagePath);
|
|
|
|
Index->Metadata.Handle = fopen(Index->Metadata.Path, "w");
|
|
fwrite(Index->Metadata.Buffer.Location, ((char* )Next - Index->Metadata.Buffer.Location), 1, Index->Metadata.Handle);
|
|
fwrite(Next, sizeof(index_metadata), 1, Index->Metadata.Handle);
|
|
fwrite(Index->Metadata.Buffer.Location + ((char* )Next - Index->Metadata.Buffer.Location) + sizeof(index_metadata),
|
|
Index->Metadata.FileSize - (((char* )Next - Index->Metadata.Buffer.Location) + sizeof(index_metadata)),
|
|
1, Index->Metadata.Handle);
|
|
fclose(Index->Metadata.Handle);
|
|
}
|
|
|
|
FreeBuffer(&Index->Metadata.Buffer);
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
int
|
|
DeleteFromIndex(index *Index, char *BaseFilename)
|
|
{
|
|
// TODO(matt): LogError()
|
|
switch(ReadFileIntoBuffer(&Index->Metadata, 0))
|
|
{
|
|
case RC_ERROR_FILE:
|
|
return RC_ERROR_FILE;
|
|
case RC_ERROR_MEMORY:
|
|
LogError(LOG_ERROR, "DeleteFromIndex(): %s", strerror(errno));
|
|
return RC_ERROR_MEMORY;
|
|
case RC_SUCCESS:
|
|
break;
|
|
}
|
|
|
|
switch(ReadFileIntoBuffer(&Index->File, 0))
|
|
{
|
|
case RC_ERROR_FILE:
|
|
return RC_ERROR_FILE;
|
|
case RC_ERROR_MEMORY:
|
|
LogError(LOG_ERROR, "DeleteFromIndex(): %s", strerror(errno));
|
|
return RC_ERROR_MEMORY;
|
|
case RC_SUCCESS:
|
|
break;
|
|
}
|
|
|
|
Index->Header = *(index_header *)Index->Metadata.Buffer.Ptr;
|
|
Index->Metadata.Buffer.Ptr += sizeof(Index->Header);
|
|
|
|
bool Found = FALSE;
|
|
int DeleteMetadataFrom = -1;
|
|
int DeleteFileFrom = -1;
|
|
int DeleteFileTo = -1;
|
|
int SizeAcc = 0;
|
|
|
|
for(int EntryIndex = 0; EntryIndex < Index->Header.EntryCount; ++EntryIndex, Index->Metadata.Buffer.Ptr += sizeof(index_metadata))
|
|
{
|
|
index_metadata This = *(index_metadata *)Index->Metadata.Buffer.Ptr;
|
|
if(!StringsDiffer(This.BaseFilename, BaseFilename))
|
|
{
|
|
Found = TRUE;
|
|
--Index->Header.EntryCount;
|
|
DeleteMetadataFrom = Index->Metadata.Buffer.Ptr - Index->Metadata.Buffer.Location;
|
|
DeleteFileFrom = StringLength("---\n") + SizeAcc;
|
|
DeleteFileTo = DeleteFileFrom + This.Size;
|
|
break;
|
|
}
|
|
SizeAcc += This.Size;
|
|
}
|
|
|
|
if(Found)
|
|
{
|
|
if(Index->Header.EntryCount == 0)
|
|
{
|
|
buffer IndexDirectory;
|
|
ClaimBuffer(&IndexDirectory, "IndexDirectory", 1024);
|
|
ConstructDirectoryPath(&IndexDirectory, PAGE_INDEX, Config.IndexLocation, "");
|
|
char IndexPagePath[1024];
|
|
CopyString(IndexPagePath, "%s/index.html", IndexDirectory.Location);
|
|
remove(IndexPagePath);
|
|
remove(IndexDirectory.Location);
|
|
DeclaimBuffer(&IndexDirectory);
|
|
remove(Index->Metadata.Path);
|
|
remove(Index->File.Path);
|
|
}
|
|
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);
|
|
Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location + sizeof(Index->Header);
|
|
|
|
fwrite(Index->Metadata.Buffer.Ptr, DeleteMetadataFrom - sizeof(Index->Header), 1, Index->Metadata.Handle);
|
|
fwrite(Index->Metadata.Buffer.Ptr + DeleteMetadataFrom - sizeof(Index->Header) + sizeof(Index->Entry), Index->Metadata.FileSize - DeleteMetadataFrom - sizeof(Index->Entry), 1, Index->Metadata.Handle);
|
|
fclose(Index->Metadata.Handle);
|
|
|
|
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->Metadata.Buffer);
|
|
FreeBuffer(&Index->File.Buffer);
|
|
return Found ? RC_SUCCESS : RC_NOOP;
|
|
}
|
|
|
|
int
|
|
IndexToBuffer(index *Index, buffers *CollationBuffers) // NOTE(matt): This guy malloc's CollationBuffers->Index
|
|
{
|
|
// TODO(matt): Consider parsing the index into a linked / skip list, or do something to save us having to iterate through
|
|
// the index file multiple times
|
|
|
|
int IndexMetadataFileReadCode = ReadFileIntoBuffer(&Index->Metadata, 0);
|
|
int IndexFileReadCode = ReadFileIntoBuffer(&Index->File, 0);
|
|
|
|
if(IndexMetadataFileReadCode == RC_SUCCESS && IndexFileReadCode == RC_SUCCESS)
|
|
{
|
|
Index->Header = *(index_header*)Index->Metadata.Buffer.Ptr;
|
|
Index->Metadata.Buffer.Ptr += sizeof(Index->Header);
|
|
Index->File.Buffer.Ptr += StringLength("---\n");
|
|
char *IndexEntryStart = Index->File.Buffer.Ptr;
|
|
|
|
bool ProjectFound = FALSE;
|
|
int ProjectIndex;
|
|
for(ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex)
|
|
{
|
|
if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID))
|
|
{
|
|
ProjectFound = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!ProjectFound)
|
|
{
|
|
fprintf(stderr, "Missing Project Info for %s\n", Config.ProjectID);
|
|
FreeBuffer(&Index->Metadata.Buffer);
|
|
FreeBuffer(&Index->File.Buffer);
|
|
return RC_ERROR_PROJECT;
|
|
}
|
|
|
|
int ThemeStringLength = StringsDiffer(Config.Theme, "") ? (StringLength(Config.Theme) * 2) : (StringLength(Config.ProjectID) * 2);
|
|
char queryContainer[680 + ThemeStringLength];
|
|
CopyString(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\">\n"
|
|
" <div id=\"cineraIndexSort\">Sort: Old to New ⏶</div>\n"
|
|
" <div id=\"cineraIndexEntries\">\n",
|
|
StringsDiffer(Config.Theme, "") ? Config.Theme : Config.ProjectID,
|
|
StringsDiffer(Config.Theme, "") ? Config.Theme : Config.ProjectID);
|
|
|
|
buffer URLPrefix;
|
|
ClaimBuffer(&URLPrefix, "URLPrefix", 1024);
|
|
ConstructURLPrefix(&URLPrefix, INCLUDE_JS, PAGE_INDEX);
|
|
|
|
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 Script[532 + StringLength(URLPrefix.Location) + (StringLength(Config.ProjectID) * 2)];
|
|
CopyString(Script,
|
|
" </div>\n"
|
|
" </div>\n"
|
|
" <script type=\"text/javascript\">\n"
|
|
" var projectID = \"%s\";\n"
|
|
" var theme = \"%s\";\n"
|
|
" var baseURL = \"%s\";\n"
|
|
" var playerLocation = \"%s\";\n"
|
|
" var outputURLPrefix = \"%s\";\n"
|
|
// TODO(matt): PlayerURL
|
|
" </script>\n"
|
|
" <script type=\"text/javascript\" src=\"%scinera_search.js\"></script>\n",
|
|
Config.ProjectID,
|
|
StringsDiffer(Config.Theme, "") ? Config.Theme : Config.ProjectID,
|
|
Config.BaseURL,
|
|
Config.PlayerLocation,
|
|
StringsDiffer(Config.PlayerURLPrefix, "") ? Config.PlayerURLPrefix : Config.ProjectID,
|
|
URLPrefix.Location);
|
|
DeclaimBuffer(&URLPrefix);
|
|
|
|
int EntryLength = 32 + StringLength(ProjectInfo[ProjectIndex].Unit) + 16 + 256;
|
|
|
|
CollationBuffers->Index.Size = StringLength(queryContainer) + (Index->Header.EntryCount * EntryLength) + StringLength(Script);
|
|
|
|
if(!(CollationBuffers->Index.Location = malloc(CollationBuffers->Index.Size)))
|
|
{
|
|
FreeBuffer(&Index->Metadata.Buffer);
|
|
FreeBuffer(&Index->File.Buffer);
|
|
return(RC_ERROR_MEMORY);
|
|
}
|
|
CollationBuffers->Index.Ptr = CollationBuffers->Index.Location;
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Index, queryContainer);
|
|
|
|
for(int EntryIndex = 0; EntryIndex < Index->Header.EntryCount; ++EntryIndex)
|
|
{
|
|
index_metadata This = *(index_metadata *)Index->Metadata.Buffer.Ptr;
|
|
char Number[16];
|
|
CopyString(Number, This.BaseFilename + StringLength(Config.ProjectID));
|
|
if(ProjectInfo[ProjectIndex].NumberingScheme == NS_LINEAR)
|
|
{
|
|
for(int i = 0; Number[i]; ++i)
|
|
{
|
|
if(Number[i] == '_')
|
|
{
|
|
Number[i] = '.';
|
|
}
|
|
}
|
|
}
|
|
|
|
SeekBufferForString(&Index->File.Buffer, "title: \"", C_SEEK_FORWARDS, C_SEEK_AFTER);
|
|
char Title[256];
|
|
CopyStringNoFormatT(Title, Index->File.Buffer.Ptr, '\n');
|
|
Title[StringLength(Title) - 1] = '\0';
|
|
|
|
ConstructPlayerURL(&PlayerURL, This.BaseFilename);
|
|
|
|
if(StringsDiffer(ProjectInfo[ProjectIndex].Unit, ""))
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Index,
|
|
" <div>\n"
|
|
" <a href=\"%s\">", PlayerURL.Location);
|
|
|
|
char Text[1024]; // NOTE(matt): Surely this will be big enough
|
|
CopyString(Text, "%s %s: %s",
|
|
ProjectInfo[ProjectIndex].Unit, // TODO(matt): Do we need to special-case the various numbering schemes?
|
|
Number,
|
|
Title);
|
|
|
|
CopyStringToBufferHTMLSafe(&CollationBuffers->Index, Text);
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Index,
|
|
"</a>\n"
|
|
" </div>\n");
|
|
}
|
|
else
|
|
{
|
|
CopyStringToBuffer(&CollationBuffers->Index,
|
|
" <div>\n"
|
|
" <a href=\"%s\">%s</a>\n"
|
|
" </div>\n",
|
|
PlayerURL.Location,
|
|
Title);
|
|
}
|
|
|
|
Index->Metadata.Buffer.Ptr += sizeof(Index->Entry);
|
|
IndexEntryStart += This.Size;
|
|
Index->File.Buffer.Ptr = IndexEntryStart;
|
|
}
|
|
|
|
DeclaimBuffer(&PlayerURL);
|
|
|
|
CopyStringToBuffer(&CollationBuffers->Index, Script);
|
|
|
|
FreeBuffer(&Index->Metadata.Buffer);
|
|
FreeBuffer(&Index->File.Buffer);
|
|
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, 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, "%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;
|
|
}
|
|
}
|
|
int PlayerOffset = 0; // NOTE(matt): Could just straight up pass the LinkOffsets.PrevStart directly...
|
|
BuffersToHTML(CollationBuffers, PlayerTemplate, PlayerPagePath, PAGE_PLAYER, &PlayerOffset);
|
|
Index->Entry.LinkOffsets.PrevStart += PlayerOffset;
|
|
|
|
ReadFileIntoBuffer(&Index->Metadata, 0);
|
|
Index->Metadata.Buffer.Ptr += sizeof(index_header);
|
|
int MetadataInsertionOffset = 0;
|
|
for(int EntryIndex = 0; EntryIndex < Index->Header.EntryCount; ++EntryIndex)
|
|
{
|
|
index_metadata This = *(index_metadata *)Index->Metadata.Buffer.Ptr;
|
|
if(!StringsDiffer(This.BaseFilename, Index->Entry.BaseFilename))
|
|
{
|
|
MetadataInsertionOffset = (Index->Metadata.Buffer.Ptr - Index->Metadata.Buffer.Location);
|
|
break;
|
|
}
|
|
Index->Metadata.Buffer.Ptr += sizeof(index_metadata);
|
|
}
|
|
|
|
if(!(Index->Metadata.Handle = fopen(Index->Metadata.Path, "w"))) { FreeBuffer(&Index->Metadata.Buffer); return RC_ERROR_FILE; }
|
|
fwrite(Index->Metadata.Buffer.Location, MetadataInsertionOffset, 1, Index->Metadata.Handle);
|
|
fwrite(&Index->Entry, sizeof(Index->Entry), 1, Index->Metadata.Handle);
|
|
fwrite(Index->Metadata.Buffer.Ptr + sizeof(Index->Entry), Index->Metadata.FileSize - MetadataInsertionOffset - sizeof(Index->Entry), 1, Index->Metadata.Handle);
|
|
fclose(Index->Metadata.Handle);
|
|
FreeBuffer(&Index->Metadata.Buffer);
|
|
|
|
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, "%s/index.html", OutputDirectoryPath.Location);
|
|
DeclaimBuffer(&OutputDirectoryPath);
|
|
IndexToBuffer(Index, CollationBuffers);
|
|
BuffersToHTML(CollationBuffers, IndexTemplate, IndexPagePath, PAGE_INDEX, 0);
|
|
FreeBuffer(&CollationBuffers->Index);
|
|
return RC_SUCCESS;
|
|
}
|
|
|
|
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, "%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;
|
|
}
|
|
|
|
void
|
|
DeleteEntry(index *Index, char *BaseFilename)
|
|
{
|
|
if(DeleteFromIndex(Index, BaseFilename) == RC_SUCCESS)
|
|
{
|
|
LinkNeighbours(Index, BaseFilename, LINK_EXCLUDE);
|
|
DeletePlayerPageFromFilesystem(BaseFilename, Config.PlayerLocation, FALSE);
|
|
}
|
|
}
|
|
|
|
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(1)) == RC_ARENA_FULL) { return RC_ARENA_FULL; };
|
|
|
|
struct inotify_event *Event;
|
|
int BytesRead = read(inotifyInstance, Events.Location, Events.Size);
|
|
if(inotifyInstance < 0) { perror("MonitorDirectory()"); }
|
|
|
|
for(Events.Ptr = Events.Location;
|
|
Events.Ptr < Events.Location + BytesRead && Events.Ptr - Events.Location < Events.Size;
|
|
Events.Ptr += sizeof(struct inotify_event) + Event->len)
|
|
{
|
|
Event = (struct inotify_event *)Events.Ptr;
|
|
char *Ptr;
|
|
Ptr = Event->name;
|
|
Ptr += (StringLength(Event->name) - StringLength(".hmml"));
|
|
if(!(StringsDiffer(Ptr, ".hmml")))
|
|
{
|
|
*Ptr = '\0';
|
|
char BaseFilename[256];
|
|
CopyString(BaseFilename, Event->name);
|
|
*Ptr = '.';
|
|
|
|
// TODO(matt): Maybe handle IN_ALL_EVENTS
|
|
if(Event->mask & IN_DELETE || Event->mask & IN_MOVED_FROM)
|
|
{
|
|
DeleteEntry(Index, BaseFilename);
|
|
}
|
|
else
|
|
{
|
|
switch(InsertIntoIndex(Index, CollationBuffers, &BespokeTemplate, BaseFilename))
|
|
{
|
|
case RC_SUCCESS:
|
|
case RC_UNFOUND:
|
|
LinkNeighbours(Index, BaseFilename, LINK_INCLUDE);
|
|
{
|
|
if(BespokeTemplate->Metadata.Filename && StringsDiffer(BespokeTemplate->Metadata.Filename, ""))
|
|
{
|
|
GeneratePlayerPage(Index, CollationBuffers, BespokeTemplate, BaseFilename);
|
|
DeclaimTemplate(BespokeTemplate);
|
|
}
|
|
else
|
|
{
|
|
GeneratePlayerPage(Index, CollationBuffers, PlayerTemplate, BaseFilename);
|
|
}
|
|
GenerateIndexPage(Index, CollationBuffers, IndexTemplate);
|
|
} break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
DeclaimBuffer(&Events);
|
|
return RC_NOOP;
|
|
}
|
|
|
|
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 = { 0 };
|
|
index2 Index2 = { 0 };
|
|
|
|
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, 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;
|
|
|
|
if(ReadFileIntoBuffer(&Index->File, 0) == RC_ERROR_FILE)
|
|
{
|
|
fprintf(stderr, "\e[1;31mUnable to open index file\e[0m %s: %s\n"
|
|
"Removing %s and starting afresh\n", Index->File.Path, strerror(errno),
|
|
Index->Metadata.Path);
|
|
fclose(Index->Metadata.Handle);
|
|
FreeBuffer(&Index->Metadata.Buffer);
|
|
remove(Index->Metadata.Path);
|
|
return RC_ERROR_FILE;
|
|
}
|
|
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, 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;
|
|
}
|
|
|
|
FreeBuffer(&Index->File.Buffer);
|
|
|
|
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);
|
|
Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location + OriginalHeaderSize;
|
|
fwrite(Index->Metadata.Buffer.Ptr, Index->Metadata.FileSize - OriginalHeaderSize, 1, Index->Metadata.Handle);
|
|
Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location;
|
|
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?
|
|
|
|
int
|
|
DeleteDeadIndexEntries(index *Index)
|
|
{
|
|
// TODO(matt): More rigorously figure out who we should delete
|
|
// Maybe compare the output directory and the input HMML names
|
|
if(ReadFileIntoBuffer(&Index->Metadata, 0) == RC_ERROR_FILE)
|
|
{
|
|
return RC_ERROR_FILE;
|
|
}
|
|
|
|
Index->Header = *(index_header *)Index->Metadata.Buffer.Ptr;
|
|
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);
|
|
}
|
|
|
|
if(StringsDiffer(Index->Header.PlayerLocation, Config.PlayerLocation))
|
|
{
|
|
buffer PlayerDirectory;
|
|
ClaimBuffer(&PlayerDirectory, "PlayerDirectory", 1024);
|
|
printf("\e[1;33mRelocating Player Page%s from %s to %s\e[0m\n",
|
|
Index->Header.EntryCount > 1 ? "s" : "",
|
|
(StringsDiffer(Index->Header.PlayerLocation, "") ? Index->Header.PlayerLocation : (StringsDiffer(Config.BaseDir, ".") ? Config.BaseDir : "\"Base Directory\"")),
|
|
(StringsDiffer(Config.PlayerLocation, "") ? Config.PlayerLocation : (StringsDiffer(Config.BaseDir, ".") ? Config.BaseDir : "\"Base Directory\"")));
|
|
Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location + sizeof(Index->Header);
|
|
for(int EntryIndex = 0; EntryIndex < Index->Header.EntryCount; ++EntryIndex)
|
|
{
|
|
index_metadata This = *(index_metadata *)Index->Metadata.Buffer.Ptr;
|
|
ConstructDirectoryPath(&PlayerDirectory, PAGE_PLAYER, Index->Header.PlayerLocation, This.BaseFilename);
|
|
DeletePlayerPageFromFilesystem(This.BaseFilename, Index->Header.PlayerLocation, TRUE);
|
|
Index->Metadata.Buffer.Ptr += sizeof(This);
|
|
}
|
|
DeclaimBuffer(&PlayerDirectory);
|
|
RemoveDirectoryRecursively(Index->Header.PlayerLocation);
|
|
ClearCopyStringNoFormat(Index->Header.PlayerLocation, sizeof(Index->Header.PlayerLocation), Config.PlayerLocation);
|
|
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 + sizeof(Index->Header);
|
|
fwrite(Index->Metadata.Buffer.Ptr, Index->Metadata.FileSize - sizeof(Index->Header), 1, Index->Metadata.Handle);
|
|
Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location;
|
|
fclose(Index->Metadata.Handle);
|
|
}
|
|
if(StringsDiffer(Index->Header.IndexLocation, Config.IndexLocation))
|
|
{
|
|
printf("\e[1;33mRelocating Index Page from %s to %s\e[0m\n",
|
|
(StringsDiffer(Index->Header.IndexLocation, "") ? Index->Header.IndexLocation : (StringsDiffer(Config.BaseDir, ".") ? Config.BaseDir : "\"Base Directory\"")),
|
|
(StringsDiffer(Config.IndexLocation, "") ? Config.IndexLocation : (StringsDiffer(Config.BaseDir, ".") ? Config.BaseDir : "\"Base Directory\"")));
|
|
buffer IndexDirectory;
|
|
ClaimBuffer(&IndexDirectory, "IndexDirectory", 1024);
|
|
ConstructDirectoryPath(&IndexDirectory, PAGE_INDEX, Index->Header.IndexLocation, "");
|
|
char IndexPagePath[2048] = { 0 };
|
|
CopyString(IndexPagePath, "%s/index.html", IndexDirectory.Location);
|
|
remove(IndexPagePath);
|
|
RemoveDirectoryRecursively(IndexDirectory.Location);
|
|
DeclaimBuffer(&IndexDirectory);
|
|
|
|
ClearCopyStringNoFormat(Index->Header.IndexLocation, sizeof(Index->Header.IndexLocation), Config.IndexLocation);
|
|
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 + sizeof(Index->Header);
|
|
fwrite(Index->Metadata.Buffer.Ptr, Index->Metadata.FileSize - sizeof(Index->Header), 1, Index->Metadata.Handle);
|
|
Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location;
|
|
fclose(Index->Metadata.Handle);
|
|
}
|
|
|
|
Index->Metadata.Buffer.Ptr = Index->Metadata.Buffer.Location + sizeof(Index->Header);
|
|
|
|
index_entry Entries[Index->Header.EntryCount];
|
|
|
|
for(int EntryIndex = 0; EntryIndex < Index->Header.EntryCount; ++EntryIndex)
|
|
{
|
|
index_metadata This = *(index_metadata *)Index->Metadata.Buffer.Ptr;
|
|
CopyStringNoFormat(Entries[EntryIndex].ID, This.BaseFilename);
|
|
Entries[EntryIndex].Present = FALSE;
|
|
Index->Metadata.Buffer.Ptr += sizeof(This);
|
|
}
|
|
|
|
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;
|
|
DeleteEntry(Index, Entries[i].ID);
|
|
}
|
|
}
|
|
|
|
FreeBuffer(&Index->Metadata.Buffer);
|
|
return Deleted ? RC_SUCCESS : RC_NOOP;
|
|
}
|
|
|
|
int
|
|
SyncIndexWithInput(index *Index, buffers *CollationBuffers, template *IndexTemplate, template *PlayerTemplate, template *BespokeTemplate)
|
|
{
|
|
bool Deleted = FALSE;
|
|
if(DeleteDeadIndexEntries(Index) == RC_SUCCESS)
|
|
{
|
|
Deleted = TRUE;
|
|
}
|
|
|
|
DIR *ProjectDirHandle;
|
|
if(!(ProjectDirHandle = opendir(Config.ProjectDir)))
|
|
{
|
|
LogError(LOG_ERROR, "Unable to scan project directory %s: %s", Config.ProjectDir, strerror(errno));
|
|
fprintf(stderr, "Unable to scan project directory %s: %s\n", Config.ProjectDir, strerror(errno));
|
|
return RC_ERROR_DIRECTORY;
|
|
}
|
|
|
|
struct dirent *ProjectFiles;
|
|
bool Inserted = FALSE;
|
|
|
|
while((ProjectFiles = readdir(ProjectDirHandle)))
|
|
{
|
|
char *Ptr = ProjectFiles->d_name;
|
|
Ptr += (StringLength(ProjectFiles->d_name) - StringLength(".hmml"));
|
|
if(!(StringsDiffer(Ptr, ".hmml")))
|
|
{
|
|
*Ptr = '\0';
|
|
switch(InsertIntoIndex(Index, CollationBuffers, &BespokeTemplate, ProjectFiles->d_name))
|
|
{
|
|
case RC_UNFOUND:
|
|
LinkNeighbours(Index, ProjectFiles->d_name, LINK_INCLUDE);
|
|
case RC_SUCCESS:
|
|
{
|
|
if(BespokeTemplate->Metadata.Filename && StringsDiffer(BespokeTemplate->Metadata.Filename, ""))
|
|
{
|
|
GeneratePlayerPage(Index, CollationBuffers, BespokeTemplate, ProjectFiles->d_name);
|
|
DeclaimTemplate(BespokeTemplate);
|
|
}
|
|
else
|
|
{
|
|
GeneratePlayerPage(Index, CollationBuffers, PlayerTemplate, ProjectFiles->d_name);
|
|
}
|
|
Inserted = TRUE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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)
|
|
{
|
|
#if 0
|
|
printf("%lu\n", sizeof(index_metadata));
|
|
exit(1);
|
|
#endif
|
|
// 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",
|
|
.ForceIntegration = FALSE,
|
|
.ProjectDir = ".",
|
|
.ProjectID = "",
|
|
.Theme = "",
|
|
.UpdateInterval = 4,
|
|
.PlayerURLPrefix = ""
|
|
};
|
|
|
|
if(getenv("XDG_CACHE_HOME"))
|
|
{
|
|
CopyString(DefaultConfig.CacheDir, "%s/cinera", getenv("XDG_CACHE_HOME"));
|
|
}
|
|
else
|
|
{
|
|
CopyString(DefaultConfig.CacheDir, "%s/.cache/cinera", getenv("HOME"));
|
|
}
|
|
|
|
Config = DefaultConfig;
|
|
|
|
if(ArgC < 2)
|
|
{
|
|
PrintUsage(Args[0], &DefaultConfig);
|
|
return RC_RIP;
|
|
}
|
|
|
|
char CommandLineArg;
|
|
while((CommandLineArg = getopt(ArgC, Args, "a:b:B:c:d:efhi: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.ForceIntegration = TRUE;
|
|
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 = { 0 };
|
|
Index.Metadata.Buffer.ID = "IndexMetadata";
|
|
// TODO(matt): Allow optionally passing a .metadata file as an argument?
|
|
CopyString(Index.Metadata.Path, "%s/%s.metadata", Config.BaseDir, Config.ProjectID);
|
|
ExamineIndex(&Index);
|
|
exit(RC_SUCCESS);
|
|
}
|
|
|
|
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;
|
|
for(int ProjectInfoIndex = 0; ProjectInfoIndex < ArrayCount(ProjectInfo); ++ProjectInfoIndex)
|
|
{
|
|
if(!StringsDiffer(Config.ProjectID, ProjectInfo[ProjectInfoIndex].ProjectID))
|
|
{
|
|
|
|
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(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): 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; };
|
|
// 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",
|
|
|
|
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,
|
|
StringsDiffer(Config.Theme, "") ? Config.Theme: Config.ProjectID,
|
|
|
|
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);
|
|
|
|
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 = { 0 };
|
|
Index.Metadata.Buffer.ID = "IndexMetadata";
|
|
CopyString(Index.Metadata.Path, "%s/%s.metadata", Config.BaseDir, Config.ProjectID);
|
|
Index.File.Buffer.ID = "IndexFile";
|
|
CopyString(Index.File.Path, "%s/%s.index", Config.BaseDir, Config.ProjectID);
|
|
|
|
printf("┌╼ Synchronising with annotation files in Project Input Directory ╾┐\n");
|
|
SyncIndexWithInput(&Index, &CollationBuffers, IndexTemplate, PlayerTemplate, BespokeTemplate);
|
|
if(Config.Mode & MODE_ONESHOT)
|
|
{
|
|
goto RIP;
|
|
}
|
|
|
|
printf("\n┌╼ Monitoring Project 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_ERROR_FATAL)
|
|
{
|
|
// 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.
|
|
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, Args[FileIndex]);
|
|
switch(HMMLToBuffers(&CollationBuffers, &BespokeTemplate, Args[FileIndex], 0, 0))
|
|
{
|
|
// TODO(matt): Actually sort out the fatality of these cases, once we are always-on
|
|
case RC_ERROR_FILE:
|
|
case RC_ERROR_FATAL:
|
|
goto RIP;
|
|
case RC_ERROR_HMML:
|
|
case RC_ERROR_MAX_REFS:
|
|
case RC_ERROR_QUOTE:
|
|
case RC_INVALID_REFERENCE:
|
|
if(FileIndex < (ArgC - 1)) { goto NextFile; }
|
|
else { goto RIP; }
|
|
case RC_SUCCESS:
|
|
break;
|
|
};
|
|
|
|
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, once we are always-on
|
|
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
|
|
|
|
}
|