Annotation-System/cinera/cinera.c

15129 lines
536 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#if 0
ctime -begin ${0%.*}.ctm
#gcc -g -fsanitize=address -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl
gcc -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl
#clang -fsanitize=address -g -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl
#clang -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} hmml.a -lcurl
ctime -end ${0%.*}.ctm
exit
#endif
#include <stdint.h>
typedef struct
{
uint32_t Major, Minor, Patch;
} version;
version CINERA_APP_VERSION = {
.Major = 0,
.Minor = 7,
.Patch = 3
};
#include <stdarg.h> // NOTE(matt): varargs
#include <stdio.h> // NOTE(matt): printf, sprintf, vsprintf, fprintf, perror
#include <stdlib.h> // NOTE(matt): calloc, malloc, free
#include "hmmlib.h"
#include <getopt.h> // NOTE(matt): getopts
#include <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 <sys/ioctl.h> // NOTE(matt): ioctl and TIOCGWINSZ
#include <wordexp.h>
#define __USE_XOPEN2K8 // NOTE(matt): O_NOFOLLOW
#include <fcntl.h> // NOTE(matt): open()
#define __USE_XOPEN2K // NOTE(matt): readlink()
#include <unistd.h> // NOTE(matt): sleep()
#define STB_IMAGE_IMPLEMENTATION
#define STBI_NO_LINEAR
#define STBI_NO_HDR
#define STBI_ONLY_GIF
#define STBI_ONLY_JPEG
#define STBI_ONLY_PNG
#include "stb_image.h"
typedef uint64_t bool;
#define TRUE 1
#define FALSE 0
#define enum8(type) int8_t
#define enum16(type) int16_t
#define enum32(type) int32_t
// DEBUG SWITCHES
#define DEBUG_MEMORY_LEAKAGE 0
#define DEBUG_MEMORY_LEAKAGE_LOOPED 0
#define DEBUG_PRINT_FUNCTION_NAMES 0
#define DEBUG_PROJECT_INDICES 0
#define DEBUG 0
#define DEBUG_MEM 0
//
////
bool PROFILING = 0;
clock_t TIMING_START;
#define START_TIMING_BLOCK(...) if(PROFILING) { printf(__VA_ARGS__); TIMING_START = clock(); }
#define END_TIMING_BLOCK() if(PROFILING) { printf("\e[1;34m%ld\e[0m\n", clock() - TIMING_START);}
#define Kilobytes(Bytes) Bytes << 10
#define Megabytes(Bytes) Bytes << 20
#define MAX_UNIT_LENGTH 16
#define MAX_PROJECT_ID_LENGTH 32
#define MAX_THEME_LENGTH MAX_PROJECT_ID_LENGTH
#define MAX_PROJECT_NAME_LENGTH 64
#define MAX_BASE_DIR_LENGTH 128
#define MAX_BASE_URL_LENGTH 128
#define MAX_RELATIVE_PAGE_LOCATION_LENGTH 32
#define MAX_VOD_ID_LENGTH 32
#define MAX_ROOT_DIR_LENGTH 128
#define MAX_ROOT_URL_LENGTH 128
#define MAX_RELATIVE_ASSET_LOCATION_LENGTH 32
#define MAX_BASE_FILENAME_LENGTH 32
#define MAX_ENTRY_OUTPUT_LENGTH MAX_BASE_FILENAME_LENGTH
#define MAX_TITLE_LENGTH 128
#define MAX_ASSET_FILENAME_LENGTH 64
// TODO(matt): Stop distinguishing between short / long and lift the size limit once we're on the LUT
#define MAX_CUSTOM_SNIPPET_SHORT_LENGTH 256
#define MAX_CUSTOM_SNIPPET_LONG_LENGTH 1024
#define INDENT_WIDTH 4
#define ArrayCount(A) sizeof(A)/sizeof(*(A))
#define Assert(Expression) do { if(!(Expression)){ fprintf(stderr, "l.%d: \e[1;31mAssertion failure\e[0m\n", __LINE__); __asm__("int3"); } } while(0)
#define BreakHere() fprintf(stderr, "[%i] BreakHere\n", __LINE__)
#define FOURCC(String) ((uint32_t)(String[0] << 0) | (uint32_t)(String[1] << 8) | (uint32_t)(String[2] << 16) | (uint32_t)(String[3] << 24))
#define Free(M) free(M); M = 0;
#define FreeAndResetCount(M, Count) { Free(M); Count = 0; }
#define FreeAndResetCountAndCapacity(M, Count, Capacity) { FreeAndResetCount(M, Count); Capacity = 0; }
#define MIN(A, B) A < B ? A : B
void
Clear(void *V, uint64_t Size)
{
char *Ptr = (char *)V;
for(int i = 0; i < Size; ++i)
{
*Ptr++ = 0;
}
}
void *
Fit(void *Base, uint16_t WidthInBytes, uint64_t ItemCount, uint32_t ItemsPerBlock, bool ZeroInitialise)
{
if(ItemCount % ItemsPerBlock == 0)
{
Base = realloc(Base, (ItemCount + ItemsPerBlock) * WidthInBytes);
if(ZeroInitialise)
{
Clear(Base + WidthInBytes * ItemCount, WidthInBytes * ItemsPerBlock);
}
}
return Base;
}
void *
FitShrinkable(void *Base, uint16_t WidthInBytes, uint64_t ItemCount, uint64_t *Capacity, uint32_t ItemsPerBlock, bool ZeroInitialise)
{
if(ItemCount == *Capacity)
{
Base = realloc(Base, (ItemCount + ItemsPerBlock) * WidthInBytes);
if(ZeroInitialise)
{
Clear(Base + WidthInBytes * ItemCount, WidthInBytes * ItemsPerBlock);
}
*Capacity += ItemsPerBlock;
}
return Base;
}
// NOTE(matt): Memory testing
#include <sys/resource.h>
int
GetUsage(void)
{
struct rusage Usage;
getrusage(RUSAGE_SELF, &Usage);
return Usage.ru_maxrss;
}
// NOTE(matt): Mem loop testing
//
// Let the usual work function happen, then:
//
// Call MEM_LOOP_PRE_FREE("Thing you're testing")
// Call the freeing function
// Call MEM_LOOP_PRE_WORK()
// Call the function whose work the freeing function is meant to free
// Call MEM_LOOP_POST("Thing you're testing")
//
#if DEBUG_MEMORY_LEAKAGE_LOOPED
#define MEM_LOOP_PRE_FREE(String) \
fprintf(stderr, "Testing %s freeing\n", String);\
int MemLoopInitial = GetUsage();\
int MemLoopOld = MemLoopInitial;\
int MemLoopNew;\
for(int i = 0; i < 1 << 16; ++i)\
{
#define MEM_LOOP_PRE_WORK() \
MemLoopNew = GetUsage();\
if(1 /*MemLoopNew > MemLoopOld*/) { fprintf(stderr, "iter %2i: %i (%s%i)\n", i, MemLoopNew, MemLoopNew > MemLoopOld ? "+" : "-", MemLoopNew - MemLoopOld); sleep(1); }\
#define MEM_LOOP_POST(String) \
MemLoopOld = MemLoopNew;\
}\
fprintf(stderr, "Done (%s): ", String);\
Colourise(MemLoopNew > MemLoopInitial ? CS_RED : CS_GREEN);\
fprintf(stderr, "%s%i\n", MemLoopNew > MemLoopInitial ? "+" : "-", MemLoopNew - MemLoopInitial);\
Colourise(CS_END);\
sleep(4);
#else
#define MEM_LOOP_PRE_FREE(String)
#define MEM_LOOP_PRE_WORK()
#define MEM_LOOP_POST(String)
#endif
//
////
// NOTE(matt): One-time memory testing
//
#if DEBUG_MEMORY_LEAKAGE
#define MEM_TEST_INITIAL() int Initial = GetUsage()
#define MEM_TEST_MID(String) if(GetUsage() > Initial) fprintf(stderr, "%s: %i kb\n", String, GetUsage() - Initial)
#define MEM_TEST_AFTER(String) int Current = GetUsage();\
Colourise(Current > Initial ? CS_RED : CS_BLUE);\
fprintf(stderr, "%s leaks %i kb\n", String, Current - Initial);\
Colourise(CS_END);
#else
#define MEM_TEST_INITIAL()
#define MEM_TEST_MID(String)
#define MEM_TEST_AFTER(String)
#endif
//
////
typedef enum
{
//MODE_FORCEINTEGRATION = 1 << 0,
MODE_EXAMINE = 1 << 0,
MODE_DRYRUN = 1 << 1,
} mode;
typedef enum
{
RC_ARENA_FULL,
RC_ERROR_CAPACITY,
RC_ERROR_DIRECTORY,
RC_ERROR_FATAL,
RC_ERROR_FILE,
RC_ERROR_HMML,
RC_ERROR_MAX_REFS,
RC_ERROR_MEMORY,
RC_ERROR_MISSING_PARAMETER,
RC_ERROR_PARSING,
RC_ERROR_PARSING_UNCLOSED_QUOTED_STRING,
RC_ERROR_PROJECT,
RC_ERROR_QUOTE,
RC_ERROR_SEEK,
RC_FOUND,
RC_INVALID_IDENTIFIER,
RC_INVALID_REFERENCE,
RC_INVALID_TEMPLATE,
RC_NOOP,
RC_PRIVATE_VIDEO,
RC_RIP,
RC_SCHEME_MIXTURE,
RC_SYNTAX_ERROR,
RC_UNFOUND,
RC_FAILURE,
RC_SUCCESS
} rc;
typedef struct
{
void *Location;
void *Ptr;
char *ID;
int Size;
} arena;
char *BufferIDStrings[] =
{
"BID_NULL",
"BID_CHECKSUM",
"BID_COLLATION_BUFFERS_CUSTOM0",
"BID_COLLATION_BUFFERS_CUSTOM1",
"BID_COLLATION_BUFFERS_CUSTOM2",
"BID_COLLATION_BUFFERS_CUSTOM3",
"BID_COLLATION_BUFFERS_CUSTOM4",
"BID_COLLATION_BUFFERS_CUSTOM5",
"BID_COLLATION_BUFFERS_CUSTOM6",
"BID_COLLATION_BUFFERS_CUSTOM7",
"BID_COLLATION_BUFFERS_CUSTOM8",
"BID_COLLATION_BUFFERS_CUSTOM9",
"BID_COLLATION_BUFFERS_CUSTOM10",
"BID_COLLATION_BUFFERS_CUSTOM11",
"BID_COLLATION_BUFFERS_CUSTOM12",
"BID_COLLATION_BUFFERS_CUSTOM13",
"BID_COLLATION_BUFFERS_CUSTOM14",
"BID_COLLATION_BUFFERS_CUSTOM15",
"BID_COLLATION_BUFFERS_INCLUDES_PLAYER",
"BID_COLLATION_BUFFERS_INCLUDES_SEARCH",
"BID_COLLATION_BUFFERS_PLAYER",
"BID_COLLATION_BUFFERS_SEARCH", // malloc'd
"BID_COLLATION_BUFFERS_SEARCH_ENTRY",
"BID_COLLATION_BUFFERS_TITLE",
"BID_COLLATION_BUFFERS_URL_PLAYER",
"BID_COLLATION_BUFFERS_URL_SEARCH",
"BID_COLLATION_BUFFERS_VIDEO_ID",
"BID_COLLATION_BUFFERS_VOD_PLATFORM",
"BID_DATABASE",
"BID_ERRORS",
"BID_FILTER",
"BID_INDEX",
"BID_INDEX_BUFFERS_CATEGORY_ICONS",
"BID_INDEX_BUFFERS_CLASS",
"BID_INDEX_BUFFERS_DATA",
"BID_INDEX_BUFFERS_HEADER",
"BID_INDEX_BUFFERS_MASTER",
"BID_INDEX_BUFFERS_TEXT",
"BID_INOTIFY_EVENTS",
"BID_LINK",
"BID_MASTER",
"BID_MENU_BUFFERS_CREDITS",
"BID_MENU_BUFFERS_FILTER",
"BID_MENU_BUFFERS_FILTER_MEDIA",
"BID_MENU_BUFFERS_FILTER_TOPICS",
"BID_MENU_BUFFERS_QUOTE",
"BID_MENU_BUFFERS_REFERENCE",
"BID_NEXT_PLAYER_URL",
"BID_PLAYER_BUFFERS_MAIN",
"BID_PLAYER_BUFFERS_MENUS",
"BID_PLAYER_BUFFERS_SCRIPT",
"BID_PREVIOUS_PLAYER_URL",
"BID_PWD_STRIPPED_PATH",
"BID_QUOTE_STAGING",
"BID_RESOLVED_PATH",
"BID_SCRIPT", // malloc'd
"BID_TOPICS",
"BID_TO_PLAYER_URL",
"BID_URL",
"BID_URL_ASSET",
"BID_URL_PLAYER",
"BID_URL_SEARCH",
"BID_VIDEO_API_RESPONSE",
};
// TODO(matt): Note which buffers malloc
typedef enum
{
BID_NULL,
BID_CHECKSUM,
BID_COLLATION_BUFFERS_CUSTOM0, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_CUSTOM1, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_CUSTOM2, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_CUSTOM3, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_CUSTOM4, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_CUSTOM5, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_CUSTOM6, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_CUSTOM7, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_CUSTOM8, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_CUSTOM9, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_CUSTOM10, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_CUSTOM11, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_CUSTOM12, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_CUSTOM13, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_CUSTOM14, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_CUSTOM15, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_INCLUDES_PLAYER,
BID_COLLATION_BUFFERS_INCLUDES_SEARCH,
BID_COLLATION_BUFFERS_PLAYER,
BID_COLLATION_BUFFERS_SEARCH, // malloc'd
BID_COLLATION_BUFFERS_SEARCH_ENTRY,
BID_COLLATION_BUFFERS_TITLE, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_URL_PLAYER, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_URL_SEARCH, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_VIDEO_ID, // NOTE(matt): Not a buffer
BID_COLLATION_BUFFERS_VOD_PLATFORM, // NOTE(matt): Not a buffer
BID_DATABASE, // File.Buffer, malloc'd
BID_ERRORS,
BID_FILTER, // malloc'd
BID_INDEX, // malloc'd
BID_INDEX_BUFFERS_CATEGORY_ICONS,
BID_INDEX_BUFFERS_CLASS,
BID_INDEX_BUFFERS_DATA,
BID_INDEX_BUFFERS_HEADER,
BID_INDEX_BUFFERS_MASTER,
BID_INDEX_BUFFERS_TEXT,
BID_INOTIFY_EVENTS,
BID_LINK,
BID_MASTER, // NOTE(matt): This shall only ever be the destination, never the source. It is also malloc'd
BID_MENU_BUFFERS_CREDITS,
BID_MENU_BUFFERS_FILTER,
BID_MENU_BUFFERS_FILTER_MEDIA,
BID_MENU_BUFFERS_FILTER_TOPICS,
BID_MENU_BUFFERS_QUOTE,
BID_MENU_BUFFERS_REFERENCE,
BID_NEXT_PLAYER_URL,
BID_PLAYER_BUFFERS_MAIN,
BID_PLAYER_BUFFERS_MENUS,
BID_PLAYER_BUFFERS_SCRIPT,
BID_PREVIOUS_PLAYER_URL,
BID_PWD_STRIPPED_PATH,
BID_QUOTE_STAGING, // malloc'd
BID_RESOLVED_PATH,
BID_SCRIPT, // malloc'd
BID_TOPICS, // File.Buffer, malloc'd
BID_TO_PLAYER_URL,
BID_URL,
BID_URL_ASSET,
BID_URL_PLAYER,
BID_URL_SEARCH,
BID_VIDEO_API_RESPONSE,
BID_COUNT,
} buffer_id;
typedef struct
{
char *Location;
char *Ptr;
buffer_id ID;
uint64_t Size;
uint32_t IndentLevel;
} buffer;
typedef struct
{
char *Base;
uint64_t Length;
} string;
int
CopyStringToBarePtr(char *Dest, string Src)
{
for(int i = 0; i < Src.Length; ++i)
{
*Dest++ = Src.Base[i];
}
return Src.Length;
}
typedef struct
{
char *Base;
char *Ptr;
} memory_page;
typedef struct memory_pen_location
{
memory_page *Page;
string String;
struct memory_pen_location *Prev;
struct memory_pen_location *Next;
} memory_pen_location;
typedef enum
{
MBT_NONE,
MBT_ASSET,
MBT_STRING,
} memory_book_type;
typedef struct
{
int64_t PageCount;
uint64_t PageSize;
uint64_t DataWidthInBytes;
memory_page *Pages;
memory_pen_location *Pen;
memory_book_type Type;
} memory_book;
void
InitBook(memory_book *M, uint64_t DataWidthInBytes, uint64_t ItemsPerPage, memory_book_type Type)
{
M->PageSize = ItemsPerPage * DataWidthInBytes;
M->Type = Type;
M->DataWidthInBytes = DataWidthInBytes;
}
memory_page *
AddPage(memory_book *M)
{
M->Pages = realloc(M->Pages, (M->PageCount + 1) * sizeof(memory_page));
M->Pages[M->PageCount].Base = calloc(1, M->PageSize);
M->Pages[M->PageCount].Ptr = M->Pages[M->PageCount].Base;
++M->PageCount;
return &M->Pages[M->PageCount - 1];
}
void
FreePage(memory_page *P)
{
Free(P->Base);
P->Ptr = 0;
}
void
FreeBook(memory_book *M)
{
for(int i = 0; i < M->PageCount; ++i)
{
FreePage(&M->Pages[i]);
}
FreeAndResetCount(M->Pages, M->PageCount);
if(M->Type == MBT_STRING)
{
memory_pen_location *This = M->Pen;
while(This)
{
M->Pen = M->Pen->Prev;
Free(This);
This = M->Pen;
}
}
memory_book Zero = {};
*M = Zero;
}
void
FreeAndReinitialiseBook(memory_book *M)
{
int PageSize = M->PageSize;
int Type = M->Type;
FreeBook(M);
M->PageSize = PageSize;
M->Type = Type;
}
memory_page *
GetOrAddPageForString(memory_book *M, string S)
{
for(int i = 0; i < M->PageCount; ++i)
{
if((M->Pages[i].Ptr - M->Pages[i].Base) + S.Length < M->PageSize)
{
return &M->Pages[i];
}
}
return AddPage(M);
}
memory_page *
GetOrAddPageForSize(memory_book *M, uint64_t Size)
{
for(int i = 0; i < M->PageCount; ++i)
{
if((M->Pages[i].Ptr - M->Pages[i].Base) + Size < M->PageSize)
{
return &M->Pages[i];
}
}
return AddPage(M);
}
string
WriteStringInBook(memory_book *M, string S)
{
string Result = {};
memory_page *Page = GetOrAddPageForString(M, S);
memory_pen_location *Pen = malloc(sizeof(memory_pen_location));
Pen->Page = Page;
Pen->String.Base = Page->Ptr;
Pen->String.Length = S.Length;
Pen->Prev = M->Pen;
Pen->Next = 0;
if(M->Pen)
{
M->Pen->Next = Pen;
}
M->Pen = Pen;
Result.Base = Page->Ptr;
Result.Length = S.Length;
Page->Ptr += CopyStringToBarePtr(Page->Ptr, S);
return Result;
}
typedef struct
{
int64_t Page;
uint64_t Line;
} book_position;
book_position
GetBookPositionFromIndex(memory_book *M, int Index)
{
book_position Result = {};
int ItemsPerPage = M->PageSize / M->DataWidthInBytes;
Result.Line = Index % ItemsPerPage;
Index -= Result.Line;
Result.Page = Index / ItemsPerPage;
return Result;
}
void *
GetPlaceInBook(memory_book *M, int Index)
{
void *Result = 0;
book_position Position = GetBookPositionFromIndex(M, Index);
memory_page *Page = M->Pages + Position.Page;
while((Position.Page + 1) > M->PageCount)
{
Page = AddPage(M);
}
char *Ptr = Page->Base;
Ptr += M->DataWidthInBytes * Position.Line;
Result = Ptr;
return Result;
}
string
ExtendStringInBook(memory_book *M, string S)
{
string Result = {};
if(M->Pen)
{
Result.Length = M->Pen->String.Length + S.Length;
if(M->Pen->String.Base - M->Pen->Page->Base + Result.Length < M->PageSize)
{
M->Pen->Page->Ptr += CopyStringToBarePtr(M->Pen->Page->Ptr, S);
}
else
{
memory_pen_location Pen = *M->Pen;
M->Pen->Page->Ptr = M->Pen->String.Base;
memory_page *Page = GetOrAddPageForString(M, Result);
M->Pen->Page = Page;
M->Pen->String.Base = Page->Ptr;
Page->Ptr += CopyStringToBarePtr(Page->Ptr, Pen.String);
Page->Ptr += CopyStringToBarePtr(Page->Ptr, S);
}
Result.Base = M->Pen->String.Base;
M->Pen->String.Length = Result.Length;
}
else
{
Result = WriteStringInBook(M, S);
}
return Result;
}
void
ResetPen(memory_book *M)
{
if(M->Pen)
{
if(M->Pen->Page)
{
M->Pen->String.Base = M->Pen->Page->Ptr;
}
else
{
M->Pen->Page = AddPage(M);
M->Pen->String.Base = M->Pen->Page->Base;
}
M->Pen->String.Length = 0;
}
}
void
EraseCurrentStringFromBook(memory_book *M)
{
M->Pen->Page->Ptr -= M->Pen->String.Length;
if(M->Pen->Prev)
{
M->Pen->Prev->Next = M->Pen->Next;
}
if(M->Pen->Next)
{
M->Pen->Next->Prev = M->Pen->Prev;
}
memory_pen_location *This = M->Pen;
M->Pen = M->Pen->Prev;
Free(This);
}
void
PrintPage(memory_page *P)
{
fprintf(stderr, "%.*s\n\n", (int)(P->Ptr - P->Base), P->Base);
}
void
PrintBook(memory_book *M)
{
switch(M->Type)
{
case MBT_STRING:
{
for(int i = 0; i < M->PageCount; ++i)
{
PrintPage(&M->Pages[i]);
}
} break;
case MBT_ASSET: Assert(0); break;
case MBT_NONE: Assert(0); break;
}
}
int64_t
StringToInt(string S)
{
int Result = 0;
for(int i = 0; i < S.Length; ++i)
{
Result = Result * 10 + S.Base[i] - '0';
}
return Result;
}
uint64_t
StringLength(char *String)
{
uint64_t i = 0;
if(String)
{
while(String[i])
{
++i;
}
}
return i;
}
int64_t
StringsDiffer(string A, string B)
{
int i = 0;
while(i < A.Length && i < B.Length && A.Base[i] == B.Base[i]) { ++i; }
if(i == A.Length && i == B.Length)
{
return 0;
}
else if(i == A.Length)
{
return -1;
}
else if(i == B.Length)
{
return 1;
}
return A.Base[i] - B.Base[i];
}
int64_t
StringsDifferLv0(string A, char *B)
{
if(B)
{
int i = 0;
while(i < A.Length && B[i] && A.Base[i] == B[i]) { ++i; }
if(i == A.Length && !B[i])
{
return 0;
}
else if(i == A.Length)
{
return -1;
}
return A.Base[i] - B[i];
}
else
{
return A.Length;
}
}
int64_t
StringsDifferS(char *NullTerminated, buffer *NotNullTerminated)
{
char *Ptr = NotNullTerminated->Ptr;
uint64_t Length = StringLength(NullTerminated);
int i = 0;
while(i < Length && Ptr - NotNullTerminated->Location < NotNullTerminated->Size && *NullTerminated == *Ptr)
{
++i, ++NullTerminated, ++Ptr;
}
if(i == Length)
{
return 0;
}
bool WithinBounds = Ptr - NotNullTerminated->Location < NotNullTerminated->Size;
return WithinBounds ? *NullTerminated - *Ptr : *NullTerminated;
}
int64_t
StringsDiffer0(char *A, char *B)
{
//fprintf(stderr, "%c vs %c\n", *A, *B);
while(*A && *B && *A == *B)
{
//fprintf(stderr, "%c vs %c\n", *A, *B);
//fprintf(stderr, "[%d] %c\n", Location, *A);
++A, ++B;
}
return *A - *B;
}
int
StringsDifferCaseInsensitive(string A, string B) // NOTE(matt): Two null-terminated strings
{
int i = 0;
while(i < A.Length && i < B.Length &&
((A.Base[i] >= 'A' && A.Base[i] <= 'Z') ? A.Base[i] + ('a' - 'A') : A.Base[i]) ==
((B.Base[i] >= 'A' && B.Base[i] <= 'Z') ? B.Base[i] + ('a' - 'A') : B.Base[i])
) { ++i; }
if(i == A.Length && i == B.Length)
{
return 0;
}
else if(i == A.Length)
{
return -1;
}
else if(i == B.Length)
{
return 1;
}
return ((A.Base[i] >= 'A' && A.Base[i] <= 'Z') ? A.Base[i] + ('a' - 'A') : A.Base[i]) -
((B.Base[i] >= 'A' && B.Base[i] <= 'Z') ? B.Base[i] + ('a' - 'A') : B.Base[i]);
}
int
StringsDiffer0CaseInsensitive(char *A, char *B) // NOTE(matt): Two null-terminated strings
{
while(*A && *B &&
((*A >= 'A' && *A <= 'Z') ? *A + ('a' - 'A') : *A) ==
((*B >= 'A' && *B <= 'Z') ? *B + ('a' - 'A') : *B))
{
++A, ++B;
}
return ((*A >= 'A' && *A <= 'Z') ? *A + ('a' - 'A') : *A) -
((*B >= 'A' && *B <= 'Z') ? *B + ('a' - 'A') : *B);
}
bool
StringsDifferT(char *A, // NOTE(matt): Null-terminated string
char *B, // NOTE(matt): Not null-terminated string (e.g. one mid-buffer)
char Terminator // NOTE(matt): Caller definable terminator. Pass 0 to only match on the extent of A
)
{
// TODO(matt): Make sure this can't crash upon reaching the end of B's buffer
int ALength = StringLength(A);
int i = 0;
while(i < ALength && A[i] && A[i] == B[i])
{
++i;
}
if((!Terminator && !A[i] && ALength == i) ||
(!A[i] && ALength == i && (B[i] == Terminator)))
{
return FALSE;
}
else
{
return TRUE;
}
}
bool
StringsMatch(string A, string B)
{
int i = 0;
while(i < A.Length && i < B.Length && A.Base[i] == B.Base[i]) { ++i; }
if(i == A.Length && i == B.Length)
{
return TRUE;
}
return FALSE;
}
bool
StringsMatchCaseInsensitive(string A, string B)
{
int i = 0;
while(i < A.Length && i < B.Length &&
((A.Base[i] >= 'A' && A.Base[i] <= 'Z') ? A.Base[i] + ('a' - 'A') : A.Base[i]) ==
((B.Base[i] >= 'A' && B.Base[i] <= 'Z') ? B.Base[i] + ('a' - 'A') : B.Base[i]))
{
++i;
}
if(i == A.Length && i == B.Length)
{
return TRUE;
}
return FALSE;
}
string ExtensionStrings[] =
{
{},
{ ".gif", 4 },
{ ".hmml", 5 },
{ ".index", 6 },
{ ".jpeg", 5 },
{ ".jpg", 4 },
{ ".png", 4 },
};
typedef enum
{
EXT_NULL,
EXT_GIF,
EXT_HMML,
EXT_INDEX,
EXT_JPEG,
EXT_JPG,
EXT_PNG,
} extension_id;
bool
ExtensionMatches(string Path, extension_id Extension) // NOTE(matt): Extension includes the preceding "."
{
bool Result = FALSE;
if(Path.Length >= ExtensionStrings[Extension].Length)
{
string Test = { .Base = Path.Base + Path.Length - ExtensionStrings[Extension].Length, .Length = ExtensionStrings[Extension].Length };
if(StringsMatchCaseInsensitive(Test, ExtensionStrings[Extension]))
{
Result = TRUE;
}
}
return Result;
}
string
Wrap0(char *String)
{
string Result = {};
Result.Base = String;
Result.Length = StringLength(String);
return Result;
}
string
Wrap0i(char *S, uint64_t MaxSize)
{
string Result = {};
Result.Base = S;
Result.Length = MaxSize;
while(Result.Length > 0 && Result.Base[Result.Length - 1] == '\0')
{
--Result.Length;
}
return Result;
}
void
ExtendString0(char **Dest, string Src)
{
if(Src.Length > 0)
{
uint64_t OriginalLength = 0;
if(*Dest) { OriginalLength = StringLength(*Dest); }
uint64_t RequiredBytes = OriginalLength + Src.Length + 1;
*Dest = realloc(*Dest, RequiredBytes);
char *Ptr = *Dest + OriginalLength;
for(int i = 0; i < Src.Length; ++i)
{
*Ptr++ = Src.Base[i];
}
*Ptr = '\0';
}
}
char *ColourStrings[] =
{
"\e[0m",
"\e[0;30m", "\e[0;31m", "\e[0;32m", "\e[0;33m", "\e[0;34m", "\e[0;35m", "\e[0;36m", "\e[0;37m",
"\e[1;30m", "\e[1;31m", "\e[1;32m", "\e[1;33m", "\e[1;34m", "\e[1;35m", "\e[1;36m", "\e[1;37m",
};
typedef enum
{
CS_END,
CS_BLACK, CS_RED, CS_GREEN, CS_YELLOW, CS_BLUE, CS_MAGENTA, CS_CYAN, CS_WHITE,
CS_BLACK_BOLD, CS_RED_BOLD, CS_GREEN_BOLD, CS_YELLOW_BOLD, CS_BLUE_BOLD, CS_MAGENTA_BOLD, CS_CYAN_BOLD, CS_WHITE_BOLD,
} colour_code;
#define CS_SUCCESS CS_GREEN_BOLD
#define CS_ADDITION CS_GREEN_BOLD
#define CS_REINSERTION CS_YELLOW_BOLD
#define CS_FAILURE CS_RED_BOLD
#define CS_ERROR CS_RED_BOLD
#define CS_WARNING CS_YELLOW
#define CS_PRIVATE CS_BLUE
#define CS_ONGOING CS_MAGENTA
#define CS_COMMENT CS_BLACK_BOLD
#define CS_DELETION CS_BLACK_BOLD
void
Colourise(colour_code C)
{
fprintf(stderr, "%s", ColourStrings[C]);
}
void
PrintString(string S)
{
//fprintf(stderr, "PrintString()\n");
fprintf(stderr, "%.*s", (int)S.Length, S.Base);
}
void
PrintStringN(string S)
{
//fprintf(stderr, "PrintString()\n");
fprintf(stderr, "\n%.*s", (int)S.Length, S.Base);
}
void
PrintStringI(string S, uint64_t Indentation)
{
for(int i = 0; i < Indentation; ++i)
{
fprintf(stderr, " ");
}
PrintString(S);
}
void
PrintStringC(colour_code Colour, string String)
{
Colourise(Colour);
PrintString(String);
Colourise(CS_END);
}
void
PrintC(colour_code Colour, char *String)
{
Colourise(Colour);
fprintf(stderr, "%s", String);
Colourise(CS_END);
}
#if DEBUG_PRINT_FUNCTION_NAMES
void
PrintFunctionName(char *N)
{
PrintC(CS_MAGENTA, N);
fprintf(stderr, "\n");
}
void
PrintLinedFunctionName(int LineNumber, char *N)
{
Colourise(CS_BLACK_BOLD);
fprintf(stderr, "[%i] ", LineNumber);
Colourise(CS_END);
PrintFunctionName(N);
}
#else
#define PrintFunctionName(S);
#define PrintLinedFunctionName(L, N);
#endif
typedef enum
{
TOKEN_NULL,
TOKEN_COMMENT_SINGLE,
TOKEN_COMMENT_MULTI_OPEN,
TOKEN_COMMENT_MULTI_CLOSE,
TOKEN_ASSIGN,
TOKEN_SEMICOLON,
TOKEN_OPEN_BRACE,
TOKEN_CLOSE_BRACE,
TOKEN_DOUBLEQUOTE,
TOKEN_NEWLINE,
TOKEN_COMMENT,
TOKEN_STRING, // Double-quoted alphanumeric, may contain spaces
TOKEN_IDENTIFIER, // Alphanumeric
TOKEN_NUMBER, // Numeric
//TOKEN_UNHANDLED, // TODO(matt): Consider the need for this
} token_type;
char *TokenTypeStrings[] =
{
"",
"COMMENT_SINGLE",
"COMMENT_MULTI_OPEN",
"COMMENT_MULTI_CLOSE",
"ASSIGN",
"SEMICOLON",
"OPEN_BRACE",
"CLOSE_BRACE",
"DOUBLEQUOTE",
"NEWLINE",
"COMMENT",
"STRING", // Double-quoted alphanumeric, may contain spaces
"IDENTIFIER", // Alphanumeric
"NUMBER", // Numeric
};
char *TokenStrings[] =
{
"",
"//",
"/*",
"*/",
"=",
";",
"{",
"}",
"\"",
"\n",
"Comment",
"String", // Double-quoted alphanumeric, may contain spaces
"Identifier", // Alphanumeric
"Number", // Numeric
};
#define ArrayCount(A) sizeof(A)/sizeof(*(A))
typedef struct
{
buffer Buffer;
char *Path;
FILE *Handle;
} file;
typedef enum
{
FID_NULL,
FID_METADATA,
} file_id;
typedef struct
{
uint64_t BytePosition;
int64_t Size;
} file_edit;
typedef enum
{
T_db_header_project,
T_db_entry,
T_db_block_assets,
} type_id;
typedef struct
{
type_id ID;
void *Ptr;
uint64_t Byte;
} file_signpost;
typedef struct
{
file_id ID;
file_edit Edit;
file_signpost ProjectsBlock;
file_signpost ProjectParent;
file_signpost ProjectHeader;
file_signpost Prev;
file_signpost This;
file_signpost Next;
file_signpost AssetsBlock;
} file_signposts;
typedef struct
{
file File;
file_signposts Signposts;
} file_signposted;
void
WriteFromByteToPointer(file *F, uint64_t *BytesThroughBuffer, void *Pointer)
{
uint64_t Byte = (char *)Pointer - F->Buffer.Location;
fwrite(F->Buffer.Location + *BytesThroughBuffer, Byte - *BytesThroughBuffer, 1, F->Handle);
*BytesThroughBuffer += Byte - *BytesThroughBuffer;
}
uint64_t
WriteFromPointerToPointer(file *F, void *From, void *To, uint64_t *BytesWritten)
{
uint64_t BytesToWrite = (char *)To - (char *)From;
fwrite(From, BytesToWrite, 1, F->Handle);
*BytesWritten += BytesToWrite;
return BytesToWrite;
}
uint64_t
WriteFromByteToEnd(file *F, uint64_t Byte)
{
fwrite(F->Buffer.Location + Byte, F->Buffer.Size - Byte, 1, F->Handle);
return F->Buffer.Size - Byte;
}
typedef struct
{
char *String;
char *IdentifierDescription;
char *IdentifierDescription_Medium;
char *LocalVariableDescription;
bool IdentifierDescriptionDisplayed;
bool IdentifierDescription_MediumDisplayed;
bool LocalVariableDescriptionDisplayed;
} config_identifier;
config_identifier ConfigIdentifiers[] =
{
{ "" },
{ "allow",
"An include-rule string of the forms: \"identifier\", \"type.member\" or \"type.member.member\", etc. For example:\n\nallow = \"project.default_medium\";\n\nAdding an \"allow\" / \"deny\" rule makes the inclusion prohibitive or permissive, respectively, \
and they cannot be mixed (i.e. only \"allow\" rules, or only \"deny\" rules)."
},
{ "art" },
{ "art_variants" },
{ "assets_root_dir",
"Absolute directory path, from where the locations of CSS, JavaScript and image files are derived (see also css_path, images_path and js_path). Setting this correctly allows Cinera to read in the \
files for the purpose of hashing them."
},
{ "assets_root_url",
"URL from where the locations of CSS, JavaScript and image files are derived (see also css_path, images_path and js_path). Setting this correctly allows HTTP requests for these files to be fulfilled."
},
{ "base_dir", "Absolute directory path, where Cinera will generate the search page (see also search_location) and directories for the player pages (see also player_location)." },
{ "base_url", "URL where the search page (see also search_location) and player pages (see also player_location) will be located." },
{ "cache_dir", "Internal directory (i.e. no access from the wider internet) where we store errors.log and quotes retrieved from insobot." },
{ "cohost",
"The ID of a person (see also person) who cohosts a project. They will then appear in the credits menu of each entry in the project. Note that setting a cohost in the configuration \
file credits this person for the entire project. There is no way to \"uncredit\" people in a HMML file. If a person ought not be credited for the whole project, just add them in \
video node of the entries for which they should be credited."
},
{ "css_path", "Path relative to assets_root_dir and assets_root_url where CSS files are located." },
{ "db_location", "Absolute file path where the database file resides. If you run multiple instances of Cinera on the same machine, please ensure this db_location differs between them." },
{ "default_medium", "The ID of a medium (see also medium) which will be the default for the project. May be overridden by setting the medium in the video node of an HMML file." },
{ "deny", "See allow." },
{ "genre", "This is a setting for the future. We currently only support the \"video\" genre, which is the default." },
{ "global_search_dir", "Absolute directory path, where the global search page will be located, collating the search pages of all projects in the Cinera instance." },
{ "global_search_template", "Path of a HTML template file relative to the global_templates_dir from which the global search page will be generated." },
{ "global_search_url", "URL where the global search page will be located, collating the search pages of all projects in the Cinera instance." },
{ "global_templates_dir", "Absolute directory path, from where the global_search_template path may be derived." },
{ "global_theme", "The theme used for global pages, e.g. search page. As with all themes, its name forms the file path of a CSS file containing the style as follows: cinera__${theme}.css" },
{ "guest",
"The ID of a person (see also person) who guests in a project. They will then appear in the credits menu of each entry in the project. Note that setting a guest in the configuration \
file credits this person for the entire project. There is no way to \"uncredit\" people in a HMML file. If a person ought not be credited for the whole project, just add them in \
video node of the entries for which they should be credited."
},
{ "hidden", "Hidden media are filtered off by default in the player. For example, we may create an \"afk\" medium to tag portions of videos where the host is \"Away from \
Keyboard\". These portions, filtered off, will be skipped to save the viewer sitting through them." },
{ "hmml_dir", "The input directory where Cinera will look for the project's .hmml files." },
{ "homepage", "The URL where visitors will find the person's homepage." },
{ "html_title", "The HTML \"enhanced\" version of title, which may employ HTML tags." },
{ "icon",
"The file path of an image that will appear in the credits menu beside the name of a person who may be supported via this support platform. The image shall be a 32×16 pixel sprite, with the left side \
used when the image is not focused, and the right side when it is focused.",
"A HTML character entity used to denote the medium in the filter menu and list of indices."
},
{ "icon_disabled" },
{ "icon_focused" },
{ "icon_normal" },
{ "icon_type" },
{ "icon_variants" },
{ "ignore_privacy",
"Creators may be able to set the publication status of their content. By default Cinera respects this status by not processing any content that is private. If a creator only publishes \
publicly, then we can set ignore_privacy to \"true\" and save the resources otherwise spent checking the privacy status."},
{ "images_path", "Path relative to assets_root_dir and assets_root_url where image files are located.", 0 },
{ "include", "The path - either absolute or relative to the containing config file - of a file to be included in the configuration." },
{ "indexer",
"The ID of a person (see also person) who indexes a project. They will then appear in the credits menu of each entry in the project. Note that setting an indexer in the configuration \
file credits this person for the entire project. There is no way to \"uncredit\" people in a HMML file. If a person ought not be credited for the whole project, just add them in \
video node of the entries for which they should be credited."
},
{ "js_path", "Path relative to assets_root_dir and assets_root_url where JavaScript files are located." },
{ "lineage", 0, 0, "A slash-separated string of all project IDs from the top of the family tree to the present project" },
{ "lineage_without_origin", 0, 0, "Same as the $lineage, without the first component (the $origin)" },
{ "log_level", "Possible log levels, from least to most verbose: \"emergency\", \"alert\", \"critical\", \"error\", \"warning\", \"notice\", \"informational\", \"debug\""},
{ "medium",
"In HMML an indexer may prefix a word with a colon, to mark it as a category. This category will then appear in the filter menu, for viewers to toggle on / off. A category may be either a topic (by \
default categories are assumed to be topics) or a medium, and both use the same colon prefix. Configuring a medium is our way of stating that a category is indeed a medium, not a topic."
},
{ "name",
"The person's name as it appears in the credits menu.",
"The name of the medium as it appears in the filter menu and in a tooltip when hovering on the medium's icon in an index item."
},
{ "numbering_scheme",
"Possible numbering schemes: \"calendrical\", \"linear\", \"seasonal\". Only \"linear\" (the default) is treated specially. We assume that .hmml file names take the form: \
\"$project$episode_number.hmml\". Under the \"linear\" scheme, Cinera tries to derive each entry's number in its project by skipping past the project ID, then replacing all underscores with full \
stops. This derived number is then used in the search results to denote the entry."
},
{ "origin", 0, 0, "The ID of the project in our branch of the family tree which has no parent" },
{ "owner",
"The ID of the person (see also person) who owns the project. There may only be one owner, and they will appear in the credits menu as the host of each entry.",
0,
"The ID of the project's owner"
},
{ "person",
"This is someone who played a role in the projects, for which they deserve credit (see also: owner, cohost, guest, indexer).",
0,
"The ID of the person within whose scope the variable occurs"
},
{ "player_location", "The location of the project's player pages relative to base_dir and base_url" },
{ "player_template", "Path of a HTML template file relative to the templates_dir from which the project's player pages will be generated." },
{ "privacy_check_interval", "In minutes, this sets how often to check if the privacy status of private entries has changed to \"public\"." },
{ "project",
"The work horse of the whole configuration. A config file lacking project scopes will produce no output. Notably project scopes may themselves contain project scopes.",
0,
"The ID of the project within which scope the variable occurs"
},
{ "query_string", "This string (default \"r\") enables web browsers to cache asset files. We hash those files to produce a number, which we then write to HTML files in hexadecimal format, e.g. \
?r=a59bb130. Hashing may be disabled by setting query_string = \"\";"
},
{ "search_location", "The location of the project's search page relative to base_dir and base_url" },
{ "search_template", "Path of a HTML template file relative to the templates_dir from which the project's search page will be generated." },
{ "single_browser_tab", "Setting this to \"true\" (default \"false\") makes the search page open player pages in its own tab." },
{ "stream_platform", "This is a setting for the future. We currently only support \"twitch\" but not in any meaningful way." },
{ "stream_username",
"We use this username to retrieve quotes from insobot. If it is not set, we use the host's ID when contacting insobot. The purpose of this setting is to let us identify project owners in one way, \
perhaps to automatically construct paths, while recognising that same person when they stream under a different username."
},
{ "support",
"Information detailing where a person may be supported, to be cited in the credits menu.",
0,
"The ID of the support platform within which scope the variable occurs"
},
{ "templates_dir", "Absolute directory path, from where the player_template and search_template path may be derived." },
{ "theme", "The theme used to style all the project's pages. As with all themes, its name forms the file path of a CSS file containing the style as follows: cinera__${theme}.css" },
{ "title", "The full name of the project" },
{ "title_list_delimiter", "Currently not implemented, probably to be removed." },
{ "title_list_end", "Currently not implemented, probably to be removed." },
{ "title_list_prefix", "Currently not implemented, probably to be removed." },
{ "title_list_suffix", "Currently not implemented, probably to be removed."} ,
{ "title_suffix", "Currently not implemented, probably to be removed." },
{ "unit", "This works in conjunction with the numbering_scheme. It is a freely-configurable string - e.g. \"Day\", \"Session\", \"Episode\", \"Meeting\" - which is written on the search page, \
preceding the derived number of each entry. If the unit is not set, then the entries will not be numbered." },
{ "vod_platform", "This is a setting more for the future. We currently only support \"youtube\" for the purposes of generating the player page. But an additional use of the vod_platform is as \
a template tag (see also Templating)." },
{ "url", "The URL where viewers may support the person, e.g. their page on a crowd funding site, the \"pledge\" page on their own website." },
};
typedef enum
{
IDENT_NULL,
IDENT_ALLOW,
IDENT_ART,
IDENT_ART_VARIANTS,
IDENT_ASSETS_ROOT_DIR,
IDENT_ASSETS_ROOT_URL,
IDENT_BASE_DIR,
IDENT_BASE_URL,
IDENT_CACHE_DIR,
IDENT_COHOST,
IDENT_CSS_PATH,
IDENT_DB_LOCATION,
IDENT_DEFAULT_MEDIUM,
IDENT_DENY,
IDENT_GENRE,
IDENT_GLOBAL_SEARCH_DIR,
IDENT_GLOBAL_SEARCH_TEMPLATE,
IDENT_GLOBAL_SEARCH_URL,
IDENT_GLOBAL_TEMPLATES_DIR,
IDENT_GLOBAL_THEME,
IDENT_GUEST,
IDENT_HIDDEN,
IDENT_HMML_DIR,
IDENT_HOMEPAGE,
IDENT_HTML_TITLE,
IDENT_ICON,
IDENT_ICON_DISABLED,
IDENT_ICON_FOCUSED,
IDENT_ICON_NORMAL,
IDENT_ICON_TYPE,
IDENT_ICON_VARIANTS,
IDENT_IGNORE_PRIVACY,
IDENT_IMAGES_PATH,
IDENT_INCLUDE,
IDENT_INDEXER,
IDENT_JS_PATH,
IDENT_LINEAGE,
IDENT_LINEAGE_WITHOUT_ORIGIN,
IDENT_LOG_LEVEL,
IDENT_MEDIUM,
IDENT_NAME,
IDENT_NUMBERING_SCHEME,
IDENT_ORIGIN,
IDENT_OWNER,
IDENT_PERSON,
IDENT_PLAYER_LOCATION,
IDENT_PLAYER_TEMPLATE,
IDENT_PRIVACY_CHECK_INTERVAL,
IDENT_PROJECT,
IDENT_QUERY_STRING,
IDENT_SEARCH_LOCATION,
IDENT_SEARCH_TEMPLATE,
IDENT_SINGLE_BROWSER_TAB,
IDENT_STREAM_PLATFORM,
IDENT_STREAM_USERNAME,
IDENT_SUPPORT,
IDENT_TEMPLATES_DIR,
IDENT_THEME,
IDENT_TITLE,
IDENT_TITLE_LIST_DELIMITER,
IDENT_TITLE_LIST_END,
IDENT_TITLE_LIST_PREFIX,
IDENT_TITLE_LIST_SUFFIX,
IDENT_TITLE_SUFFIX,
IDENT_UNIT,
IDENT_VOD_PLATFORM,
IDENT_URL,
IDENT_COUNT,
} config_identifier_id;
void
Indent(uint64_t Indent)
{
for(int i = 0; i < INDENT_WIDTH * Indent; ++i)
{
fprintf(stderr, " ");
}
}
void
IndentedCarriageReturn(int IndentationLevel)
{
fprintf(stderr, "\n");
Indent(IndentationLevel);
}
void
NewParagraph(int IndentationLevel)
{
fprintf(stderr, "\n");
IndentedCarriageReturn(IndentationLevel);
}
void
NewSection(char *Title, int *IndentationLevel)
{
IndentedCarriageReturn(*IndentationLevel);
if(Title)
{
fprintf(stderr, "%s:", Title);
}
++*IndentationLevel;
IndentedCarriageReturn(*IndentationLevel);
}
void
EndSection(int *IndentationLevel)
{
--*IndentationLevel;
}
int
GetTerminalColumns(void)
{
struct winsize TermDim;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &TermDim);
return TermDim.ws_col;
}
void
ClearTerminal(void)
{
fprintf(stderr, "\033[2J");
}
void
TypesetString(int CurrentColumn, string String)
{
if(String.Length > 0)
{
int TermCols = GetTerminalColumns();
int AvailableCharacters = TermCols - CurrentColumn;
int CharactersToWrite = String.Length;
string Pen = { .Base = String.Base + String.Length - CharactersToWrite,
.Length = MIN(String.Length, AvailableCharacters) };
int Length = Pen.Length;
while(Length > 0 && Length < String.Length - (String.Length - CharactersToWrite) && Pen.Base[Length] != ' ')
{
--Length;
}
bool SplitAtWhitespace = FALSE;
if(Length > 0)
{
if(Length < String.Length - (String.Length - CharactersToWrite) && Pen.Base[Length] == ' ')
{
SplitAtWhitespace = TRUE;
}
Pen.Length = Length;
}
for(int i = 0; i < Pen.Length; ++i)
{
if(Pen.Base[i] == '\n')
{
Pen.Length = i;
SplitAtWhitespace = TRUE;
break;
}
}
PrintString(Pen);
CharactersToWrite -= Pen.Length;
if(SplitAtWhitespace)
{
--CharactersToWrite;
}
while(CharactersToWrite > 0)
{
fprintf(stderr, "\n");
for(int i = 0; i < CurrentColumn; ++i)
{
fprintf(stderr, " ");
}
string Pen = { .Base = String.Base + String.Length - CharactersToWrite,
.Length = MIN(CharactersToWrite, AvailableCharacters) };
Length = Pen.Length;
while(Length > 0 && Length < String.Length - (String.Length - CharactersToWrite) && Pen.Base[Length] != ' ')
{
--Length;
}
SplitAtWhitespace = FALSE;
if(Length > 0)
{
if(Length < String.Length - (String.Length - CharactersToWrite) && Pen.Base[Length] == ' ')
{
SplitAtWhitespace = TRUE;
}
Pen.Length = Length;
}
for(int i = 0; i < Pen.Length; ++i)
{
if(Pen.Base[i] == '\n')
{
Pen.Length = i;
SplitAtWhitespace = TRUE;
break;
}
}
PrintString(Pen);
CharactersToWrite -= Pen.Length;
if(SplitAtWhitespace)
{
--CharactersToWrite;
}
}
}
}
typedef struct
{
token_type Type;
string Content;
int64_t int64_t;
uint64_t LineNumber;
} token;
typedef struct
{
file File;
uint64_t Count;
token *Token;
uint64_t CurrentIndex;
uint64_t CurrentLine;
} tokens;
typedef struct
{
tokens *Tokens;
uint64_t Count;
} tokens_list;
char *ArtVariantStrings[] =
{
"light_normal",
"light_focused",
"light_disabled",
"dark_normal",
"dark_focused",
"dark_disabled",
};
typedef enum
{
AVS_LIGHT_NORMAL,
AVS_LIGHT_FOCUSED,
AVS_LIGHT_DISABLED,
AVS_DARK_NORMAL,
AVS_DARK_FOCUSED,
AVS_DARK_DISABLED,
AVS_COUNT,
} art_variant_shifters;
typedef struct
{
char *String;
uint64_t Mapping:63;
uint64_t Unused:1;
} config_art_variant;
config_art_variant ConfigArtVariants[] =
{
{ },
{ "light_normal", 1 << AVS_LIGHT_NORMAL },
{ "light_focused", 1 << AVS_LIGHT_FOCUSED },
{ "light_disabled", 1 << AVS_LIGHT_DISABLED },
{ "light", ((1 << AVS_LIGHT_NORMAL) | (1 << AVS_LIGHT_FOCUSED) | (1 << AVS_LIGHT_DISABLED)) },
{ "dark_normal", 1 << AVS_DARK_NORMAL },
{ "dark_focused", 1 << AVS_DARK_FOCUSED },
{ "dark_disabled", 1 << AVS_DARK_DISABLED },
{ "dark", ((1 << AVS_DARK_NORMAL) | (1 << AVS_DARK_FOCUSED) | (1 << AVS_DARK_DISABLED)) },
{ "normal", ((1 << AVS_LIGHT_NORMAL) | (1 << AVS_DARK_NORMAL)) },
{ "focused", ((1 << AVS_LIGHT_FOCUSED) | (1 << AVS_DARK_FOCUSED)) },
{ "disabled", ((1 << AVS_LIGHT_NORMAL) | (1 << AVS_DARK_NORMAL)) },
{ "all", ((1 << AVS_LIGHT_NORMAL) | (1 << AVS_LIGHT_FOCUSED) | (1 << AVS_LIGHT_DISABLED) | (1 << AVS_DARK_NORMAL) | (1 << AVS_DARK_FOCUSED) | (1 << AVS_DARK_DISABLED)) },
};
typedef enum
{
CAV_DEFAULT_UNSET,
CAV_LIGHT_NORMAL,
CAV_LIGHT_FOCUSED,
CAV_LIGHT_DISABLED,
CAV_LIGHT,
CAV_DARK_NORMAL,
CAV_DARK_FOCUSED,
CAV_DARK_DISABLED,
CAV_DARK,
CAV_NORMAL,
CAV_FOCUSED,
CAV_DISABLED,
CAV_ALL,
CAV_COUNT,
} config_art_variant_id;
typedef enum
{
S_ERROR,
S_WARNING,
} severity;
void
ConfigErrorFilenameAndLineNumber(char *Filename, uint64_t LineNumber, severity Severity)
{
fprintf(stderr, "\n"
"┌─ ");
switch(Severity)
{
case S_ERROR: PrintC(CS_ERROR, "Config error"); break;
case S_WARNING: PrintC(CS_WARNING, "Config warning"); break;
}
if(Filename)
{
fprintf(stderr, " on line ");
Colourise(CS_BLUE_BOLD);
fprintf(stderr, "%lu", LineNumber);
Colourise(CS_END);
fprintf(stderr, " of ");
PrintC(CS_CYAN, Filename);
}
fprintf(stderr, "\n"
"└─╼ ");
}
// TODO(matt): Get more errors going through Error()
void
ConfigError(char *Filename, uint64_t LineNumber, severity Severity, char *Message, string *Received)
{
ConfigErrorFilenameAndLineNumber(Filename, LineNumber, Severity);
fprintf(stderr,
"%s", Message);
if(Received)
{
PrintStringC(CS_MAGENTA_BOLD, *Received);
}
fprintf(stderr, "\n");
}
void
ConfigErrorUnset(config_identifier_id FieldID)
{
ConfigErrorFilenameAndLineNumber(0, 0, S_ERROR);
fprintf(stderr,
"Unset %s\n", ConfigIdentifiers[FieldID].String);
}
void
ConfigFileIncludeError(char *Filename, uint64_t LineNumber, string Path)
{
ConfigErrorFilenameAndLineNumber(Filename, LineNumber, S_WARNING);
fprintf(stderr,
"Included file could not be opened (%s): ", strerror(errno));
PrintStringC(CS_MAGENTA_BOLD, Path);
fprintf(stderr, "\n");
}
void
ConfigErrorSizing(char *Filename, uint64_t LineNumber, config_identifier_id FieldID, string *Received, uint64_t MaxSize)
{
ConfigErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR);
fprintf(stderr, "%s value is too long (%lu/%lu characters): ", ConfigIdentifiers[FieldID].String, Received->Length, MaxSize);
PrintStringC(CS_MAGENTA_BOLD, *Received);
fprintf(stderr, "\n");
}
void
ConfigErrorInt(char *Filename, uint64_t LineNumber, severity Severity, char *Message, uint64_t Number)
{
ConfigErrorFilenameAndLineNumber(Filename, LineNumber, Severity);
fprintf(stderr,
"%s%s%lu%s\n", Message, ColourStrings[CS_BLUE_BOLD], Number, ColourStrings[CS_END]);
}
void
ConfigErrorExpectation(tokens *T, token_type GreaterExpectation, token_type LesserExpectation)
{
ConfigErrorFilenameAndLineNumber(T->File.Path, T->Token[T->CurrentIndex].LineNumber, S_ERROR);
fprintf(stderr,
"Syntax error: Received ");
if(T->Token[T->CurrentIndex].Content.Base)
{
PrintStringC(CS_RED, T->Token[T->CurrentIndex].Content);
}
else
{
fprintf(stderr, "%s%ld%s", ColourStrings[CS_BLUE_BOLD], T->Token[T->CurrentIndex].int64_t, ColourStrings[CS_END]);
}
fprintf(stderr,
" but Expected ");
PrintStringC(CS_GREEN, Wrap0(TokenStrings[GreaterExpectation]));
if(LesserExpectation)
{
fprintf(stderr,
" or ");
PrintStringC(CS_GREEN, Wrap0(TokenStrings[LesserExpectation]));
}
fprintf(stderr,
"\n");
}
typedef 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_COUNT,
} log_level;
char *LogLevelStrings[] =
{
"emergency",
"alert",
"critical",
"error",
"warning",
"notice",
"informational",
"debug",
};
log_level
GetLogLevelFromString(char *Filename, token *T)
{
for(int i = 0; i < LOG_COUNT; ++i)
{
if(!StringsDifferLv0(T->Content, LogLevelStrings[i])) { return i; }
}
ConfigError(Filename, T->LineNumber, S_ERROR, "Unknown log level: ", &T->Content);
fprintf(stderr, " Valid log levels:\n");
for(int i = 0; i < LOG_COUNT; ++i)
{
fprintf(stderr, " %s\n", LogLevelStrings[i]);
}
return LOG_COUNT;
}
void
FreeBuffer(buffer *Buffer)
{
Free(Buffer->Location);
Buffer->Ptr = 0;
Buffer->Size = 0;
//Buffer->ID = 0;
Buffer->IndentLevel = 0;
}
char *AssetTypeNames[] =
{
"Generic",
"CSS",
"Image",
"JavaScript"
};
typedef enum
{
ASSET_GENERIC,
ASSET_CSS,
ASSET_IMG,
ASSET_JS,
ASSET_TYPE_COUNT
} asset_type;
char *GenreStrings[] =
{
"video",
};
typedef enum
{
GENRE_VIDEO,
GENRE_COUNT,
} genre;
genre
GetGenreFromString(char *Filename, token *T)
{
for(int i = 0; i < GENRE_COUNT; ++i)
{
if(!StringsDifferLv0(T->Content, GenreStrings[i])) { return i; }
}
ConfigError(Filename, T->LineNumber, S_ERROR, "Unknown genre: ", &T->Content);
fprintf(stderr, " Valid genres:\n");
for(int i = 0; i < GENRE_COUNT; ++i)
{
fprintf(stderr, " %s\n", GenreStrings[i]);
}
return GENRE_COUNT;
}
char *NumberingSchemeStrings[] =
{
"calendrical",
"linear",
"seasonal",
};
typedef enum
{
NS_CALENDRICAL,
NS_LINEAR,
NS_SEASONAL,
NS_COUNT,
} numbering_scheme;
numbering_scheme
GetNumberingSchemeFromString(char *Filename, token *T)
{
for(int i = 0; i < NS_COUNT; ++i)
{
if(!StringsDifferLv0(T->Content, NumberingSchemeStrings[i])) { return i; }
}
ConfigError(Filename, T->LineNumber, S_ERROR, "Unknown numbering scheme: ", &T->Content);
fprintf(stderr, " Valid numbering schemes:\n");
for(int i = 0; i < NS_COUNT; ++i)
{
fprintf(stderr, " %s\n", NumberingSchemeStrings[i]);
}
return NS_COUNT;
}
bool
GetBoolFromString(char *Filename, token *T)
{
if(!StringsDifferLv0(T->Content, "true") ||
!StringsDifferLv0(T->Content, "True") ||
!StringsDifferLv0(T->Content, "TRUE") ||
!StringsDifferLv0(T->Content, "yes"))
{
return TRUE;
}
else if(!StringsDifferLv0(T->Content, "false") ||
!StringsDifferLv0(T->Content, "False") ||
!StringsDifferLv0(T->Content, "FALSE") ||
!StringsDifferLv0(T->Content, "no"))
{
return FALSE;
}
else
{
ConfigError(Filename, T->LineNumber, S_ERROR, "Unknown boolean: ", &T->Content);
fprintf(stderr, " Valid booleans:\n");
PrintC(CS_GREEN, " true\n");
PrintC(CS_GREEN, " True\n");
PrintC(CS_GREEN, " TRUE\n");
PrintC(CS_GREEN, " yes\n");
PrintC(CS_RED, " false\n");
PrintC(CS_RED, " False\n");
PrintC(CS_RED, " FALSE\n");
PrintC(CS_RED, " no\n");
return -1;
}
}
/*
// NOTE(matt): Sprite stuff
art = "riscy_sprite.png";
art_variants = "light_normal light_focused light_disabled";
art_variants = "light";
art_variants = "dark_normal dark_focused dark_disabled";
art_variants = "dark";
art_variants = "light_normal dark_normal";
art_variants = "normal";
art_variants = "all";
//og_image = "*.png";
icon = "&#61450;"; // all
icon_type = "textual"; // or "graphical"
icon_variants = "all"; // for "graphical" icons, same as art_variants
icon_normal = "&#61450;"; // fills the whole normal row
icon_focused = "&#61450;"; // fills the whole focused row
icon_disabled = "&#61450;"; // fills the whole disabled row
// NOTE(matt): In the future, allow setting art and graphical icons in this manner, for Cinera itself to compose the sprite
art_light_normal = "*.png";
art_light_focused = "*.png";
art_light_disabled = "*.png";
art_dark_normal = "*.png";
art_dark_focused = "*.png";
art_dark_disabled = "*.png";
icon_light_normal = "*.png";
icon_light_focused = "*.png";
icon_light_disabled = "*.png";
icon_dark_normal = "*.png";
icon_dark_focused = "*.png";
icon_dark_disabled = "*.png";
//
// NOTE(matt): icon, with icon_type ("graphical" or "textual")
// projects, media and support
// if "graphical", one file with icon_variants
// if "textual", one or many texts: icon_normal, icon_focused, icon_disabled
// art, may only be graphical
// projects and entries
*/
uint64_t
GetArtVariantFromString(string S)
{
uint64_t Result = 0;
for(int i = 0; i < CAV_COUNT; ++i)
{
config_art_variant *This = ConfigArtVariants + i;
if(StringsMatch(S, Wrap0(This->String)))
{
Result = This->Mapping;
}
}
return Result;
}
uint64_t
SkipWhitespaceS(string *S, uint64_t Position)
{
while(Position < S->Length && S->Base[Position] &&
(S->Base[Position] == ' ' ||
S->Base[Position] == '\n' ||
S->Base[Position] == '\t'))
{
++Position;
}
return Position;
}
bool
IsValidIdentifierCharacter(char C)
{
return ((C >= 'a' && C <= 'z') || (C >= 'A' && C <= 'Z') || C == '_' || C == '-');
}
bool
IsNumber(char C)
{
return (C >= '0' && C <= '9');
}
int64_t
ParseArtVariantsString(char *Filename, token *ArtVariantsString)
{
uint64_t Result = 0;
bool Valid = TRUE;
int i = SkipWhitespaceS(&ArtVariantsString->Content, 0);
for(; i < ArtVariantsString->Content.Length; i = SkipWhitespaceS(&ArtVariantsString->Content, i))
{
if(IsValidIdentifierCharacter(ArtVariantsString->Content.Base[i]))
{
string S = {};
S.Base = ArtVariantsString->Content.Base + i;
for(; i < ArtVariantsString->Content.Length && IsValidIdentifierCharacter(ArtVariantsString->Content.Base[i]); ++i)
{
++S.Length;
}
uint64_t Variant = GetArtVariantFromString(S);
if(Variant != 0)
{
Result |= Variant;
}
else
{
ConfigError(Filename, ArtVariantsString->LineNumber, S_WARNING, "Unknown art_variant: ", &S);
Valid = FALSE;
}
}
}
if(!Valid)
{
fprintf(stderr, " Valid variant_types:\n");
int FirstValidVariant = 1;
for(int i = FirstValidVariant; i < CAV_COUNT; ++i)
{
fprintf(stderr, " %s\n", ConfigArtVariants[i].String);
}
Result = -1;
}
return Result;
}
char *IconTypeStrings[] =
{
0,
"graphical",
"textual",
};
typedef enum
{
IT_DEFAULT_UNSET,
IT_GRAPHICAL,
IT_TEXTUAL,
IT_COUNT,
} icon_type;
icon_type
GetIconTypeFromString(char *Filename, token *T)
{
for(int i = 0; i < IT_COUNT; ++i)
{
if(!StringsDifferLv0(T->Content, IconTypeStrings[i])) { return i; }
}
ConfigError(Filename, T->LineNumber, S_ERROR, "Unknown icon_type: ", &T->Content);
fprintf(stderr, " Valid icon_types:\n");
int FirstValidIconType = 1;
for(int i = FirstValidIconType; i < IT_COUNT; ++i)
{
fprintf(stderr, " %s\n", IconTypeStrings[i]);
}
return NS_COUNT;
}
// DBVersion 1
typedef struct { unsigned int DBVersion; version AppVersion; version HMMLVersion; unsigned int EntryCount; } db_header1;
typedef struct { int Size; char BaseFilename[32]; } db_entry1;
typedef struct { file File; file Metadata; db_header1 Header; db_entry1 Entry; } database1;
//
// DBVersion 2
typedef struct { unsigned int DBVersion; version AppVersion; version HMMLVersion; unsigned int EntryCount; char SearchLocation[32]; char PlayerLocation[32]; } db_header2;
typedef db_entry1 db_entry2;
typedef struct { file File; file Metadata; db_header2 Header; db_entry2 Entry; } database2;
//
#define MAX_PLAYER_URL_PREFIX_LENGTH 16
// TODO(matt): Increment CINERA_DB_VERSION!
typedef struct
{
unsigned int CurrentDBVersion;
version CurrentAppVersion;
version CurrentHMMLVersion;
unsigned int InitialDBVersion;
version InitialAppVersion;
version InitialHMMLVersion;
unsigned short int EntryCount;
char ProjectID[MAX_PROJECT_ID_LENGTH + 1];
char ProjectName[MAX_PROJECT_NAME_LENGTH + 1];
char BaseURL[MAX_BASE_URL_LENGTH + 1];
char SearchLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1];
char PlayerLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1];
char PlayerURLPrefix[MAX_PLAYER_URL_PREFIX_LENGTH + 1];
} db_header3;
typedef struct
{
unsigned int PrevStart, NextStart;
unsigned short int PrevEnd, NextEnd;
} link_insertion_offsets; // NOTE(matt): PrevStart is Absolute (or relative to start of file), the others are Relative to PrevStart
typedef struct
{
link_insertion_offsets LinkOffsets;
unsigned short int Size;
char BaseFilename[MAX_BASE_FILENAME_LENGTH + 1];
char Title[MAX_TITLE_LENGTH + 1];
} db_entry3;
typedef struct
{
file File;
file Metadata;
db_header3 Header;
db_entry3 Entry;
} database3;
#pragma pack(push, 1)
typedef struct
{
uint32_t HexSignature; // 'CNRA'
uint32_t CurrentDBVersion;
version CurrentAppVersion;
version CurrentHMMLVersion;
uint32_t InitialDBVersion;
version InitialAppVersion;
version InitialHMMLVersion;
uint32_t BlockCount;
} db_header4;
typedef struct
{
uint32_t BlockID; // 'NTRY'
uint16_t Count;
char ProjectID[MAX_PROJECT_ID_LENGTH];
char ProjectName[MAX_PROJECT_NAME_LENGTH];
char BaseURL[MAX_BASE_URL_LENGTH];
char SearchLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH];
char PlayerLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH];
char PlayerURLPrefix[MAX_PLAYER_URL_PREFIX_LENGTH]; // TODO(matt): Replace this with the OutputPath, when we add that
} db_header_entries4;
typedef db_entry3 db_entry4;
typedef struct
{
uint32_t BlockID; // 'ASET'
uint16_t Count;
char RootDir[MAX_ROOT_DIR_LENGTH];
char RootURL[MAX_ROOT_URL_LENGTH];
char CSSDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH];
char ImagesDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH];
char JSDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH];
} db_header_assets4;
typedef struct
{
int32_t Hash;
uint32_t LandmarkCount;
enum8(asset_type) Type;
char Filename[MAX_ASSET_FILENAME_LENGTH];
} db_asset4;
typedef struct
{
int32_t EntryIndex;
uint32_t Position;
} db_landmark4;
typedef struct
{
file File;
file Metadata;
db_header4 Header;
db_header_entries4 EntriesHeader;
db_entry4 Entry;
db_header_assets4 AssetsHeader;
db_asset4 Asset;
db_landmark4 Landmark;
} database4;
// Database 5
typedef db_header4 db_header5;
typedef struct
{
uint32_t BlockID; // 'PROJ' // NOTE(matt): This corresponds to the old NTRY block
char GlobalSearchDir[MAX_BASE_DIR_LENGTH];
char GlobalSearchURL[MAX_BASE_URL_LENGTH];
uint64_t Count;
char Reserved[244];
} db_block_projects5;
char *SpecialAssetIndexStrings[] =
{
"SAI_TEXTUAL",
"SAI_UNSET",
};
typedef enum
{
SAI_TEXTUAL = -2,
SAI_UNSET = -1,
} special_asset_index;
typedef struct
{
char ID[MAX_PROJECT_ID_LENGTH];
char Title[MAX_PROJECT_NAME_LENGTH];
char BaseDir[MAX_BASE_DIR_LENGTH];
char BaseURL[MAX_BASE_URL_LENGTH];
char SearchLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH];
char PlayerLocation[MAX_RELATIVE_PAGE_LOCATION_LENGTH];
char Theme[MAX_THEME_LENGTH];
char Unit[MAX_UNIT_LENGTH];
enum32(special_asset_index) ArtIndex;
enum32(special_asset_index) IconIndex;
uint64_t EntryCount;
uint64_t ChildCount;
char Reserved[24];
} db_header_project5;
typedef struct
{
char HMMLBaseFilename[MAX_BASE_FILENAME_LENGTH];
char OutputLocation[MAX_ENTRY_OUTPUT_LENGTH];
char Title[MAX_TITLE_LENGTH];
unsigned short int Size;
link_insertion_offsets LinkOffsets;
enum32(special_asset_index) ArtIndex;
char Reserved[46];
} db_entry5;
typedef struct db_block_assets5
{
uint32_t BlockID; // 'ASET'
uint16_t Count;
char RootDir[MAX_ROOT_DIR_LENGTH];
char RootURL[MAX_ROOT_URL_LENGTH];
char CSSDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH];
char ImagesDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH];
char JSDir[MAX_RELATIVE_ASSET_LOCATION_LENGTH];
char Reserved[154];
} db_block_assets5;
typedef struct
{
char Filename[MAX_ASSET_FILENAME_LENGTH];
enum8(asset_type) Type;
int32_t Hash;
uint32_t LandmarkCount;
uint64_t Associated:1;
uint64_t Variants:63;
uint32_t Width;
uint32_t Height;
char Reserved[39];
} db_asset5;
// TODO(matt): Consider the required sizes of these people
typedef struct
{
int32_t Generation;
int32_t Index;
} db_project_index5;
typedef struct
{
db_project_index5 Project;
int32_t EntryIndex;
uint32_t Position;
} db_landmark5;
typedef struct
{
file File;
file_signposted Metadata;
db_header5 Header;
db_block_projects5 ProjectsBlock;
db_header_project5 ProjectHeader;
db_entry5 Entry;
db_block_assets5 AssetsBlock;
db_asset5 Asset;
db_landmark5 Landmark;
} database5;
//
// //
#pragma pack(pop)
#define CINERA_DB_VERSION 5
#define db_header db_header5
#define db_block_projects db_block_projects5
#define db_header_project db_header_project5
#define db_entry db_entry5
#define db_block_assets db_block_assets5
#define db_asset db_asset5
#define db_project_index db_project_index5
#define db_landmark db_landmark5
#define database database5
// TODO(matt): Increment CINERA_DB_VERSION!
typedef struct
{
buffer_id BufferID;
uint32_t Offset;
} landmark;
typedef struct
{
db_project_index ProjectIndex;
config_identifier_id Type; // IDENT_ART or IDENT_ICON
} asset_association;
typedef struct
{
uint32_t Width;
uint32_t Height;
} vec2;
typedef struct
{
vec2 TileDim;
int32_t XLight;
int32_t XDark;
int32_t YNormal;
int32_t YFocused;
int32_t YDisabled;
} sprite;
typedef struct asset
{
asset_type Type;
char Filename[MAX_ASSET_FILENAME_LENGTH];
int32_t Hash;
vec2 Dimensions;
sprite Sprite;
uint64_t Variants:63;
uint64_t Associated:1;
int32_t FilenameAt:29;
int32_t Known:1;
int32_t OffsetLandmarks:1;
int32_t DeferredUpdate:1;
uint32_t SearchLandmarkCount;
landmark *Search;
uint32_t PlayerLandmarkCount;
landmark *Player;
} asset;
asset BuiltinAssets[] =
{
{ ASSET_CSS, "cinera.css" },
{ ASSET_CSS, "cinera_topics.css" },
{ ASSET_IMG, "cinera_icon_filter.png" },
{ ASSET_JS, "cinera_pre.js" },
{ ASSET_JS, "cinera_post.js" },
{ ASSET_JS, "cinera_search.js" },
{ ASSET_JS, "cinera_player_pre.js" },
{ ASSET_JS, "cinera_player_post.js" },
};
typedef enum
{
ASSET_CSS_CINERA,
ASSET_CSS_TOPICS,
ASSET_IMG_FILTER,
ASSET_JS_CINERA_PRE,
ASSET_JS_CINERA_POST,
ASSET_JS_SEARCH,
ASSET_JS_PLAYER_PRE,
ASSET_JS_PLAYER_POST,
BUILTIN_ASSETS_COUNT,
} builtin_asset_id;
typedef struct
{
int Count;
memory_book Asset;
} assets;
typedef enum
{
// Contents and Player Pages Mandatory
TAG_INCLUDES,
// Contents Page Mandatory
TAG_SEARCH,
// Player Page Mandatory
TAG_PLAYER,
// Player Page Optional
TAG_CUSTOM0,
TAG_CUSTOM1,
TAG_CUSTOM2,
TAG_CUSTOM3,
TAG_CUSTOM4,
TAG_CUSTOM5,
TAG_CUSTOM6,
TAG_CUSTOM7,
TAG_CUSTOM8,
TAG_CUSTOM9,
TAG_CUSTOM10,
TAG_CUSTOM11,
TAG_CUSTOM12,
TAG_CUSTOM13,
TAG_CUSTOM14,
TAG_CUSTOM15,
TAG_TITLE,
TAG_VIDEO_ID,
TAG_VOD_PLATFORM,
// Anywhere Optional
TAG_ASSET,
TAG_CSS,
TAG_IMAGE,
TAG_JS,
TAG_NAV,
TAG_PROJECT,
TAG_PROJECT_ID,
TAG_PROJECT_PLAIN,
TAG_SEARCH_URL,
TAG_THEME,
TAG_URL,
TEMPLATE_TAG_COUNT,
} template_tag_code;
char *TemplateTags[] = {
"__CINERA_INCLUDES__",
"__CINERA_SEARCH__",
"__CINERA_PLAYER__",
"__CINERA_CUSTOM0__",
"__CINERA_CUSTOM1__",
"__CINERA_CUSTOM2__",
"__CINERA_CUSTOM3__",
"__CINERA_CUSTOM4__",
"__CINERA_CUSTOM5__",
"__CINERA_CUSTOM6__",
"__CINERA_CUSTOM7__",
"__CINERA_CUSTOM8__",
"__CINERA_CUSTOM9__",
"__CINERA_CUSTOM10__",
"__CINERA_CUSTOM11__",
"__CINERA_CUSTOM12__",
"__CINERA_CUSTOM13__",
"__CINERA_CUSTOM14__",
"__CINERA_CUSTOM15__",
"__CINERA_TITLE__",
"__CINERA_VIDEO_ID__",
"__CINERA_VOD_PLATFORM__",
"__CINERA_ASSET__",
"__CINERA_CSS__",
"__CINERA_IMAGE__",
"__CINERA_JS__",
"__CINERA_NAV__",
"__CINERA_PROJECT__",
"__CINERA_PROJECT_ID__",
"__CINERA_PROJECT_PLAIN__",
"__CINERA_SEARCH_URL__",
"__CINERA_THEME__",
"__CINERA_URL__",
};
char *NavigationTypes[] =
{
0,
"dropdown",
"horizontal",
"plain",
};
typedef enum
{
NT_NULL,
NT_DROPDOWN,
NT_HORIZONTAL,
NT_PLAIN,
NT_COUNT,
} navigation_type;
typedef struct
{
int Offset;
asset *Asset;
navigation_type NavigationType;
enum8(template_tag_codes) TagCode;
} tag_offset;
typedef enum
{
TEMPLATE_GLOBAL_SEARCH,
TEMPLATE_SEARCH,
TEMPLATE_PLAYER,
TEMPLATE_BESPOKE
} template_type;
typedef struct
{
int Validity; // NOTE(matt): Bitmask describing which page the template is valid for, i.e. contents and / or player page
template_type Type;
bool RequiresCineraJS;
uint64_t TagCount;
tag_offset *Tags;
} template_metadata;
typedef struct
{
file File;
template_metadata Metadata;
} template;
void
ClearTemplateMetadata(template *Template)
{
Template->Metadata.TagCount = 0;
Template->Metadata.Validity = 0;
}
void
FreeFile(file *F)
{
FreeBuffer(&F->Buffer);
Free(F->Path);
F->Handle = 0;
}
void
FreeSignpostedFile(file_signposted *F)
{
FreeFile(&F->File);
file_signposts Zero = {};
F->Signposts = Zero;
}
void
FreeTemplate(template *Template)
{
FreeFile(&Template->File);
FreeAndResetCount(Template->Metadata.Tags, Template->Metadata.TagCount);
ClearTemplateMetadata(Template);
}
string
StripComponentFromPath(string Path)
{
string Result = Path;
while(Result.Length > 0 && Result.Base[Result.Length - 1] != '/')
{
--Result.Length;
}
if(Result.Length > 1)
{
--Result.Length;
}
return Result;
}
int
StripComponentFromPath0(char *Path)
{
// TODO(matt): Beware that this may leak, perhaps? I dunno.
char *Ptr = Path + StringLength(Path) - 1;
if(Ptr < Path) { return RC_ERROR_DIRECTORY; }
while(Ptr > Path && *Ptr != '/')
{
--Ptr;
}
*Ptr = '\0';
return RC_SUCCESS;
}
int
FinalPathComponentPosition(string Path)
{
char *Ptr = Path.Base + Path.Length - 1;
while(Ptr > Path.Base && *Ptr != '/')
{
--Ptr;
}
if(*Ptr == '/')
{
++Ptr;
}
return Ptr - Path.Base;
}
string
GetFinalComponent(string Path)
{
int NewPos = FinalPathComponentPosition(Path);
string Result = { .Base = Path.Base + NewPos, .Length = Path.Length - NewPos };
return Result;
}
string
GetBaseFilename(string Filepath,
extension_id Extension // Pass EXT_NULL (or 0) to retain the whole file path, only without its parent directories
)
{
string Result = GetFinalComponent(Filepath);
if(Extension != EXT_NULL)
{
Result.Length -= ExtensionStrings[Extension].Length;
}
return Result;
}
char *WatchTypeStrings[] =
{
"WT_HMML",
"WT_ASSET",
"WT_CONFIG",
};
typedef enum
{
WT_HMML,
WT_ASSET,
WT_CONFIG,
} watch_type;
#include "cinera_config.c"
typedef struct
{
watch_type Type;
string Path;
extension_id Extension;
project *Project;
asset *Asset;
} watch_file;
typedef struct
{
int Descriptor;
string WatchedPath;
string TargetPath;
uint64_t FileCount;
uint64_t FileCapacity;
watch_file *Files;
} watch_handle;
typedef struct
{
uint64_t Count;
uint64_t Capacity;
watch_handle *Handles;
memory_book Paths;
uint32_t DefaultEventsMask;
} watch_handles;
#define AFD 1
#define AFE 0
config *Config;
project *CurrentProject;
// NOTE(matt): Globals
mode Mode;
arena MemoryArena;
//config *Config;
assets Assets;
int inotifyInstance;
watch_handles WatchHandles;
database DB;
time_t LastPrivacyCheck;
time_t LastQuoteFetch;
#define GLOBAL_UPDATE_INTERVAL 1
//
typedef struct
{
db_header_project *Project;
db_entry *Prev;
db_entry *This;
db_entry *Next;
db_entry WorkingThis;
uint32_t PreLinkPrevOffsetTotal, PreLinkThisOffsetTotal, PreLinkNextOffsetTotal;
uint32_t PrevOffsetModifier, ThisOffsetModifier, NextOffsetModifier;
bool FormerIsFirst, LatterIsFinal;
bool DeletedEntryWasFirst, DeletedEntryWasFinal;
int16_t PrevIndex, PreDeletionThisIndex, ThisIndex, NextIndex;
} neighbourhood;
void
LogUsage(buffer *Buffer)
{
#if DEBUG
// NOTE(matt): Stack-string
char LogPath[256];
CopyString(LogPath, "%s/%s", CurrentProject->CacheDir, "buffers.log");
FILE *LogFile;
if(!(LogFile = fopen(LogPath, "a+")))
{
MakeDir(CurrentProject->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
}
bool MakeDir(string Path);
__attribute__ ((format (printf, 2, 3)))
void
LogError(int LogLevel, char *Format, ...)
{
if(Config->LogLevel >= LogLevel)
{
char *LogPath = MakeString0("ls", &Config->CacheDir, "/errors.log");
FILE *LogFile;
if(!(LogFile = fopen(LogPath, "a+")))
{
MakeDir(Config->CacheDir);
if(!(LogFile = fopen(LogPath, "a+")))
{
perror("LogUsage");
Free(LogPath);
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);
Free(LogPath);
}
}
typedef struct
{
buffer IncludesSearch;
buffer SearchEntry;
buffer Search; // NOTE(matt): This buffer is malloc'd separately, rather than claimed from the memory_arena
buffer IncludesPlayer;
buffer Player;
char Custom0[MAX_CUSTOM_SNIPPET_SHORT_LENGTH];
char Custom1[MAX_CUSTOM_SNIPPET_SHORT_LENGTH];
char Custom2[MAX_CUSTOM_SNIPPET_SHORT_LENGTH];
char Custom3[MAX_CUSTOM_SNIPPET_SHORT_LENGTH];
char Custom4[MAX_CUSTOM_SNIPPET_SHORT_LENGTH];
char Custom5[MAX_CUSTOM_SNIPPET_SHORT_LENGTH];
char Custom6[MAX_CUSTOM_SNIPPET_SHORT_LENGTH];
char Custom7[MAX_CUSTOM_SNIPPET_SHORT_LENGTH];
char Custom8[MAX_CUSTOM_SNIPPET_SHORT_LENGTH];
char Custom9[MAX_CUSTOM_SNIPPET_SHORT_LENGTH];
char Custom10[MAX_CUSTOM_SNIPPET_SHORT_LENGTH];
char Custom11[MAX_CUSTOM_SNIPPET_SHORT_LENGTH];
char Custom12[MAX_CUSTOM_SNIPPET_LONG_LENGTH];
char Custom13[MAX_CUSTOM_SNIPPET_LONG_LENGTH];
char Custom14[MAX_CUSTOM_SNIPPET_LONG_LENGTH];
char Custom15[MAX_CUSTOM_SNIPPET_LONG_LENGTH];
char Title[MAX_TITLE_LENGTH];
char URLSearch[MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH];
char URLPlayer[MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_BASE_FILENAME_LENGTH];
char VideoID[MAX_VOD_ID_LENGTH];
char VODPlatform[16];
} buffers;
int
ClaimBuffer(buffer *Buffer, buffer_id 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)
{
if(Buffer->Size > 0)
{
float PercentageUsed = (float)(Buffer->Ptr - Buffer->Location) / Buffer->Size * 100;
#if DEBUG
printf("DeclaimBuffer(%s)\n"
" Used: %ld / %d (%.2f%%)\n"
"\n"
" Total ClaimedMemory: %ld\n\n",
Buffer->ID,
Buffer->Ptr - Buffer->Location,
Buffer->Size,
PercentageUsed,
MemoryArena.Ptr - MemoryArena.Location);
#endif
LogUsage(Buffer);
if(PercentageUsed >= 95.0f)
{
// TODO(matt): Implement either dynamically growing buffers, or phoning home to miblodelcarpio@gmail.com
LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", BufferIDStrings[Buffer->ID], PercentageUsed);
fprintf(stderr, "%sWarning%s: %s used %.2f%% of its allotted memory\n",
ColourStrings[CS_ERROR], ColourStrings[CS_END],
BufferIDStrings[Buffer->ID], PercentageUsed);
}
else if(PercentageUsed >= 80.0f)
{
// TODO(matt): Implement either dynamically growing buffers, or phoning home to miblodelcarpio@gmail.com
LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", BufferIDStrings[Buffer->ID], PercentageUsed);
fprintf(stderr, "%sWarning%s: %s used %.2f%% of its allotted memory\n",
ColourStrings[CS_WARNING], ColourStrings[CS_END],
BufferIDStrings[Buffer->ID], PercentageUsed);
}
*Buffer->Location = '\0';
Buffer->Ptr = Buffer->Location;
MemoryArena.Ptr -= Buffer->Size;
Buffer->Size = 0;
//Buffer->ID = 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;
}
void
RewindCollationBuffers(buffers *CollationBuffers)
{
RewindBuffer(&CollationBuffers->IncludesPlayer);
RewindBuffer(&CollationBuffers->Player);
RewindBuffer(&CollationBuffers->IncludesSearch);
RewindBuffer(&CollationBuffers->SearchEntry);
}
// NOTE(matt): Special-purposes indices, for project generation-index and entry index
db_project_index GLOBAL_SEARCH_PAGE_INDEX = { -1, 0 };
typedef enum
{
SP_SEARCH = -1,
} special_page_id;
// TODO(matt): Consider putting the ref_info and quote_info into linked lists on the heap, just to avoid all the hardcoded sizes
typedef struct
{
char Date[32];
char Text[512];
} quote_info;
typedef struct
{
char Timecode[8];
int Identifier;
} identifier;
#define MAX_REF_IDENTIFIER_COUNT 64
typedef struct
{
char RefTitle[620];
char ID[512];
char URL[512];
char Source[256];
identifier Identifier[MAX_REF_IDENTIFIER_COUNT];
int IdentifierCount;
} ref_info;
typedef struct
{
char Marker[32];
char WrittenText[32];
} category_info;
typedef struct
{
category_info Category[64];
int Count;
} categories;
typedef struct
{
char *Name;
colour_code Colour;
} edit_type;
typedef enum
{
EDIT_INSERTION,
EDIT_APPEND,
EDIT_REINSERTION,
EDIT_DELETION,
EDIT_ADDITION,
} edit_type_id;
edit_type EditTypes[] =
{
{ "Inserted", CS_ADDITION },
{ "Appended", CS_ADDITION },
{ "Reinserted", CS_REINSERTION },
{ "Deleted", CS_DELETION },
{ "Added", CS_ADDITION }
};
#define CopyString(Dest, DestSize, Format, ...) CopyString_(__LINE__, (Dest), (DestSize), (Format), ##__VA_ARGS__)
__attribute__ ((format (printf, 4, 5)))
int
CopyString_(int LineNumber, char Dest[], int DestSize, char *Format, ...)
{
int Length = 0;
va_list Args;
va_start(Args, Format);
Length = vsnprintf(Dest, DestSize, Format, Args);
if(Length >= DestSize)
{
printf("CopyString() call on line %d has been passed a buffer too small (%d bytes) to contain null-terminated %d(+1)-character string\n", LineNumber, DestSize, Length);
__asm__("int3");
}
va_end(Args);
return Length;
}
#define ClearCopyString(Dest, DestSize, Format, ...) ClearCopyString_(__LINE__, (Dest), (DestSize), (Format), ##__VA_ARGS__)
__attribute__ ((format (printf, 4, 5)))
int
ClearCopyString_(int LineNumber, char Dest[], int DestSize, char *Format, ...)
{
Clear(Dest, DestSize);
int Length = 0;
va_list Args;
va_start(Args, Format);
Length = vsnprintf(Dest, DestSize, Format, Args);
if(Length >= DestSize)
{
printf("CopyString() call on line %d has been passed a buffer too small (%d bytes) to contain null-terminated %d(+1)-character string\n", LineNumber, DestSize, Length);
__asm__("int3");
}
va_end(Args);
return Length;
}
#define CopyStringNoFormat(Dest, DestSize, String) CopyStringNoFormat_(__LINE__, Dest, DestSize, String)
int
CopyStringNoFormat_(int LineNumber, char *Dest, int DestSize, string String)
{
if(String.Length + 1 > DestSize)
{
printf("CopyStringNoFormat() call on line %d has been passed a buffer too small (%d bytes) to contain null-terminated %ld(+1)-character string:\n"
"%.*s\n", LineNumber, DestSize, String.Length, (int)String.Length, String.Base);
__asm__("int3");
}
for(int i = 0; i < String.Length; ++i)
{
*Dest++ = String.Base[i];
}
*Dest = '\0';
return String.Length;
}
#define ClearCopyStringNoFormat(Dest, DestSize, String) ClearCopyStringNoFormat_(__LINE__, Dest, DestSize, String)
int
ClearCopyStringNoFormat_(int LineNumber, char *Dest, int DestSize, string String)
{
if(String.Length + 1 > DestSize)
{
printf("ClearCopyStringNoFormat() call on line %d has been passed a buffer too small (%d bytes) to contain null-terminated %ld(+1)-character string:\n"
"%.*s\n", LineNumber, DestSize, String.Length, (int)String.Length, String.Base);
__asm__("int3");
}
Clear(Dest, DestSize);
for(int i = 0; i < String.Length; ++i)
{
*Dest++ = String.Base[i];
}
*Dest = '\0';
return String.Length;
}
// TODO(matt): Maybe do a version of this that takes a string as a Terminator
#define CopyStringNoFormatT(Dest, DestSize, String, Terminator) CopyStringNoFormatT_(__LINE__, Dest, DestSize, String, Terminator)
int
CopyStringNoFormatT_(int LineNumber, char *Dest, int DestSize, char *String, char Terminator)
{
int Length = 0;
char *Start = String;
while(*String != Terminator)
{
*Dest++ = *String++;
++Length;
}
if(Length >= DestSize)
{
printf("CopyStringNoFormatT() call on line %d has been passed a buffer too small (%d bytes) to contain %c-terminated %d(+1)-character string:\n"
"%.*s\n", LineNumber, DestSize, Terminator == 0 ? '0' : Terminator, Length, Length, Start);
__asm__("int3");
}
*Dest = '\0';
return Length;
}
#define CopyStringToBuffer(Dest, Format, ...) CopyStringToBuffer_(__LINE__, Dest, Format, ##__VA_ARGS__)
__attribute__ ((format (printf, 3, 4)))
void
CopyStringToBuffer_(int LineNumber, buffer *Dest, char *Format, ...)
{
va_list Args;
va_start(Args, Format);
int Length = vsnprintf(Dest->Ptr, Dest->Size - (Dest->Ptr - Dest->Location), Format, Args);
va_end(Args);
if(Length + (Dest->Ptr - Dest->Location) >= Dest->Size)
{
fprintf(stderr, "CopyStringToBuffer(%s) call on line %d cannot accommodate null-terminated %d(+1)-character string:\n"
"%s\n", BufferIDStrings[Dest->ID], LineNumber, Length, Format);
__asm__("int3");
}
Dest->Ptr += Length;
}
#define CopyStringToBufferNoFormat(Dest, String) CopyStringToBufferNoFormat_(__LINE__, Dest, String)
void
CopyStringToBufferNoFormat_(int LineNumber, buffer *Dest, string String)
{
if(String.Length + 1 > Dest->Size - (Dest->Ptr - Dest->Location))
{
fprintf(stderr, "CopyStringToBufferNoFormat(%s) call on line %d cannot accommodate %ld-character string:\n"
"%.*s\n", BufferIDStrings[Dest->ID], LineNumber, String.Length, (int)String.Length, String.Base);
__asm__("int3");
}
for(int i = 0; i < String.Length; ++i)
{
*Dest->Ptr++ = String.Base[i];
}
*Dest->Ptr = '\0';
}
#define CopyStringToBufferNoFormatL(Dest, Length, String) CopyStringToBufferNoFormatL_(__LINE__, Dest, Length, String)
void
CopyStringToBufferNoFormatL_(int LineNumber, buffer *Dest, int Length, char *String)
{
char *Start = String;
for(int i = 0; i < Length; ++i)
{
*Dest->Ptr++ = *String++;
}
if(Dest->Ptr - Dest->Location >= Dest->Size)
{
fprintf(stderr, "CopyStringToBufferNoFormat(%s) call on line %d cannot accommodate %ld-character string:\n"
"%s\n", BufferIDStrings[Dest->ID], LineNumber, StringLength(Start), Start);
__asm__("int3");
}
*Dest->Ptr = '\0';
}
#define CopyStringToBufferHTMLSafe(Dest, String) CopyStringToBufferHTMLSafe_(__LINE__, Dest, String)
void
CopyStringToBufferHTMLSafe_(int LineNumber, buffer *Dest, string String)
{
int Length = String.Length;
for(int i = 0; i < String.Length; ++i)
{
switch(String.Base[i])
{
case '<': case '>': Length += 3; break;
case '&': case '\'': Length += 4; break;
case '\"': Length += 5; break;
default: break;
}
}
if((Dest->Ptr - Dest->Location) + Length >= Dest->Size)
{
fprintf(stderr, "CopyStringToBufferHTMLSafe(%s) call on line %d cannot accommodate %d(+1)-character HTML-sanitised string:\n"
"%.*s\n", BufferIDStrings[Dest->ID], LineNumber, Length, (int)String.Length, String.Base);
__asm__("int3");
}
for(int i = 0; i < String.Length; ++i)
{
switch(String.Base[i])
{
case '<': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'l'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 3; break;
case '>': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'g'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 3; break;
case '&': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'a'; *Dest->Ptr++ = 'm'; *Dest->Ptr++ = 'p'; *Dest->Ptr++ = ';'; Length += 4; break;
case '\"': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'q'; *Dest->Ptr++ = 'u'; *Dest->Ptr++ = 'o'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 5; break;
case '\'': *Dest->Ptr++ = '&'; *Dest->Ptr++ = '#'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = '9'; *Dest->Ptr++ = ';'; Length += 4; break;
default: *Dest->Ptr++ = String.Base[i]; break;
}
}
*Dest->Ptr = '\0';
}
#define CopyStringToBufferHTMLSafeBreakingOnSlash(Dest, String) CopyStringToBufferHTMLSafeBreakingOnSlash_(__LINE__, Dest, String)
void
CopyStringToBufferHTMLSafeBreakingOnSlash_(int LineNumber, buffer *Dest, char *String)
{
char *Start = String;
int Length = StringLength(String);
while(*String)
{
switch(*String)
{
case '<': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'l'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 3; break;
case '>': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'g'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 3; break;
case '&': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'a'; *Dest->Ptr++ = 'm'; *Dest->Ptr++ = 'p'; *Dest->Ptr++ = ';'; Length += 4; break;
case '\'': *Dest->Ptr++ = '&'; *Dest->Ptr++ = '#'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = '9'; *Dest->Ptr++ = ';'; Length += 4; break;
case '\"': *Dest->Ptr++ = '&'; *Dest->Ptr++ = 'q'; *Dest->Ptr++ = 'u'; *Dest->Ptr++ = 'o'; *Dest->Ptr++ = 't'; *Dest->Ptr++ = ';'; Length += 5; break;
case '/': *Dest->Ptr++ = '/'; *Dest->Ptr++ = '&'; *Dest->Ptr++ = '#'; *Dest->Ptr++ = '8'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '0'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = ';'; Length += 7; break;
default: *Dest->Ptr++ = *String; break;
}
++String;
}
if(Dest->Ptr - Dest->Location >= Dest->Size)
{
fprintf(stderr, "CopyStringToBufferHTMLSafeBreakingOnSlash(%s) call on line %d cannot accommodate %d(+1)-character HTML-sanitised string:\n"
"%s\n", BufferIDStrings[Dest->ID], LineNumber, Length, Start);
__asm__("int3");
}
*Dest->Ptr = '\0';
}
#define CopyStringToBufferHTMLPercentEncoded(Dest, String) CopyStringToBufferHTMLPercentEncoded_(__LINE__, Dest, String)
void
CopyStringToBufferHTMLPercentEncoded_(int LineNumber, buffer *Dest, string String)
{
int Length = String.Length;
for(int i = 0; i < String.Length; ++ i)
{
switch(String.Base[i])
{
case ' ':
case '\"':
case '%':
case '&':
case '<':
case '>':
case '?':
case '\\':
case '^':
case '`':
case '{':
case '|':
case '}': Length += 2; break;
default: break;
}
}
if((Dest->Ptr - Dest->Location) + Length >= Dest->Size)
{
fprintf(stderr, "CopyStringToBufferHTMLPercentEncodedL(%s) call on line %d cannot accommodate %d(+1)-character percent-encoded string:\n"
"%.*s\n", BufferIDStrings[Dest->ID], LineNumber, Length, (int)String.Length, String.Base);
__asm__("int3");
}
for(int i = 0; i < String.Length; ++ i)
{
switch(String.Base[i])
{
case ' ': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '0'; Length += 2; break;
case '\"': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '2'; Length += 2; break;
case '%': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '5'; Length += 2; break;
case '&': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '2'; *Dest->Ptr++ = '6'; Length += 2; break;
case '<': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = 'C'; Length += 2; break;
case '>': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = 'E'; Length += 2; break;
case '?': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '3'; *Dest->Ptr++ = 'F'; Length += 2; break;
case '\\': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '5'; *Dest->Ptr++ = 'C'; Length += 2; break;
case '^': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '5'; *Dest->Ptr++ = 'E'; Length += 2; break;
case '`': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '6'; *Dest->Ptr++ = '0'; Length += 2; break;
case '{': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '7'; *Dest->Ptr++ = 'B'; Length += 2; break;
case '|': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '7'; *Dest->Ptr++ = 'C'; Length += 2; break;
case '}': *Dest->Ptr++ = '%'; *Dest->Ptr++ = '7'; *Dest->Ptr++ = 'D'; Length += 2; break;
default: *Dest->Ptr++ = String.Base[i]; break;
}
}
*Dest->Ptr = '\0';
}
#define CopyBuffer(Dest, Src) CopyBuffer_(__LINE__, Dest, Src)
void
CopyBuffer_(int LineNumber, buffer *Dest, buffer *Src)
{
if((Dest->Ptr - Dest->Location + Src->Ptr - Src->Location) >= Dest->Size)
{
fprintf(stderr, "CopyBuffer(%s) call on line %d cannot accommodate %ld(+1)-character %s\n", BufferIDStrings[Dest->ID], LineNumber, StringLength(Src->Location), BufferIDStrings[Src->ID]);
__asm__("int3");
}
for(int i = 0; i < Src->Ptr - Src->Location; ++i)
{
*Dest->Ptr++ = Src->Location[i];
}
*Dest->Ptr = '\0';
}
typedef enum
{
PAGE_PLAYER = 1 << 0,
PAGE_SEARCH = 1 << 1
} page_type;
// NOTE(matt): Perhaps this OffsetLandmarks() could be made redundant once we're on the LUT
void
OffsetLandmarks(buffer *Dest, buffer *Src, page_type PageType)
{
if(Config->QueryString.Length > 0)
{
for(int i = 0; i < Assets.Count; ++i)
{
asset *Asset = GetPlaceInBook(&Assets.Asset, i);
if(PageType == PAGE_PLAYER)
{
for(int LandmarkIndex = 0; LandmarkIndex < Asset->PlayerLandmarkCount; ++LandmarkIndex)
{
if(Asset->Player[LandmarkIndex].BufferID == Src->ID)
{
Asset->Player[LandmarkIndex].Offset += Dest->Ptr - Dest->Location;
Asset->Player[LandmarkIndex].BufferID = Dest->ID;
}
}
}
else
{
for(int LandmarkIndex = 0; LandmarkIndex < Asset->SearchLandmarkCount; ++LandmarkIndex)
{
if(Asset->Search[LandmarkIndex].BufferID == Src->ID)
{
Asset->Search[LandmarkIndex].Offset += Dest->Ptr - Dest->Location;
Asset->Search[LandmarkIndex].BufferID = Dest->ID;
}
}
}
}
}
}
#define CopyLandmarkedBuffer(Dest, Src, PrevStartLinkOffset, PageType) CopyLandmarkedBuffer_(__LINE__, Dest, Src, PrevStartLinkOffset, PageType)
void
CopyLandmarkedBuffer_(int LineNumber, buffer *Dest, buffer *Src, uint32_t *PrevStartLinkOffset, page_type PageType)
{
if((Dest->Ptr - Dest->Location + Src->Ptr - Src->Location) >= Dest->Size)
{
fprintf(stderr, "CopyLandmarkedBuffer(%s) call on line %d cannot accommodate %ld(+1)-character %s\n", BufferIDStrings[Dest->ID], LineNumber, StringLength(Src->Location), BufferIDStrings[Src->ID]);
__asm__("int3");
}
if(PrevStartLinkOffset) { *PrevStartLinkOffset += Dest->Ptr - Dest->Location; }
OffsetLandmarks(Dest, Src, PageType);
for(int i = 0; i < Src->Ptr - Src->Location; ++i)
{
*Dest->Ptr++ = Src->Location[i];
}
*Dest->Ptr = '\0';
}
#define CopyBufferSized(Dest, Src, Size) CopyBufferSized_(__LINE__, Dest, Src, Size)
void
CopyBufferSized_(int LineNumber, buffer *Dest, buffer *Src, uint64_t Size)
{
// NOTE(matt): Similar to CopyBuffer(), just without null-terminating
if((Dest->Ptr - Dest->Location + Size) > Dest->Size)
{
fprintf(stderr, "CopyBufferSized(%s) call on line %d cannot accommodate %ld-character %s\n", BufferIDStrings[Dest->ID], LineNumber, Size, BufferIDStrings[Src->ID]);
__asm__("int3");
}
for(int i = 0; i < Size; ++i)
{
*Dest->Ptr++ = Src->Location[i];
}
}
void
AppendBuffer(buffer *Dest, buffer *Src)
{
uint64_t BytePosition = Dest->Ptr - Dest->Location;
Dest->Size = Dest->Ptr - Dest->Location + Src->Ptr - Src->Location + 1;
Dest->Location = realloc(Dest->Location, Dest->Size);
Dest->Ptr = Dest->Location + BytePosition;
CopyBuffer(Dest, Src);
}
void
AppendLandmarkedBuffer(buffer *Dest, buffer *Src, page_type PageType)
{
OffsetLandmarks(Dest, Src, PageType);
AppendBuffer(Dest, Src);
}
void
AppendStringToBuffer(buffer *B, string S)
{
uint64_t BytePosition = B->Ptr - B->Location;
B->Size = B->Ptr - B->Location + S.Length + 1;
B->Location = realloc(B->Location, B->Size);
B->Ptr = B->Location + BytePosition;
CopyStringToBufferNoFormat(B, S);
}
void
AppendInt32ToBuffer(buffer *B, int32_t I)
{
int Digits = DigitsInInt(&I);
uint64_t BytePosition = B->Ptr - B->Location;
B->Size = B->Ptr - B->Location + Digits + 1;
B->Location = realloc(B->Location, B->Size);
B->Ptr = B->Location + BytePosition;
char Temp[Digits + 1];
sprintf(Temp, "%i", I);
CopyStringToBufferNoFormat(B, Wrap0(Temp));
}
void
AppendUint32ToBuffer(buffer *B, uint32_t I)
{
int Digits = DigitsInUint(&I);
uint64_t BytePosition = B->Ptr - B->Location;
B->Size = B->Ptr - B->Location + Digits + 1;
B->Location = realloc(B->Location, B->Size);
B->Ptr = B->Location + BytePosition;
char Temp[Digits + 1];
sprintf(Temp, "%i", I);
CopyStringToBufferNoFormat(B, Wrap0(Temp));
}
uint64_t
AmpersandEncodedStringLength(string S)
{
uint64_t Result = S.Length;
for(int i = 0; i < S.Length; ++i)
{
switch(S.Base[i])
{
case '<': case '>': Result += 3; break;
case '&': case '\'': Result += 4; break;
case '\"': Result += 5; break;
default: break;
}
}
return Result;
}
void
AppendStringToBufferHTMLSafe(buffer *B, string S)
{
uint64_t BytePosition = B->Ptr - B->Location;
B->Size = B->Ptr - B->Location + AmpersandEncodedStringLength(S) + 1;
B->Location = realloc(B->Location, B->Size);
B->Ptr = B->Location + BytePosition;
CopyStringToBufferHTMLSafe(B, S);
}
void
IndentBuffer(buffer *B, uint64_t IndentationLevel)
{
for(int i = 0; i < INDENT_WIDTH * IndentationLevel; ++i)
{
AppendStringToBuffer(B, Wrap0(" "));
}
}
enum
{
C_SEEK_FORWARDS,
C_SEEK_BACKWARDS
} seek_directions;
enum
{
C_SEEK_START, // First character of string
C_SEEK_BEFORE, // Character before first character
C_SEEK_END, // Last character of string
C_SEEK_AFTER // Character after last character
} seek_positions;
int
SeekBufferForString(buffer *Buffer, char *String,
enum8(seek_directions) Direction,
enum8(seek_positions) Position)
{
// TODO(matt): Optimise? Some means of analysing the String to increment
// the pointer in bigger strides
// Perhaps count up runs of consecutive chars and seek for the char with
// the longest run, in strides of that run-length
char *InitialLocation = Buffer->Ptr;
if(Direction == C_SEEK_FORWARDS)
{
while(Buffer->Ptr - Buffer->Location < Buffer->Size - StringLength(String)
&& StringsDifferT(String, Buffer->Ptr, 0))
{
++Buffer->Ptr;
}
}
else
{
while(Buffer->Ptr > Buffer->Location
&& StringsDifferT(String, Buffer->Ptr, 0))
{
--Buffer->Ptr;
}
}
if(StringsDifferT(String, Buffer->Ptr, 0))
{
Buffer->Ptr = InitialLocation;
return RC_UNFOUND;
}
switch(Position)
{
case C_SEEK_START:
break;
case C_SEEK_BEFORE:
if(Buffer->Ptr > Buffer->Location)
{
--Buffer->Ptr;
break;
}
else
{
return RC_ERROR_SEEK; // Ptr remains at string start
}
case C_SEEK_END:
Buffer->Ptr += StringLength(String) - 1;
break;
case C_SEEK_AFTER:
if(Buffer->Size >= Buffer->Ptr - Buffer->Location + StringLength(String))
{
Buffer->Ptr += StringLength(String);
break;
}
else
{
return RC_ERROR_SEEK; // Ptr remains at string start
// NOTE(matt): Should it, however, be left at the end of the string?
}
}
return RC_SUCCESS;
}
void
ResolvePath(char **Path)
{
buffer B;
ClaimBuffer(&B, BID_RESOLVED_PATH, StringLength(*Path) + 1);
CopyStringToBufferNoFormat(&B, Wrap0(*Path));
B.Ptr = B.Location;
while(SeekBufferForString(&B, "/../", C_SEEK_FORWARDS, C_SEEK_END) == RC_SUCCESS)
{
char *NextComponentHead = B.Ptr;
int RemainingChars = StringLength(NextComponentHead);
--B.Ptr;
SeekBufferForString(&B, "/", C_SEEK_BACKWARDS, C_SEEK_BEFORE);
SeekBufferForString(&B, "/", C_SEEK_BACKWARDS, C_SEEK_START);
CopyStringToBufferNoFormat(&B, Wrap0(NextComponentHead));
Clear(B.Ptr, B.Size - (B.Ptr - B.Location));
B.Ptr -= RemainingChars;
}
Free(*Path);
ExtendString0(Path, Wrap0(B.Location));
DeclaimBuffer(&B);
}
char *
ExpandPath(string Path, string *RelativeToFile)
{
int Flags = WRDE_NOCMD | WRDE_UNDEF | WRDE_APPEND;
wordexp_t Expansions = {};
char *WorkingPath = MakeString0("l", &Path);
wordexp(WorkingPath, &Expansions, Flags);
Free(WorkingPath);
char *Result = 0;
if(Expansions.we_wordc > 0)
{
if(Expansions.we_wordv[0][0] != '/')
{
if(RelativeToFile)
{
string RelativeDir = StripComponentFromPath(*RelativeToFile);
Result = MakeString0("lss", &RelativeDir, "/", Expansions.we_wordv[0]);
}
else
{
Result = MakeString0("sss", getenv("PWD"), "/", Expansions.we_wordv[0]);
}
}
else
{
Result = MakeString0("s", Expansions.we_wordv[0]);
}
}
wordfree(&Expansions);
ResolvePath(&Result);
return Result;
}
bool
MakeDir(string Path)
{
for(int i = 1; i < Path.Length; ++i)
{
if(Path.Base[i] == '/')
{
Path.Base[i] = '\0';
int ReturnCode = mkdir(Path.Base, 00755);
Path.Base[i] = '/';
if(ReturnCode == -1 && errno == EACCES)
{
return FALSE;
}
}
}
char *FullPath = MakeString0("l", &Path);
int ReturnCode = mkdir(FullPath, 00755);
Free(FullPath);
if(ReturnCode == -1 && errno == EACCES)
{
return FALSE;
}
return TRUE;
}
char *
GetDirectoryPath(char *Filepath)
{
char *Ptr = Filepath + StringLength(Filepath) - 1;
while(Ptr > Filepath && *Ptr != '/')
{
--Ptr;
}
if(Ptr == Filepath)
{
*Ptr++ = '.';
}
*Ptr = '\0';
return Filepath;
}
void
PushTemplateTag(template *Template, int Offset, enum8(template_tag_types) TagType, asset *Asset, navigation_type NavigationType)
{
Template->Metadata.Tags = Fit(Template->Metadata.Tags, sizeof(*Template->Metadata.Tags), Template->Metadata.TagCount, 16, FALSE);
Template->Metadata.Tags[Template->Metadata.TagCount].Offset = Offset;
Template->Metadata.Tags[Template->Metadata.TagCount].TagCode = TagType;
Template->Metadata.Tags[Template->Metadata.TagCount].Asset = Asset;
Template->Metadata.Tags[Template->Metadata.TagCount].NavigationType = NavigationType;
++Template->Metadata.TagCount;
}
void
InitTemplate(template *Template, string Location, template_type Type)
{
Template->Metadata.Type = Type;
if(Type == TEMPLATE_GLOBAL_SEARCH)
{
Template->File = InitFile(&Config->GlobalTemplatesDir, &Config->GlobalSearchTemplatePath, EXT_NULL);
}
else
{
if(Location.Base[0] == '/')
{
Template->File = InitFile(0, &Location, EXT_NULL);
}
else
{
Template->File = InitFile(Type == TEMPLATE_BESPOKE ? &CurrentProject->HMMLDir : &CurrentProject->TemplatesDir, &Location, EXT_NULL);
}
}
printf("%sPacking%s template: %s\n", ColourStrings[CS_ONGOING], ColourStrings[CS_END], Template->File.Path);
ReadFileIntoBuffer(&Template->File);
ClearTemplateMetadata(Template);
}
int
TimecodeToSeconds(char *Timecode)
{
int HMS[3] = { 0, 0, 0 }; // 0 == Seconds; 1 == Minutes; 2 == Hours
int Colons = 0;
while(*Timecode)
{
//if((*Timecode < '0' || *Timecode > '9') && *Timecode != ':') { return FALSE; }
if(*Timecode == ':')
{
++Colons;
//if(Colons > 2) { return FALSE; }
for(int i = 0; i < Colons; ++i)
{
HMS[Colons - i] = HMS[Colons - (i + 1)];
}
HMS[0] = 0;
}
else
{
HMS[0] = HMS[0] * 10 + *Timecode - '0';
}
++Timecode;
}
//if(HMS[0] > 59 || HMS[1] > 59 || Timecode[-1] == ':') { return FALSE; }
return HMS[2] * 60 * 60 + HMS[1] * 60 + HMS[0];
}
typedef struct
{
unsigned int Hue:16;
unsigned int Saturation:8;
unsigned int Lightness:8;
} hsl_colour;
hsl_colour
CharToColour(char Char)
{
hsl_colour Colour;
if(Char >= 'a' && Char <= 'z')
{
Colour.Hue = (((float)Char - 'a') / ('z' - 'a') * 360);
Colour.Saturation = (((float)Char - 'a') / ('z' - 'a') * 26 + 74);
}
else if(Char >= 'A' && Char <= 'Z')
{
Colour.Hue = (((float)Char - 'A') / ('Z' - 'A') * 360);
Colour.Saturation = (((float)Char - 'A') / ('Z' - 'A') * 26 + 74);
}
else if(Char >= '0' && Char <= '9')
{
Colour.Hue = (((float)Char - '0') / ('9' - '0') * 360);
Colour.Saturation = (((float)Char - '0') / ('9' - '0') * 26 + 74);
}
else
{
Colour.Hue = 180;
Colour.Saturation = 50;
}
return Colour;
}
void
StringToColourHash(hsl_colour *Colour, string String)
{
Colour->Hue = 0;
Colour->Saturation = 0;
Colour->Lightness = 74;
for(int i = 0; i < String.Length; ++i)
{
Colour->Hue += CharToColour(String.Base[i]).Hue;
Colour->Saturation += CharToColour(String.Base[i]).Saturation;
}
Colour->Hue = Colour->Hue % 360;
Colour->Saturation = Colour->Saturation % 26 + 74;
}
char *
SanitisePunctuation(char *String)
{
char *Ptr = String;
while(*Ptr)
{
if(*Ptr == ' ')
{
*Ptr = '_';
}
if((*Ptr < '0' || *Ptr > '9') &&
(*Ptr < 'a' || *Ptr > 'z') &&
(*Ptr < 'A' || *Ptr > 'Z'))
{
*Ptr = '-';
}
++Ptr;
}
return String;
}
// TODO(matt): Use ExtendString0()
// AFD
void
ConstructURLPrefix(buffer *URLPrefix, asset_type AssetType, enum8(pages) PageType)
{
RewindBuffer(URLPrefix);
if(Config->AssetsRootURL.Length > 0)
{
CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config->AssetsRootURL);
CopyStringToBuffer(URLPrefix, "/");
}
else
{
if(PageType == PAGE_PLAYER)
{
CopyStringToBuffer(URLPrefix, "../");
}
CopyStringToBuffer(URLPrefix, "../");
}
switch(AssetType)
{
case ASSET_CSS:
if(Config->CSSDir.Length > 0)
{
CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config->CSSDir);
CopyStringToBuffer(URLPrefix, "/");
}
break;
case ASSET_IMG:
if(Config->ImagesDir.Length > 0 )
{
CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config->ImagesDir);
CopyStringToBuffer(URLPrefix, "/");
}
break;
case ASSET_JS:
if(Config->JSDir.Length > 0)
{
CopyStringToBufferHTMLPercentEncoded(URLPrefix, Config->JSDir);
CopyStringToBuffer(URLPrefix, "/");
}
break;
default: break;
}
}
// TODO(matt): Do a PushSpeaker() type of thing
typedef struct
{
hsl_colour Colour;
person *Person;
char *Abbreviation;
bool Seen;
} speaker;
typedef struct
{
speaker *Speaker;
uint64_t Count;
} speakers;
enum
{
CreditsError_NoHost,
CreditsError_NoIndexer,
CreditsError_NoCredentials
} credits_errors;
size_t
StringToFletcher32(const char *Data, size_t Length)
{// https://en.wikipedia.org/wiki/Fletcher%27s_checksum
size_t c0, c1;
unsigned short int i;
for(c0 = c1 = 0; Length >= 360; Length -= 360)
{
for(i = 0; i < 360; ++i)
{
c0 += *Data++;
c1 += c0;
}
c0 %= 65535;
c1 %= 65535;
}
for(i = 0; i < Length; ++i)
{
c0 += *Data++;
c1 += c0;
}
c0 %= 65535;
c1 %= 65535;
return (c1 << 16 | c0);
}
char *
ConstructAssetPath(file *AssetFile, string Filename, asset_type Type)
{
char *Result = 0;
if(Config->AssetsRootDir.Length > 0)
{
ExtendString0(&Result, Config->AssetsRootDir);
}
string AssetDir = {};
switch(Type)
{
case ASSET_CSS: AssetDir = Config->CSSDir; break;
case ASSET_IMG: AssetDir = Config->ImagesDir; break;
case ASSET_JS: AssetDir = Config->JSDir; break;
default: break;
}
if(AssetDir.Length > 0)
{
ExtendString0(&Result, Wrap0("/"));
ExtendString0(&Result, AssetDir);
}
if(Filename.Length > 0)
{
ExtendString0(&Result, Wrap0("/"));
ExtendString0(&Result, Filename);
}
return Result;
}
void
CycleFile(file *File)
{
fclose(File->Handle);
// TODO(matt): Rather than freeing the buffer, why not just realloc it to fit the new file? Couldn't that work easily?
// The reason we Free / Reread is to save us having to shuffle the buffer contents around, basically
// If we switch the whole database over to use linked lists - the database being the only file we actually cycle
// at the moment - then we may actually obviate the need to cycle at all
FreeBuffer(&File->Buffer);
ReadFileIntoBuffer(File);
}
void
SetSignpostOffset(buffer *B, file_edit *E, file_signpost *S)
{
S->Byte = (char *)S->Ptr - B->Location;
if(S->Byte >= E->BytePosition)
{
S->Byte += E->Size;
}
}
void
CycleSignpostedFile(file_signposted *File)
{
// NOTE(matt) This file_signposted struct is totally hardcoded to the Metadata file, but may suffice for now
//
// TODO(matt): This is probably insufficient. We likely need to offset the pointers any time we add stuff to the database
fclose(File->File.Handle);
// TODO(matt): Rather than freeing the buffer, why not just realloc it to fit the new file? Couldn't that work easily?
// The reason we Free / Reread is to save us having to shuffle the buffer contents around, basically
// If we switch the whole database over to use linked lists - the database being the only file we actually cycle
// at the moment - then we may actually obviate the need to cycle at all
file_signposts *S = &File->Signposts;
buffer *B = &File->File.Buffer;
uint64_t PtrByte = B->Ptr - B->Location;
if(S->ProjectsBlock.Ptr) { SetSignpostOffset(B, &S->Edit, &S->ProjectsBlock); }
if(S->ProjectParent.Ptr) { SetSignpostOffset(B, &S->Edit, &S->ProjectParent); }
if(S->ProjectHeader.Ptr) { SetSignpostOffset(B, &S->Edit, &S->ProjectHeader); }
if(S->Prev.Ptr) { SetSignpostOffset(B, &S->Edit, &S->Prev); }
if(S->This.Ptr) { SetSignpostOffset(B, &S->Edit, &S->This); }
if(S->Next.Ptr) { SetSignpostOffset(B, &S->Edit, &S->Next); }
if(S->AssetsBlock.Ptr) { SetSignpostOffset(B, &S->Edit, &S->AssetsBlock); }
FreeBuffer(B);
ReadFileIntoBuffer(&File->File);
B->Ptr = B->Location + PtrByte;
if(S->ProjectsBlock.Ptr) { S->ProjectsBlock.Ptr = B->Location + S->ProjectsBlock.Byte; }
if(S->ProjectParent.Ptr) { S->ProjectParent.Ptr = B->Location + S->ProjectParent.Byte; }
if(S->ProjectHeader.Ptr) { S->ProjectHeader.Ptr = B->Location + S->ProjectHeader.Byte; }
if(S->Prev.Ptr) { S->Prev.Ptr = B->Location + S->Prev.Byte; }
if(S->This.Ptr) { S->This.Ptr = B->Location + S->This.Byte; }
if(S->Next.Ptr) { S->Next.Ptr = B->Location + S->Next.Byte; }
if(S->AssetsBlock.Ptr) { S->AssetsBlock.Ptr = B->Location + S->AssetsBlock.Byte; }
S->Edit.BytePosition = 0;
S->Edit.Size = 0;
}
void
ClearTerminalRow(uint64_t Length)
{
for(uint64_t i = 0; i < Length; ++i)
{
fprintf(stderr, "\b \b");
}
}
typedef struct
{
uint32_t First;
uint32_t Length;
} landmark_range;
bool
ProjectIndicesMatch(db_project_index A, db_project_index B)
{
return (A.Generation == B.Generation && A.Index == B.Index);
}
db_landmark *
LocateFirstLandmark(db_asset *A)
{
char *Ptr = (char *)A;
Ptr += sizeof(*A);
return (db_landmark *)Ptr;
}
db_landmark *
LocateLandmark(db_asset *A, uint64_t Index)
{
char *Ptr = (char *)A;
Ptr += sizeof(*A) + sizeof(db_landmark) * Index;
return (db_landmark *)Ptr;
}
uint32_t
GetIndexRangeLength(db_asset *A, landmark_range ProjectRange, uint64_t LandmarkIndex)
{
uint32_t Result = 0;
db_landmark *FirstLandmarkOfRange = LocateLandmark(A, LandmarkIndex);
db_landmark *Landmark = FirstLandmarkOfRange;
while(LandmarkIndex < (ProjectRange.First + ProjectRange.Length) && ProjectIndicesMatch(FirstLandmarkOfRange->Project, Landmark->Project) && FirstLandmarkOfRange->EntryIndex == Landmark->EntryIndex)
{
++Result;
++LandmarkIndex;
++Landmark;
}
return Result;
}
landmark_range
GetIndexRange(db_asset *A, landmark_range ProjectRange, uint64_t LandmarkIndex)
{
landmark_range Result = {};
Result.First = LandmarkIndex;
db_landmark *LandmarkInRange = LocateLandmark(A, LandmarkIndex);
db_landmark *Landmark = LandmarkInRange;
while(Result.First > ProjectRange.First && ProjectIndicesMatch(LandmarkInRange->Project, Landmark->Project) && LandmarkInRange->EntryIndex == Landmark->EntryIndex)
{
--Result.First;
--Landmark;
}
if(LandmarkInRange->EntryIndex != Landmark->EntryIndex)
{
++Result.First;
}
Result.Length = GetIndexRangeLength(A, ProjectRange, Result.First);
return Result;
}
int
ProjectIndicesDiffer(db_project_index A, db_project_index B)
{
if(A.Generation < B.Generation || (A.Generation == B.Generation && A.Index < B.Index))
{
return -1;
}
else if(A.Generation > B.Generation || (A.Generation == B.Generation && A.Index > B.Index))
{
return 1;
}
else
{
return 0;
}
}
landmark_range
BinarySearchForMetadataLandmark(db_asset *Asset, landmark_range ProjectRange, int EntryIndex)
{
landmark_range Result = ProjectRange;
if(ProjectRange.Length > 0)
{
db_landmark *FirstLandmark = LocateFirstLandmark(Asset);
uint64_t Lower = ProjectRange.First;
db_landmark *LowerLandmark = FirstLandmark + Lower;
uint64_t Upper = Lower + ProjectRange.Length - 1;
db_landmark *UpperLandmark = FirstLandmark + Upper;
if(EntryIndex < LowerLandmark->EntryIndex)
{
Result.First = Lower;
Result.Length = 0;
return Result;
}
// TODO(matt): Is there a slicker way of doing this?
if(EntryIndex > UpperLandmark->EntryIndex)
{
Result.First = Upper + 1;
Result.Length = 0;
return Result;
}
int Pivot = Upper - ((Upper - Lower) >> 1);
db_landmark *PivotLandmark;
do {
LowerLandmark = FirstLandmark + Lower;
PivotLandmark = FirstLandmark + Pivot;
UpperLandmark = FirstLandmark + Upper;
if(EntryIndex == LowerLandmark->EntryIndex)
{
return GetIndexRange(Asset, ProjectRange, Lower);
}
if(EntryIndex == PivotLandmark->EntryIndex)
{
return GetIndexRange(Asset, ProjectRange, Pivot);
}
if(EntryIndex == UpperLandmark->EntryIndex)
{
return GetIndexRange(Asset, ProjectRange, Upper);
}
if(EntryIndex < PivotLandmark->EntryIndex) { Upper = Pivot; }
else { Lower = Pivot; }
Pivot = Upper - ((Upper - Lower) >> 1);
} while(Upper > Pivot);
Result.First = Upper;
Result.Length = 0;
return Result;
}
return Result;
}
void
PrintEntryIndex(db_project_index Project, int64_t EntryIndex)
{
fprintf(stderr, "%s%i:%i%s %s%3li%s",
ColourStrings[CS_MAGENTA], Project.Generation, Project.Index, ColourStrings[CS_END],
ColourStrings[CS_BLUE_BOLD], EntryIndex, ColourStrings[CS_END]);
}
void
PrintLandmark(db_landmark *L)
{
PrintEntryIndex(L->Project, L->EntryIndex);
fprintf(stderr, " %6u", L->Position);
}
void
PrintAsset(db_asset *A, uint16_t *Index)
{
if(Index)
{
Colourise(CS_BLACK_BOLD);
fprintf(stderr, "[%i]", *Index);
Colourise(CS_END);
}
string FilenameL = Wrap0i(A->Filename, sizeof(A->Filename));
fprintf(stderr, " %s asset: %.*s [%8x]\n",
AssetTypeNames[A->Type], (int)FilenameL.Length, FilenameL.Base, A->Hash);
if(A->Type == ASSET_IMG)
{
bool Associated = A->Associated;
uint64_t Variants = A->Variants;
if(Index)
{
int SpacesRequired = 1 + 1 + DigitsInInt(Index);
for(int i = 0; i < SpacesRequired; ++i)
{
fprintf(stderr, " ");
}
}
fprintf(stderr, " Associated: ");
if(Associated) { PrintC(CS_GREEN, "true"); }
else { PrintC(CS_RED, "false"); }
fprintf(stderr, " • Variants: ");
Colourise(CS_BLUE_BOLD);
fprintf(stderr, "%lu", Variants);
Colourise(CS_END);
fprintf(stderr, " • Dimensions: ");
Colourise(CS_BLUE_BOLD);
fprintf(stderr, "%u", A->Width);
fprintf(stderr, "×");
fprintf(stderr, "%u", A->Height);
Colourise(CS_END);
fprintf(stderr, "\n");
}
}
void
PrintAssetAndLandmarks(db_asset *A, uint16_t *Index)
{
fprintf(stderr, "\n"
"\n");
PrintAsset(A, Index);
db_landmark *FirstLandmark = LocateFirstLandmark(A);
//Colourise(CS_BLACK_BOLD); fprintf(stderr, "%4u ", 0); Colourise(CS_END);
//PrintLandmark(FirstLandmark);
for(int i = 0; i < A->LandmarkCount; ++i)
{
db_landmark *This = FirstLandmark + i;
if((i % 8) == 0)
{
fprintf(stderr, "\n");
}
else
{
PrintC(CS_BLACK_BOLD, "");
}
Colourise(CS_BLACK_BOLD); fprintf(stderr, "%4u ", i); Colourise(CS_END);
PrintLandmark(This);
}
fprintf(stderr, "\n");
}
// TODO(matt): Almost definitely redo this using Locate*() functions...
void
SnipeChecksumAndCloseFile(file *HTMLFile, db_asset *Asset, int LandmarksInFile, buffer *Checksum, int64_t *RunningLandmarkIndex)
{
db_landmark *FirstLandmark = LocateFirstLandmark(Asset);
for(int j = 0; j < LandmarksInFile; ++j, ++*RunningLandmarkIndex)
{
db_landmark *Landmark = FirstLandmark + *RunningLandmarkIndex;
HTMLFile->Buffer.Ptr = HTMLFile->Buffer.Location + Landmark->Position;
CopyBufferSized(&HTMLFile->Buffer, Checksum, Checksum->Ptr - Checksum->Location);
}
HTMLFile->Handle = fopen(HTMLFile->Path, "w");
fwrite(HTMLFile->Buffer.Location, HTMLFile->Buffer.Size, 1, HTMLFile->Handle);
fclose(HTMLFile->Handle);
HTMLFile->Handle = 0;
FreeFile(HTMLFile);
}
// TODO(matt): Bounds-check the Metadata.File.Buffer
void *
SkipAsset(db_asset *Asset)
{
char *Ptr = (char *)Asset;
Ptr += sizeof(db_asset) + sizeof(db_landmark) * Asset->LandmarkCount;
return Ptr;
}
void *
SkipAssetsBlock(db_block_assets *Block)
{
char *Ptr = (char *)Block;
Ptr += sizeof(db_block_assets);
for(int i = 0; i < Block->Count; ++i)
{
db_asset *Asset = (db_asset *)Ptr;
Ptr = SkipAsset(Asset);
}
return Ptr;
}
typedef struct
{
uint64_t CurrentGeneration;
uint64_t Count;
uint32_t *EntriesInGeneration;
} project_generations;
void
PushGeneration(project_generations *G)
{
G->EntriesInGeneration = Fit(G->EntriesInGeneration, sizeof(*G->EntriesInGeneration), G->Count, 4, TRUE);
++G->Count;
}
db_project_index
GetCurrentProjectIndex(project_generations *G)
{
db_project_index Result = {};
Result.Generation = G->CurrentGeneration;
Result.Index = G->EntriesInGeneration[G->CurrentGeneration];
return Result;
}
void
AddEntryToGeneration(project_generations *G, project *P)
{
if(G)
{
if(G->Count <= G->CurrentGeneration)
{
PushGeneration(G);
}
if(P)
{
P->Index = GetCurrentProjectIndex(G);
}
++G->EntriesInGeneration[G->CurrentGeneration];
}
}
void
IncrementCurrentGeneration(project_generations *G)
{
if(G) { ++G->CurrentGeneration; }
}
void
DecrementCurrentGeneration(project_generations *G)
{
if(G) { --G->CurrentGeneration; }
}
void
PrintGenerations(project_generations *G, bool IndicateCurrentGeneration)
{
for(uint64_t i = 0; i < G->Count; ++i)
{
fprintf(stderr, "%lu: %u%s\n", i, G->EntriesInGeneration[i], IndicateCurrentGeneration && i == G->CurrentGeneration ? " [Current Generation]" : "");
}
}
void
FreeGenerations(project_generations *G)
{
FreeAndResetCount(G->EntriesInGeneration, G->Count);
}
void *
SkipProject(db_header_project *Project)
{
char *Ptr = (char *)Project;
Ptr += sizeof(db_header_project) + sizeof(db_entry) * Project->EntryCount;
return Ptr;
}
void *
SkipProjectAndChildren(db_header_project *Project)
{
db_header_project *Ptr = SkipProject(Project);
for(int i = 0; i < Project->ChildCount; ++i)
{
Ptr = SkipProjectAndChildren(Ptr);
}
return Ptr;
}
void *
SkipProjectsBlock(db_block_projects *Block)
{
char *Ptr = (char *)Block;
Ptr += sizeof(db_block_projects);
for(int i = 0; i < Block->Count; ++i)
{
db_header_project *Project = (db_header_project *)Ptr;
Ptr = SkipProjectAndChildren(Project);
}
return Ptr;
}
typedef enum
{
B_ASET,
B_PROJ,
} block_id;
uint32_t
GetFourFromBlockID(block_id ID)
{
switch(ID)
{
case B_ASET: return FOURCC("ASET");
case B_PROJ: return FOURCC("PROJ");
}
return 0;
}
void *
SkipBlock(void *Block)
{
void *Result = 0;
uint32_t FirstInt = *(uint32_t *)Block;
if(FirstInt == GetFourFromBlockID(B_PROJ))
{
Result = SkipProjectsBlock(Block);
}
else if(FirstInt == GetFourFromBlockID(B_ASET))
{
Result = SkipAssetsBlock(Block);
}
Assert(Result);
return Result;
}
void *
LocateBlock(block_id BlockID)
{
void *Result = 0;
db_header *Header = (db_header *)DB.Metadata.File.Buffer.Location;
char *Ptr = (char *)Header;
Ptr += sizeof(db_header);
for(int i = 0; i < Header->BlockCount; ++i)
{
uint32_t FirstInt = *(uint32_t *)Ptr;
if(FirstInt == GetFourFromBlockID(BlockID))
{
Result = Ptr;
break;
}
else
{
Ptr = SkipBlock(Ptr);
}
}
return Result;
}
db_header_project *
LocateFirstChildProjectOfBlock(db_block_projects *P)
{
char *Ptr = (char *)P;
Ptr += sizeof(*P);
return (db_header_project *)Ptr;
}
db_header_project *
LocateFirstChildProject(db_header_project *P)
{
char *Ptr = (char *)P;
Ptr += sizeof(*P) + sizeof(db_entry) * P->EntryCount;
return (db_header_project *)Ptr;
}
// TODO(matt): Test this project index accumulation stuff, for goodness' sake!
db_header_project *
LocateProjectRecursively(db_header_project *Header, db_project_index *DesiredProject, db_project_index *Accumulator)
{
db_header_project *Child = LocateFirstChildProject(Header);
for(int i = 0; i < Header->ChildCount; ++i)
{
if(Accumulator->Generation == DesiredProject->Generation)
{
if(Accumulator->Index == DesiredProject->Index)
{
return Child;
}
else
{
++Accumulator->Index;
}
}
++Accumulator->Generation;
db_header_project *Test = LocateProjectRecursively(Child, DesiredProject, Accumulator);
if(Test)
{
return Test;
}
--Accumulator->Generation;
Child = SkipProjectAndChildren(Child);
}
return 0;
}
db_header_project *
LocateProject(db_project_index Project)
{
if(Project.Generation != -1)
{
db_block_projects *ProjectsBlock = DB.Metadata.Signposts.ProjectsBlock.Ptr ? DB.Metadata.Signposts.ProjectsBlock.Ptr : LocateBlock(B_PROJ);
db_header_project *Child = LocateFirstChildProjectOfBlock(ProjectsBlock);
db_project_index Accumulator = {};
if(ProjectsBlock)
{
if(Project.Generation == Accumulator.Generation)
{
for(int i = 0; i < ProjectsBlock->Count; ++i)
{
if(Project.Index == Accumulator.Index)
{
return Child;
}
Child = SkipProjectAndChildren(Child);
++Accumulator.Index;
}
}
else
{
++Accumulator.Generation;
for(int i = 0; i < ProjectsBlock->Count; ++i)
{
db_header_project *Test = LocateProjectRecursively(Child, &Project, &Accumulator);
if(Test)
{
return Test;
}
Child = SkipProjectAndChildren(Child);
}
--Accumulator.Generation;
}
}
}
return 0;
}
db_entry *
LocateFirstEntry(db_header_project *P)
{
char *Ptr = (char *)P;
Ptr += sizeof(db_header_project);
return (db_entry *)Ptr;
}
db_entry *
LocateEntry(db_project_index DBProjectIndex, int32_t EntryIndex)
{
db_header_project *Project = LocateProject(DBProjectIndex);
if(Project && EntryIndex < Project->EntryCount)
{
char *Ptr = (char *)Project;
Ptr += sizeof(db_header_project) + sizeof(db_entry) * EntryIndex;
db_entry *Result = (db_entry *)Ptr;
return Result;
}
return 0;
}
void
SetFileEditPosition(file_signposted *File)
{
File->Signposts.Edit.BytePosition = ftell(File->File.Handle);
File->Signposts.Edit.Size = 0;
}
void
AccumulateFileEditSize(file_signposted *File, int64_t Bytes)
{
File->Signposts.Edit.Size += Bytes;
}
// TODO(matt): Consider enforcing an order of these blocks, basically putting the easy-to-skip ones first...
void *
InitBlock(block_id ID)
{
db_header *Header = (db_header *)DB.Metadata.File.Buffer.Location;
++Header->BlockCount;
DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w");
fwrite(DB.Metadata.File.Buffer.Location, DB.Metadata.File.Buffer.Size, 1, DB.Metadata.File.Handle);
SetFileEditPosition(&DB.Metadata);
switch(ID)
{
case B_ASET:
{
db_block_assets Block = {};
fwrite(&Block, sizeof(Block), 1, DB.Metadata.File.Handle);
AccumulateFileEditSize(&DB.Metadata, sizeof(Block));
break;
}
case B_PROJ:
{
db_block_projects Block = {};
fwrite(&Block, sizeof(Block), 1, DB.Metadata.File.Handle);
AccumulateFileEditSize(&DB.Metadata, sizeof(Block));
break;
}
}
uint32_t BytesIntoFile = DB.Metadata.File.Buffer.Size;
CycleSignpostedFile(&DB.Metadata);
return &DB.Metadata.File.Buffer.Location + BytesIntoFile;
}
db_asset *
LocateFirstAsset(db_block_assets *B)
{
char *Ptr = (char *)B;
Ptr += sizeof(*B);
return (db_asset *)Ptr;
}
#define PrintAssetsBlock(B) PrintAssetsBlock_(B, __LINE__)
void *
PrintAssetsBlock_(db_block_assets *B, int LineNumber)
{
#if 0
fprintf(stderr, "[%d] ", LineNumber);
PrintFunctionName("PrintAssetsBlock()");
#endif
if(!B) { B = LocateBlock(B_ASET); }
PrintC(CS_BLUE_BOLD, "\n"
"\n"
"Assets Block (ASET)");
db_asset *A = LocateFirstAsset(B);
for(uint16_t i = 0; i < B->Count; ++i)
{
PrintAssetAndLandmarks(A, &i);
A = SkipAsset(A);
}
return A;
}
char *
ConstructDirectoryPath(db_header_project *P, string *PageLocation, string *EntryOutput)
{
char *Result = 0;
if(P)
{
ExtendString0(&Result, Wrap0i(P->BaseDir, sizeof(P->BaseDir)));
}
if(PageLocation && PageLocation->Length > 0)
{
if(P)
{
ExtendString0(&Result, Wrap0("/"));
}
ExtendString0(&Result, *PageLocation);
}
if(EntryOutput)
{
ExtendString0(&Result, Wrap0("/"));
ExtendString0(&Result, *EntryOutput);
}
return Result;
}
char *
ConstructHTMLIndexFilePath(db_header_project *P, string *PageLocation, string *EntryOutput)
{
char *Result = ConstructDirectoryPath(P, PageLocation, EntryOutput);
ExtendString0(&Result, Wrap0("/index.html"));
return Result;
}
void
ReadSearchPageIntoBuffer(db_header_project *P, file *File)
{
string SearchLocationL = Wrap0i(P->SearchLocation, sizeof(P->SearchLocation));
File->Path = ConstructHTMLIndexFilePath(P, &SearchLocationL, 0);
ReadFileIntoBuffer(File);
}
void
ReadGlobalSearchPageIntoBuffer(file *File)
{
db_block_projects *ProjectsBlock = DB.Metadata.Signposts.ProjectsBlock.Ptr ? DB.Metadata.Signposts.ProjectsBlock.Ptr : LocateBlock(B_PROJ);
string SearchLocationL = Wrap0i(ProjectsBlock->GlobalSearchDir, sizeof(ProjectsBlock->GlobalSearchDir));
File->Path = ConstructHTMLIndexFilePath(0, &SearchLocationL, 0);
ReadFileIntoBuffer(File);
}
void
ReadPlayerPageIntoBuffer(db_header_project *P, file *File, db_entry *Entry)
{
string EntryOutput = Wrap0i(Entry->OutputLocation, sizeof(Entry->OutputLocation));
string PlayerLocationL = Wrap0i(P->PlayerLocation, sizeof(P->PlayerLocation));
File->Path = ConstructHTMLIndexFilePath(P, &PlayerLocationL, &EntryOutput);
ReadFileIntoBuffer(File);
}
rc
SnipeChecksumIntoHTML(db_asset *Asset, buffer *Checksum)
{
db_landmark *FirstLandmark = LocateFirstLandmark(Asset);
rc Result = RC_SUCCESS;
for(int64_t RunningLandmarkIndex = 0; RunningLandmarkIndex < Asset->LandmarkCount;)
{
db_landmark *Landmark = FirstLandmark + RunningLandmarkIndex;
// TODO(matt): Do the Next vs Current Project check to see whether we even need to do LocateProject()
db_header_project *P = LocateProject(Landmark->Project);
file HTML = {};
if(Landmark->EntryIndex >= 0)
{
db_entry *Entry = LocateEntry(Landmark->Project, Landmark->EntryIndex);
if(Entry)
{
ReadPlayerPageIntoBuffer(P, &HTML, Entry);
}
else
{
PrintC(CS_ERROR, "\nInvalid landmark (aborting update): ");
PrintAsset(Asset, 0);
PrintLandmark(Landmark);
Result = RC_FAILURE;
break;
}
}
else
{
switch(Landmark->EntryIndex)
{
case SP_SEARCH:
{
if(P)
{
ReadSearchPageIntoBuffer(P, &HTML);
}
else
{
ReadGlobalSearchPageIntoBuffer(&HTML);
}
} break;
default:
{
Colourise(CS_RED);
fprintf(stderr, "SnipeChecksumIntoHTML() does not know about special page with index %i\n", Landmark->EntryIndex);
Colourise(CS_END);
Assert(0);
} break;
}
}
landmark_range Range = {};
Range.First = RunningLandmarkIndex;
Range.Length = Asset->LandmarkCount - Range.First;
int Length = GetIndexRangeLength(Asset, Range, RunningLandmarkIndex);
SnipeChecksumAndCloseFile(&HTML, Asset, Length, Checksum, &RunningLandmarkIndex);
}
return Result;
}
void
PrintWatchFile(watch_file *W)
{
fprintf(stderr, "%s", WatchTypeStrings[W->Type]);
fprintf(stderr, "");
if(W->Extension != EXT_NULL)
{
fprintf(stderr, "*");
PrintString(ExtensionStrings[W->Extension]);
}
else
{
PrintString(W->Path);
}
if(W->Project)
{
fprintf(stderr, "");
PrintLineage(W->Project->Lineage, FALSE);
}
if(W->Asset)
{
fprintf(stderr, "");
fprintf(stderr, "%s", AssetTypeNames[W->Asset->Type]);
}
}
void
PrintWatchHandle(watch_handle *W)
{
fprintf(stderr, "%4d", W->Descriptor);
fprintf(stderr, "");
if(StringsDiffer(W->TargetPath, W->WatchedPath))
{
PrintStringC(CS_MAGENTA, W->WatchedPath);
fprintf(stderr, "\n ");
PrintStringC(CS_RED_BOLD, W->TargetPath);
}
else
{
PrintStringC(CS_GREEN_BOLD, W->TargetPath);
}
for(int i = 0; i < W->FileCount; ++i)
{
watch_file *This = W->Files + i;
fprintf(stderr, "\n"
" ");
PrintWatchFile(This);
}
}
#define PrintWatchHandles() PrintWatchHandles_(__LINE__)
void
PrintWatchHandles_(int LineNumber)
{
typography T =
{
.UpperLeftCorner = "",
.UpperLeft = "",
.Horizontal = "",
.UpperRight = "",
.Vertical = "",
.LowerLeftCorner = "",
.LowerLeft = "",
.Margin = " ",
.Delimiter = ": ",
.Separator = "",
};
fprintf(stderr, "\n"
"%s%s%s [%i] PrintWatchHandles()", T.UpperLeftCorner, T.Horizontal, T.UpperRight, LineNumber);
for(int i = 0; i < WatchHandles.Count; ++i)
{
watch_handle *This = WatchHandles.Handles + i;
fprintf(stderr, "\n");
PrintWatchHandle(This);
}
fprintf(stderr, "\n"
"%s%s%s", T.LowerLeftCorner, T.Horizontal, T.UpperRight);
}
bool
IsSymlink(char *Filepath)
{
int File = open(Filepath, O_RDONLY | O_NOFOLLOW);
bool Result = (errno == ELOOP);
close(File);
return Result;
}
void
PushWatchFileUniquely(watch_handle *Handle, string Filepath, extension_id Extension, watch_type Type, project *Project, asset *Asset)
{
bool Required = TRUE;
for(int i = 0; i < Handle->FileCount; ++i)
{
watch_file *This = Handle->Files + i;
if(This->Type == Type)
{
if(Extension != EXT_NULL)
{
if(This->Extension == Extension)
{
Required = FALSE;
break;
}
}
else
{
if(StringsMatch(This->Path, Filepath))
{
Required = FALSE;
EraseCurrentStringFromBook(&WatchHandles.Paths);
break;
}
}
}
}
if(Required)
{
Handle->Files = FitShrinkable(Handle->Files, sizeof(*Handle->Files), Handle->FileCount, &Handle->FileCapacity, 16, TRUE);
watch_file *New = Handle->Files + Handle->FileCount;
if(Extension != EXT_NULL)
{
New->Extension = Extension;
}
else
{
New->Path = Filepath;
}
New->Type = Type;
New->Project = Project;
New->Asset = Asset;
++Handle->FileCount;
}
}
bool
FileExists(string Path)
{
// TODO(matt): Whenever we need to produce a 0-terminated string like this, we may as well do:
// WriteString0InBook() [...whatever stuff...] EraseCurrentStringFromBook()
// NOTE(matt): Stack-string
char Path0[Path.Length + 1];
CopyStringNoFormat(Path0, sizeof(Path0), Path);
bool Result = TRUE;
int File = open(Path0, O_RDONLY);
if(File == -1)
{
Result = FALSE;
}
close(File);
return Result;
}
typedef struct
{
string A;
string B;
} string_2x;
string_2x
WriteFilenameThenTargetPathIntoBook(memory_book *M, string Path, watch_type Type) // NOTE(matt): Follows symlinks
{
// NOTE(matt): The order in which we write the strings into the memory_book is deliberate and permits duplicate TargetPath
// or Filename to be erased by PushWatchHandle() and PushWatchFileUniquely()
string_2x Result = {};
// NOTE(matt): Stack-string
char OriginalPath0[Path.Length + 1];
CopyStringNoFormat(OriginalPath0, sizeof(OriginalPath0), Path);
// NOTE(matt): Stack-string
char ResolvedSymlinkPath[4096] = {};
string FullPath = {};
if(IsSymlink(OriginalPath0))
{
fprintf(stderr, "%s\n", OriginalPath0);
int PathLength = readlink(OriginalPath0, ResolvedSymlinkPath, 4096);
FullPath = Wrap0i(ResolvedSymlinkPath, PathLength);
PrintString(FullPath);
fprintf(stderr, "\n");
}
else
{
FullPath = Path;
}
if(Type == WT_HMML)
{
Result.B = WriteStringInBook(M, FullPath);
}
else
{
Result.A = WriteStringInBook(M, GetFinalComponent(FullPath));
Result.B = WriteStringInBook(M, StripComponentFromPath(FullPath));
}
return Result;
}
string
GetNearestExistingPath(string Path)
{
string Result = Path;
while(!FileExists(Result))
{
Result = StripComponentFromPath(Result);
}
return Result;
}
void
PushWatchHandle(string Path, extension_id Extension, watch_type Type, project *Project, asset *Asset)
{
// NOTE(matt): This function influences RemoveAndFreeAllButFirstWatchFile(). If we change to write different strings into
// the W->Paths memory_book, we must reflect that change over to RemoveAndFreeAllButFirstWatchFile()
// NOTE(matt): Path types:
// WT_ASSET: File, but we seem to strip the file component from the path, leaving the parent directory
// WT_CONFIG: Directory of -c config, then File of existing config files
// WT_HMML: Directory
//
// So do we need to watch Files and Directories differently?
//
// We could surely detect the type of Path we've been passed, whether DIR or FILE
//
// Rationale:
// WT_HMML: We must watch the directory, so that we can actually pick up new .hmml files
// HMML filenames are supposed to end .hmml, so we may use that to make sure we pick out the
// watch handle
// And we point to the Project so that we may switch to the correct CurrentProject
//
// For HMMLDir that don't exist, they may come into existence after the initial Sync.
// How can we handle this? Must we watch the nearest existing ancestor directory, and keep
// progressively swapping that out until we're watching the HMMLDir itself?
//
// Do we augment watch_handle with "NearestAncestorPath", in addition to TargetPath?
// So if the TargetPath doesn't exist, we instead watch the NearestAncestorPath. The all watch
// handles have a valid WatchDescriptor (i.e. not -1).
//
// WT_CONFIG: We must watch the -c directory, to enable us to handle the absence of a config
// But thereafter, we may (and probably should) watch the files directly, as we are doing
// The reason to watch at all is to enable live-reloading
// WT_ASSET: We have a finite set of assets, right? So shouldn't that mean that we could just watch the
// files directly?
//
// We have BuiltinAssets, which we push onto the Assets whether or not the file exists, but we
// only push their watch handle if the file does exist. Maybe any time we create an asset file,
// e.g. cinera_topics.css, or we see an asset file referred to in a template, we try pushing on
// its watch handle.
//
// Q: Why watch at all?
// A: So that we can rehash the file and snipe its new checksum into the html files
// So wouldn't it be worth pointing to the Asset directly, so that we have immediate access to
// this data?
//
// Permitting multiple "types" at the same path?
//
// This could only possibly happen for directories, which excludes WT_ASSET.
// WT_HMML
// We determine it to be a WT_HMML if the filepath ends .hmml
// WT_CONFIG
// The only WT_CONFIG directory is the parent of the -c, whose path we always know
//
//
// Side issue: Why bother storing the whole path, when the Event->name only contains the BaseFilename?
// Is it so we can distinguish between paths when pushing / getting watch handles?
//
// I reckon we store the Target and the Watched paths, both of which must surely be the full thing
//
// Symlinks: What do we watch? The symlink, or the path to which the symlink points?
//
// Okay, I reckon what we do is the following:
//
// watch_handle
// {
// char *TargetDirectory
// char *WatchedDirectory
// int FileCount
// char **Files
// }
//
// The Target and Files remain the same always
// The WatchedDirectory may change according to which directories exist
//
// Watch the target directory
// For each watch_handle, list out the specific files that we're watching
//
// We only need to write anything into the book once
//
// WrittenPath:
// /home/matt/tmp/file
// Filename: file
// TargetPath: /home/matt/tmp
//
// If we write the WrittenPath, then realise that the TargetPath is already in there, we'd inadvertently
// erase the whole WrittenPath
//
// If we write the Filename then the TargetPath, then realise the TargetPath is already there, we'd erase
// the TargetPath just fine. But then if we later realise we have the Filename too, we have no way to erase
// that. While suggests that we should straight up make the memory_book Current be part of a linked list,
// so we could erase multiple things if needed.
//
// But, write the Filename, then the TargetPath:
// TargetPath doesn't already exist
// Filename, couldn't already exist
//
// TargetPath does exist, so may be erased
// Filename
// does exist, so may be erase
// doesn't exist, so must remain
//
// Seems like we really want to "resolve" the whole path somewhere temporary (on the stack), then:
// string Filename = GetFinalComponent(Wrap0(Temp));
// string TargetPath = StripComponentFromPath(Wrap0(Temp));
// WriteStringInBook(Filename);
// WriteStringInBook(TargetPath);
string_2x FilenameAndDir = WriteFilenameThenTargetPathIntoBook(&WatchHandles.Paths, Path, Type);
string Filename = FilenameAndDir.A;
string TargetPath = FilenameAndDir.B;
watch_handle *Watch = 0;
for(int i = 0; i < WatchHandles.Count; ++i)
{
if(StringsMatch(TargetPath, WatchHandles.Handles[i].TargetPath))
{
Watch = WatchHandles.Handles + i;
EraseCurrentStringFromBook(&WatchHandles.Paths);
break;
}
}
if(!Watch)
{
WatchHandles.Handles = FitShrinkable(WatchHandles.Handles, sizeof(*WatchHandles.Handles), WatchHandles.Count, &WatchHandles.Capacity, 8, TRUE);
Watch = WatchHandles.Handles + WatchHandles.Count;
Watch->TargetPath = TargetPath;
Watch->WatchedPath = GetNearestExistingPath(TargetPath);
// NOTE(matt): Stack-string
char WatchablePath0[Watch->WatchedPath.Length + 1];
CopyStringNoFormat(WatchablePath0, sizeof(WatchablePath0), Watch->WatchedPath);
Watch->Descriptor = inotify_add_watch(inotifyInstance, WatchablePath0, WatchHandles.DefaultEventsMask);
++WatchHandles.Count;
//PrintWatchHandles();
}
PushWatchFileUniquely(Watch, Filename, Extension, Type, Project, Asset);
}
bool
DescriptorIsRedundant(int Descriptor)
{
bool Result = TRUE;
for(int i = 0; i < WatchHandles.Count; ++i)
{
watch_handle *This = WatchHandles.Handles + i;
if(Descriptor == This->Descriptor)
{
string WatchablePath = GetNearestExistingPath(This->TargetPath);
if(StringsMatch(This->WatchedPath, WatchablePath))
{
Result = FALSE;
break;
}
}
}
return Result;
}
int
GetExistingWatchDescriptor(string Path)
{
int Result = -1;
for(int i = 0; i < WatchHandles.Count; ++i)
{
watch_handle *This = WatchHandles.Handles + i;
if(StringsMatch(Path, This->WatchedPath))
{
Result = This->Descriptor;
break;
}
}
return Result;
}
void
UpdateWatchHandles(int Descriptor)
{
for(int i = 0; i < WatchHandles.Count; ++i)
{
watch_handle *This = WatchHandles.Handles + i;
if(Descriptor == This->Descriptor)
{
string WatchablePath = GetNearestExistingPath(This->TargetPath);
if(StringsDiffer(This->WatchedPath, WatchablePath))
{
if(DescriptorIsRedundant(Descriptor))
{
inotify_rm_watch(inotifyInstance, This->Descriptor);
}
int NewDescriptor = GetExistingWatchDescriptor(WatchablePath);
if(NewDescriptor == -1)
{
int NullTerminationBytes = 1;
char WatchablePath0[WatchablePath.Length + NullTerminationBytes];
CopyStringNoFormat(WatchablePath0, sizeof(WatchablePath0), WatchablePath);
NewDescriptor = inotify_add_watch(inotifyInstance, WatchablePath0, WatchHandles.DefaultEventsMask);
}
This->Descriptor = NewDescriptor;
This->WatchedPath = WatchablePath;
}
}
}
}
db_asset *
LocateAsset(db_block_assets *Block, asset *Asset, int *Index)
{
db_asset *Result = 0;
*Index = SAI_UNSET;
if(!Block)
{
Block = InitBlock(B_ASET);
}
char *Ptr = (char *)Block;
Ptr += sizeof(*Block);
for(int i = 0; i < Block->Count; ++i)
{
db_asset *StoredAsset = (db_asset *)Ptr;
string FilenameInDB = Wrap0i(StoredAsset->Filename, sizeof(StoredAsset->Filename));
string FilenameInMemory = Wrap0i(Asset->Filename, sizeof(Asset->Filename));
if(StringsMatch(FilenameInDB, FilenameInMemory) && StoredAsset->Type == Asset->Type)
{
*Index = i;
Result = StoredAsset;
break;
}
Ptr = SkipAsset(StoredAsset);
}
return Result;
}
void *
LocateEndOfAssetsBlock(db_block_assets *Block)
{
char *Ptr = (char *)Block;
Ptr += sizeof(*Block);
for(int i = 0; i < Block->Count; ++i)
{
db_asset *Asset = (db_asset *)Ptr;
Ptr += sizeof(*Asset) + sizeof(db_landmark) * Asset->LandmarkCount;
}
return Ptr;
}
void
UpdateNeighbourhoodPointers(neighbourhood *N, file_signposts *S)
{
N->Project = S->ProjectHeader.Ptr;
N->Prev = S->Prev.Ptr;
N->This = S->This.Ptr;
N->Next = S->Next.Ptr;
}
int
UpdateAssetInDB(asset *Asset)
{
int AssetIndexInDB = SAI_UNSET;
if(Config->QueryString.Length > 0)
{
db_block_assets *AssetsBlock = LocateBlock(B_ASET);
if(!AssetsBlock)
{
AssetsBlock = InitBlock(B_ASET);
}
DB.Metadata.Signposts.AssetsBlock.Ptr = AssetsBlock;
db_asset *StoredAsset = LocateAsset(AssetsBlock, Asset, &AssetIndexInDB);
if(StoredAsset)
{
StoredAsset->Associated = Asset->Associated;
StoredAsset->Variants = Asset->Variants;
StoredAsset->Width = Asset->Dimensions.Width;
StoredAsset->Height = Asset->Dimensions.Height;
if(StoredAsset->Hash != Asset->Hash)
{
StoredAsset->Hash = Asset->Hash;
char *Ptr = (char *)Asset;
Ptr += sizeof(*Asset);
buffer Checksum = {};
ClaimBuffer(&Checksum, BID_CHECKSUM, 16);
CopyStringToBuffer(&Checksum, "%08x", StoredAsset->Hash);
file AssetFile = {};
AssetFile.Path = ConstructAssetPath(&AssetFile, Wrap0i(StoredAsset->Filename, sizeof(StoredAsset->Filename)), StoredAsset->Type);
ResolvePath(&AssetFile.Path);
string ChecksumL = Wrap0i(Checksum.Location, Checksum.Ptr - Checksum.Location);
string Message = MakeString("sssslsss",
ColourStrings[CS_ONGOING], "Updating", ColourStrings[CS_END], " checksum ", &ChecksumL, " of ", AssetFile.Path, " in HTML files");
fprintf(stderr, "%.*s", (int)Message.Length, Message.Base);
uint64_t MessageLength = Message.Length;
FreeString(&Message);
if(SnipeChecksumIntoHTML(StoredAsset, &Checksum) == RC_SUCCESS)
{
ClearTerminalRow(MessageLength);
fprintf(stderr, "%sUpdated%s checksum %.*s of %s\n", ColourStrings[CS_REINSERTION], ColourStrings[CS_END], (int)ChecksumL.Length, ChecksumL.Base, AssetFile.Path);
}
DeclaimBuffer(&Checksum);
DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w");
fwrite(DB.Metadata.File.Buffer.Location, DB.Metadata.File.Buffer.Size, 1, DB.Metadata.File.Handle);
SetFileEditPosition(&DB.Metadata);
CycleSignpostedFile(&DB.Metadata);
Asset->DeferredUpdate = FALSE;
}
}
else
{
// Append new asset, not bothering to insertion sort because there likely won't be many
DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w");
char *InsertionPoint = LocateEndOfAssetsBlock(AssetsBlock);
AssetIndexInDB = AssetsBlock->Count;
++AssetsBlock->Count;
uint64_t BytesIntoFile = InsertionPoint - DB.Metadata.File.Buffer.Location;
fwrite(DB.Metadata.File.Buffer.Location, BytesIntoFile, 1, DB.Metadata.File.Handle);
SetFileEditPosition(&DB.Metadata);
db_asset StoredAsset = {};
StoredAsset.Hash = Asset->Hash;
StoredAsset.Type = Asset->Type;
StoredAsset.Variants = Asset->Variants;
StoredAsset.Width = Asset->Dimensions.Width;
StoredAsset.Height = Asset->Dimensions.Height;
StoredAsset.Associated = Asset->Associated;
ClearCopyStringNoFormat(StoredAsset.Filename, sizeof(StoredAsset.Filename), Wrap0i(Asset->Filename, sizeof(Asset->Filename)));
fwrite(&StoredAsset, sizeof(StoredAsset), 1, DB.Metadata.File.Handle);
AccumulateFileEditSize(&DB.Metadata, sizeof(StoredAsset));
printf("%sAppended%s %s asset: %s [%08x]\n", ColourStrings[CS_ADDITION], ColourStrings[CS_END], AssetTypeNames[StoredAsset.Type], StoredAsset.Filename, StoredAsset.Hash);
fwrite(DB.Metadata.File.Buffer.Location + BytesIntoFile, DB.Metadata.File.Buffer.Size - BytesIntoFile, 1, DB.Metadata.File.Handle);
CycleSignpostedFile(&DB.Metadata);
}
if(!Asset->Known)
{
Asset->Known = TRUE;
}
}
return AssetIndexInDB;
}
void
PushAssetLandmark(buffer *Dest, asset *Asset, int PageType, bool GrowableBuffer)
{
if(Config->QueryString.Length > 0)
{
if(GrowableBuffer)
{
AppendStringToBuffer(Dest, Wrap0("?"));
AppendStringToBuffer(Dest, Config->QueryString);
AppendStringToBuffer(Dest, Wrap0("="));
}
else
{
CopyStringToBuffer(Dest, "?%.*s=", (int)Config->QueryString.Length, Config->QueryString.Base);
}
if(PageType == PAGE_PLAYER)
{
Asset->Player = Fit(Asset->Player, sizeof(*Asset->Player), Asset->PlayerLandmarkCount, 8, TRUE);
Asset->Player[Asset->PlayerLandmarkCount].Offset = Dest->Ptr - Dest->Location;
Asset->Player[Asset->PlayerLandmarkCount].BufferID = Dest->ID;
++Asset->PlayerLandmarkCount;
}
else
{
Asset->Search = Fit(Asset->Search, sizeof(*Asset->Search), Asset->SearchLandmarkCount, 8, TRUE);
Asset->Search[Asset->SearchLandmarkCount].Offset = Dest->Ptr - Dest->Location;
Asset->Search[Asset->SearchLandmarkCount].BufferID = Dest->ID;
++Asset->SearchLandmarkCount;
}
if(GrowableBuffer)
{
// NOTE(matt): Stack-string
char Hash[16] = {};
CopyString(Hash, sizeof(Hash), "%08x", Asset->Hash);
AppendStringToBuffer(Dest, Wrap0(Hash));
}
else
{
CopyStringToBuffer(Dest, "%08x", Asset->Hash);
}
}
}
void
ResetAssetLandmarks(void)
{
for(int AssetIndex = 0; AssetIndex < Assets.Count; ++AssetIndex)
{
asset *A = GetPlaceInBook(&Assets.Asset, AssetIndex);
FreeAndResetCount(A->Player, A->PlayerLandmarkCount);
FreeAndResetCount(A->Search, A->SearchLandmarkCount);
A->OffsetLandmarks = FALSE;
}
}
vec2
GetImageDimensions(buffer *B)
{
vec2 Result = {};
stbi_info_from_memory(&*(stbi_uc *)B->Location, B->Size, (int *)&Result.Width, (int *)&Result.Height, 0);
return Result;
}
int32_t
UpdateAsset(asset *Asset, bool Defer)
{
int32_t AssetIndexInDB = SAI_UNSET;
file File = {};
File.Path = ConstructAssetPath(&File, Wrap0i(Asset->Filename, sizeof(Asset->Filename)), Asset->Type);
ReadFileIntoBuffer(&File);
if(File.Buffer.Location)
{
Asset->Hash = StringToFletcher32(File.Buffer.Location, File.Buffer.Size);
if(Asset->Type == ASSET_IMG)
{
Asset->Dimensions = GetImageDimensions(&File.Buffer);
}
if(!Defer)
{
AssetIndexInDB = UpdateAssetInDB(Asset);
}
else
{
Asset->DeferredUpdate = TRUE;
}
}
else if(Asset->Associated)
{
AssetIndexInDB = UpdateAssetInDB(Asset);
}
FreeFile(&File);
return AssetIndexInDB;
}
//typedef struct
//{
// vec2 TileDim;
// int32_t XLight;
// int32_t XDark;
// int32_t YNormal;
// int32_t YFocused;
// int32_t YDisabled;
//} sprite;
void
ComputeSpriteData(asset *A)
{
if(A->Variants)
{
sprite *S = &A->Sprite;
bool HasLight = (A->Variants & (1 << AVS_LIGHT_NORMAL) || A->Variants & (1 << AVS_LIGHT_FOCUSED) || A->Variants & (1 << AVS_LIGHT_DISABLED));
bool HasDark = (A->Variants & (1 << AVS_DARK_NORMAL) || A->Variants & (1 << AVS_DARK_FOCUSED) || A->Variants & (1 << AVS_DARK_DISABLED));
int TileCountX = HasLight + HasDark;
bool HasNormal = (A->Variants & (1 << AVS_LIGHT_NORMAL) || A->Variants & (1 << AVS_DARK_NORMAL));
bool HasFocused = (A->Variants & (1 << AVS_LIGHT_FOCUSED) || A->Variants & (1 << AVS_DARK_FOCUSED));
bool HasDisabled = (A->Variants & (1 << AVS_LIGHT_DISABLED) || A->Variants & (1 << AVS_DARK_DISABLED));
int TileCountY = HasNormal + HasFocused + HasDisabled;
S->TileDim.Width = A->Dimensions.Width / TileCountX;
S->TileDim.Height = A->Dimensions.Height / TileCountY;
S->XLight = 0;
S->XDark = 0;
if(HasDark) { S->XDark += S->TileDim.Width * HasLight; }
A->Sprite.YNormal = 0;
A->Sprite.YFocused = 0;
A->Sprite.YDisabled = 0;
if(HasFocused) { S->YFocused -= S->TileDim.Height * HasNormal; }
if(HasDisabled) { S->YDisabled -= S->TileDim.Height * (HasNormal + HasFocused); }
if(!HasNormal && HasDisabled) { S->YNormal = S->YDisabled; }
}
}
asset *
PlaceAsset(string Filename, asset_type Type, uint64_t Variants, bool Associated, int Position)
{
asset *This = GetPlaceInBook(&Assets.Asset, Position);
This->Type = Type;
This->Variants = Variants;
This->Associated = Associated;
ClearCopyString(This->Filename, sizeof(This->Filename), "%.*s", (int)Filename.Length, Filename.Base);
This->FilenameAt = FinalPathComponentPosition(Filename);
if(Position == Assets.Count && !This->Known) { ++Assets.Count; }
file File = {};
File.Path = ConstructAssetPath(&File, Filename, Type);
ReadFileIntoBuffer(&File);
if(File.Buffer.Location)
{
This->Hash = StringToFletcher32(File.Buffer.Location, File.Buffer.Size);
if(This->Type == ASSET_IMG)
{
This->Dimensions = GetImageDimensions(&File.Buffer);
ComputeSpriteData(This);
}
PushWatchHandle(Wrap0(File.Path), EXT_NULL, WT_ASSET, 0, This);
}
else
{
ResolvePath(&File.Path);
printf("%sNonexistent%s %s asset: %s\n", ColourStrings[CS_WARNING], ColourStrings[CS_END], AssetTypeNames[Type], File.Path);
}
FreeFile(&File);
return This;
}
asset *
PushAsset(string Filename, asset_type Type, uint64_t Variants, bool Associated)
{
int i;
for(i = 0; i < Assets.Count; ++i)
{
asset *Asset = GetPlaceInBook(&Assets.Asset, i);
if(!StringsDifferLv0(Filename, Asset->Filename) && Type == Asset->Type)
{
break;
}
}
return PlaceAsset(Filename, Type, Variants, Associated, i);
}
void
FreeAssets(assets *A)
{
for(int i = 0; i < A->Count; ++i)
{
asset *Asset = GetPlaceInBook(&Assets.Asset, i);
FreeAndResetCount(Asset->Search, Asset->SearchLandmarkCount);
FreeAndResetCount(Asset->Player, Asset->PlayerLandmarkCount);
}
FreeBook(&A->Asset);
A->Count = 0;
}
void
InitBuiltinAssets(void)
{
// NOTE(matt): This places assets in their own known slots, letting people like GenerateTopicColours() index them directly
Assert(BUILTIN_ASSETS_COUNT == ArrayCount(BuiltinAssets));
for(int AssetIndex = 0; AssetIndex < BUILTIN_ASSETS_COUNT; ++AssetIndex)
{
asset *This = BuiltinAssets + AssetIndex;
if(!(PlaceAsset(Wrap0(This->Filename), This->Type, This->Variants, This->Associated, AssetIndex)) && AssetIndex == ASSET_CSS_TOPICS)
{
printf( " %s└───────┴────┴─── Don't worry about this one. We'll generate it if needed%s\n", ColourStrings[CS_COMMENT], ColourStrings[CS_END]);
}
}
Assets.Count = BUILTIN_ASSETS_COUNT;
// TODO(matt): Think deeply about how and when we push these support icon assets on
#if 0
Assert(SUPPORT_ICON_COUNT - BUILTIN_ASSETS_COUNT == ArrayCount(SupportIcons));
for(int SupportIconIndex = BUILTIN_ASSETS_COUNT; SupportIconIndex < SUPPORT_ICON_COUNT; ++SupportIconIndex)
{
PlaceAsset(Wrap0(SupportIcons[SupportIconIndex - BUILTIN_ASSETS_COUNT]), ASSET_IMG, SupportIconIndex);
}
#endif
}
void
InitAssets(void)
{
InitBook(&Assets.Asset, sizeof(asset), 16, MBT_ASSET);
InitBuiltinAssets();
db_block_assets *AssetsBlock = LocateBlock(B_ASET);
if(AssetsBlock)
{
DB.Metadata.Signposts.AssetsBlock.Ptr = AssetsBlock;
db_asset *Asset = LocateFirstAsset(AssetsBlock);
for(int i = 0; i < AssetsBlock->Count; ++i)
{
PushAsset(Wrap0i(Asset->Filename, sizeof(Asset->Filename)), Asset->Type, Asset->Variants, Asset->Associated);
Asset = SkipAsset(Asset);
}
}
}
void
ConstructResolvedAssetURL(buffer *Buffer, asset *Asset, enum8(pages) PageType)
{
ClaimBuffer(Buffer, BID_URL_ASSET, (MAX_ROOT_URL_LENGTH + 1 + MAX_RELATIVE_ASSET_LOCATION_LENGTH + 1) * 2);
ConstructURLPrefix(Buffer, Asset->Type, PageType);
CopyStringToBufferHTMLPercentEncoded(Buffer, Wrap0i(Asset->Filename, sizeof(Asset->Filename)));
string BufferL = {};
BufferL.Base = Buffer->Location;
BufferL.Length = Buffer->Ptr - Buffer->Location;
char *ResolvablePath = MakeString0("l", &BufferL);
ResolvePath(&ResolvablePath);
RewindBuffer(Buffer);
CopyStringToBuffer(Buffer, "%s", ResolvablePath);
Free(ResolvablePath);
}
void
ClearNullTerminatedString(char *String)
{
while(*String)
{
*String++ = '\0';
}
}
char *
InitialString(char *Dest, string Src)
{
Free(Dest);
string Char = Wrap0i(Src.Base, 1);
ExtendString0(&Dest, Char);
for(int i = 1; i < Src.Length; ++i)
{
if(Src.Base[i] == ' ' && i < Src.Length)
{
++i;
Char = Wrap0i(Src.Base + i, 1);
ExtendString0(&Dest, Char);
}
}
return Dest;
}
char *
GetFirstSubstring(char *Dest, string Src)
{
Free(Dest);
string Substring = {};
Substring.Base = Src.Base;
for(int i = 0; i < Src.Length; ++i, ++Substring.Length)
{
if(Src.Base[i] == ' ')
{
ExtendString0(&Dest, Substring);
return Dest;
}
}
ExtendString0(&Dest, Substring);
return Dest;
}
char *
InitialAndGetFinalString(char *Dest, string Src)
{
Free(Dest);
int FinalStringBase;
for(FinalStringBase = Src.Length; FinalStringBase > 0; --FinalStringBase)
{
if(Src.Base[FinalStringBase - 1] == ' ') { break; }
}
if(FinalStringBase > 0 && Src.Base[FinalStringBase] == ' ' && FinalStringBase < Src.Length)
{
++FinalStringBase;
}
string FinalString = Wrap0i(Src.Base + FinalStringBase, Src.Length - FinalStringBase);
if(FinalStringBase > 0)
{
string Initial = Wrap0i(Src.Base, 1);
ExtendString0(&Dest, Initial);
ExtendString0(&Dest, Wrap0(". "));
for(int i = 0; i < FinalStringBase; ++i)
{
if(Src.Base[i] == ' ' && i + 1 < FinalStringBase)
{
++i;
string Initial = Wrap0i(Src.Base + i, 1);
ExtendString0(&Dest, Initial);
ExtendString0(&Dest, Wrap0(". "));
}
}
}
ExtendString0(&Dest, FinalString);
return Dest;
}
bool
AbbreviationsClash(speakers *Speakers)
{
for(int i = 0; i < Speakers->Count; ++i)
{
for(int j = i + 1; j < Speakers->Count; ++j)
{
if(!StringsDiffer0(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[j].Abbreviation))
{
return TRUE;
}
}
}
return FALSE;
}
void
SortAndAbbreviateSpeakers(speakers *Speakers)
{
// TODO(matt): Handle Abbreviation in its new form as a char *, rather than a fixed-sized char[], so probably doing
// MakeString0() or ExpandString0() or something
for(int i = 0; i < Speakers->Count; ++i)
{
for(int j = i + 1; j < Speakers->Count; ++j)
{
if(StringsDiffer(Speakers->Speaker[i].Person->ID, Speakers->Speaker[j].Person->ID) > 0)
{
person *Temp = Speakers->Speaker[j].Person;
Speakers->Speaker[j].Person = Speakers->Speaker[i].Person;
Speakers->Speaker[i].Person = Temp;
break;
}
}
}
for(int i = 0; i < Speakers->Count; ++i)
{
StringToColourHash(&Speakers->Speaker[i].Colour, Speakers->Speaker[i].Person->ID);
Speakers->Speaker[i].Abbreviation = InitialString(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Person->Name);
}
int Attempt = 0;
while(AbbreviationsClash(Speakers))
{
for(int i = 0; i < Speakers->Count; ++i)
{
switch(Attempt)
{
case 0: Speakers->Speaker[i].Abbreviation = GetFirstSubstring(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Person->Name); break;
case 1: Speakers->Speaker[i].Abbreviation = InitialAndGetFinalString(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Person->Name); break;
case 2: Free(Speakers->Speaker[i].Abbreviation); ExtendString0(&Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Person->Name); break;
}
}
++Attempt;
}
}
person *
GetPersonFromConfig(string Person)
{
for(int i = 0; i < Config->PersonCount; ++i)
{
if(!StringsDifferCaseInsensitive(Config->Person[i].ID, Person))
{
return &Config->Person[i];
}
}
return 0;
}
asset *
GetAsset(string Filename, asset_type AssetType)
{
asset *Result = 0;
for(int i = 0; i < Assets.Count; ++i)
{
asset *This = GetPlaceInBook(&Assets.Asset, i);
if(StringsMatch(Filename, Wrap0i(This->Filename, sizeof(This->Filename)))
&& AssetType == This->Type)
{
Result = This;
break;
}
}
return Result;
}
void
PushSupportIconAssets()
{
for(int i = 0; i < Config->PersonCount; ++i)
{
person *Person = Config->Person + i;
for(int j = 0; j < Person->SupportCount; ++j)
{
support *Support = Person->Support + j;
if(Support->IconType == IT_GRAPHICAL)
{
asset *IconAsset = GetAsset(Support->Icon, ASSET_IMG);
if(!IconAsset)
{
IconAsset = PushAsset(Support->Icon, ASSET_IMG, Support->IconVariants, FALSE);
}
Support->IconAsset = IconAsset;
}
}
}
}
typedef enum
{
AI_PROJECT_ART,
AI_PROJECT_ICON,
AI_ENTRY_ART,
} associable_identifier;
void *
ConfirmAssociationsOfProject(db_header_project *Project, asset *Asset, db_asset *AssetInDB, int Index)
{
if(Project->ArtIndex == Index)
{
Asset->Associated = TRUE;
AssetInDB->Associated = TRUE;
}
if(Project->IconIndex == Index)
{
Asset->Associated = TRUE;
AssetInDB->Associated = TRUE;
}
db_entry *Entry = LocateFirstEntry(Project);
for(int j = 0; j < Project->EntryCount; ++j, ++Entry)
{
if(Entry->ArtIndex == Index)
{
Asset->Associated = TRUE;
AssetInDB->Associated = TRUE;
}
}
db_header_project *Child = LocateFirstChildProject(Project);
for(int ChildIndex = 0; ChildIndex < Project->ChildCount; ++ChildIndex)
{
Child = ConfirmAssociationsOfProject(Child, Asset, AssetInDB, Index);
}
return SkipProjectAndChildren(Project);
}
void
ConfirmAssociations(asset *Asset, db_asset *AssetInDB, int Index)
{
Asset->Associated = FALSE;
AssetInDB->Associated = FALSE;
db_block_projects *ProjectsBlock = LocateBlock(B_PROJ);
db_header_project *Project = LocateFirstChildProjectOfBlock(ProjectsBlock);
for(int i = 0; i < ProjectsBlock->Count; ++i)
{
Project = ConfirmAssociationsOfProject(Project, Asset, AssetInDB, Index);
}
}
db_asset *
LocateAssetByIndex(uint16_t Index)
{
db_asset *Result = 0;
db_block_assets *AssetsBlock = LocateBlock(B_ASET);
db_asset *This = LocateFirstAsset(AssetsBlock);
for(int i = 0; i < AssetsBlock->Count; ++i)
{
if(Index == i)
{
Result = This;
break;
}
This = SkipAsset(This);
}
return Result;
}
asset *
SyncAssetAssociation(string Path, uint64_t Variants, db_project_index Index, associable_identifier Type)
{
asset *Asset = GetAsset(Path, ASSET_IMG);
if(!Asset)
{
Asset = PushAsset(Path, ASSET_IMG, Variants, TRUE);
}
Asset->Associated = TRUE;
DB.Metadata.Signposts.ProjectHeader.Ptr = LocateProject(Index);
int NewIndex = UpdateAsset(Asset, FALSE);
db_header_project *ProjectInDB = DB.Metadata.Signposts.ProjectHeader.Ptr;
int32_t *StoredIndex = 0;
switch(Type)
{
case AI_PROJECT_ART: { StoredIndex = &ProjectInDB->ArtIndex; } break;
case AI_PROJECT_ICON: { StoredIndex = &ProjectInDB->IconIndex; } break;
default: Assert(0); break;
}
int OldIndex = *StoredIndex;
*StoredIndex = NewIndex;
if(OldIndex >= 0 && OldIndex != NewIndex)
{
db_asset *OldAssetInDB = LocateAssetByIndex(OldIndex);
asset *OldAsset = GetAsset(Wrap0i(OldAssetInDB->Filename, sizeof(OldAssetInDB->Filename)), OldAssetInDB->Type);
ConfirmAssociations(OldAsset, OldAssetInDB, OldIndex);
//DeleteStaleAssets(); // TODO(matt): Maybe do this later?
}
return Asset;
}
void
PushMediumIconAsset(medium *Medium)
{
if(Medium->IconType == IT_GRAPHICAL)
{
asset *Asset = GetAsset(Medium->Icon, ASSET_IMG);
if(!Asset)
{
Asset = PushAsset(Medium->Icon, ASSET_IMG, Medium->IconVariants, FALSE);
}
Medium->IconAsset = Asset;
}
}
void
PushProjectAssets(project *P)
{
string Theme = MakeString("sls", "cinera__", &P->Theme, ".css");
if(!GetAsset(Theme, ASSET_CSS))
{
PushAsset(Theme, ASSET_CSS, CAV_DEFAULT_UNSET, FALSE); // NOTE(matt): This may want to be associated, if we change to
// storing the Theme by index, rather than a string
}
if(P->Art.Length)
{
P->ArtAsset = SyncAssetAssociation(P->Art, P->ArtVariants, P->Index, AI_PROJECT_ART);
}
if(P->Icon.Length)
{
P->IconAsset = SyncAssetAssociation(P->Icon, P->IconVariants, P->Index, AI_PROJECT_ICON);
}
for(int i = 0; i < P->MediumCount; ++i)
{
PushMediumIconAsset(P->Medium + i);
}
for(int i = 0; i < P->ChildCount; ++i)
{
PushProjectAssets(&P->Child[i]);
}
FreeString(&Theme);
}
void
PushThemeAssets()
{
string GlobalTheme = MakeString("sls", "cinera__", &Config->GlobalTheme, ".css");
if(!GetAsset(GlobalTheme, ASSET_CSS))
{
PushAsset(GlobalTheme, ASSET_CSS, CAV_DEFAULT_UNSET, FALSE); // NOTE(matt): This may want to be associated if we change to
// storing the Theme by index rather than a string
}
FreeString(&GlobalTheme);
for(int i = 0; i < Config->ProjectCount; ++i)
{
PushProjectAssets(&Config->Project[i]);
}
}
void
PushConfiguredAssets()
{
PushSupportIconAssets();
PushThemeAssets();
}
char *RoleStrings[] =
{
"Cohost",
"Guest",
"Host",
"Indexer",
};
typedef enum
{
R_COHOST,
R_GUEST,
R_HOST,
R_INDEXER,
} role;
void
PushIcon(buffer *Buffer, bool GrowableBuffer, icon_type IconType, string IconString, asset *IconAsset, uint64_t Variants, page_type PageType, bool *RequiresCineraJS)
{
if(IconType == IT_GRAPHICAL)
{
buffer AssetURL = {};
ConstructResolvedAssetURL(&AssetURL, IconAsset, PageType);
if(Variants)
{
sprite *S = &IconAsset->Sprite;
*RequiresCineraJS = TRUE;
if(GrowableBuffer)
{
AppendStringToBuffer(Buffer, Wrap0("<div class=\"cineraSprite\" data-sprite-width="));
AppendUint32ToBuffer(Buffer, IconAsset->Dimensions.Width);
AppendStringToBuffer(Buffer, Wrap0(" data-sprite-height="));
AppendUint32ToBuffer(Buffer, IconAsset->Dimensions.Height);
AppendStringToBuffer(Buffer, Wrap0(" data-tile-width="));
AppendUint32ToBuffer(Buffer, S->TileDim.Width);
AppendStringToBuffer(Buffer, Wrap0(" data-tile-height="));
AppendUint32ToBuffer(Buffer, S->TileDim.Height);
AppendStringToBuffer(Buffer, Wrap0(" data-x-light="));
AppendUint32ToBuffer(Buffer, S->XLight);
AppendStringToBuffer(Buffer, Wrap0(" data-x-dark="));
AppendUint32ToBuffer(Buffer, S->XDark);
AppendStringToBuffer(Buffer, Wrap0(" data-y-normal="));
AppendUint32ToBuffer(Buffer, S->YNormal);
AppendStringToBuffer(Buffer, Wrap0(" data-y-focused="));
AppendUint32ToBuffer(Buffer, S->YFocused);
AppendStringToBuffer(Buffer, Wrap0(" data-y-disabled="));
AppendUint32ToBuffer(Buffer, S->YDisabled);
AppendStringToBuffer(Buffer, Wrap0(" data-src=\""));
AppendStringToBuffer(Buffer, Wrap0(AssetURL.Location));
}
else
{
CopyStringToBuffer(Buffer,
"<div class=\"cineraSprite\" data-sprite-width=%u data-sprite-height=%u data-tile-width=%u data-tile-height=%u data-x-light=%i data-x-dark=%i data-y-normal=%i data-y-focused=%i data-y-disabled=%i data-src=\"%s",
IconAsset->Dimensions.Width,
IconAsset->Dimensions.Height,
S->TileDim.Width,
S->TileDim.Height,
S->XLight,
S->XDark,
S->YNormal,
S->YFocused,
S->YDisabled,
AssetURL.Location);
}
PushAssetLandmark(Buffer, IconAsset, PageType, GrowableBuffer);
if(GrowableBuffer)
{
AppendStringToBuffer(Buffer, Wrap0("\"></div>"));
}
else
{
CopyStringToBuffer(Buffer, "\"></div>");
}
}
else
{
if(GrowableBuffer)
{
AppendStringToBuffer(Buffer, Wrap0("<img src=\""));
AppendStringToBuffer(Buffer, Wrap0(AssetURL.Location));
}
else
{
CopyStringToBuffer(Buffer,
"<img src=\"%s",
AssetURL.Location);
}
PushAssetLandmark(Buffer, IconAsset, PageType, GrowableBuffer);
if(GrowableBuffer)
{
AppendStringToBuffer(Buffer, Wrap0("\">"));
}
else
{
CopyStringToBuffer(Buffer, "\">");
}
}
DeclaimBuffer(&AssetURL);
}
else
{
if(GrowableBuffer)
{
AppendStringToBuffer(Buffer, IconString);
}
else
{
CopyStringToBuffer(Buffer, "%.*s", (int)IconString.Length, IconString.Base);
}
}
}
void
PushCredentials(buffer *CreditsMenu, speakers *Speakers, person *Actor, role Role, bool *RequiresCineraJS)
{
if(Role != R_INDEXER)
{
Speakers->Speaker = Fit(Speakers->Speaker, sizeof(*Speakers->Speaker), Speakers->Count, 4, TRUE);
Speakers->Speaker[Speakers->Count].Person = Actor;
++Speakers->Count;
}
if(CreditsMenu->Ptr == CreditsMenu->Location)
{
CopyStringToBuffer(CreditsMenu,
" <div class=\"menu credits\">\n"
" <span>Credits</span>\n"
" <div class=\"credits_container\">\n");
}
CopyStringToBuffer(CreditsMenu,
" <span class=\"credit\">\n");
if(Actor->Homepage.Length)
{
CopyStringToBuffer(CreditsMenu,
" <a class=\"person\" href=\"%.*s\" target=\"_blank\">\n"
" <div class=\"role\">%s</div>\n"
" <div class=\"name\">%.*s</div>\n"
" </a>\n",
(int)Actor->Homepage.Length, Actor->Homepage.Base,
RoleStrings[Role],
(int)Actor->Name.Length, Actor->Name.Base);
}
else
{
CopyStringToBuffer(CreditsMenu,
" <div class=\"person\">\n"
" <div class=\"role\">%s</div>\n"
" <div class=\"name\">%.*s</div>\n"
" </div>\n",
RoleStrings[Role],
(int)Actor->Name.Length, Actor->Name.Base);
}
// TODO(matt): Handle multiple support platforms!
// AFD
if(Actor->Support)
{
support *Support = Actor->Support;
CopyStringToBuffer(CreditsMenu,
" <a class=\"support\" href=\"%.*s\" target=\"_blank\">",
(int)Support->URL.Length, Support->URL.Base);
PushIcon(CreditsMenu, FALSE, Support->IconType, Support->Icon, Support->IconAsset, Support->IconVariants, PAGE_PLAYER, RequiresCineraJS);
CopyStringToBuffer(CreditsMenu, "</a>\n");
}
CopyStringToBuffer(CreditsMenu,
" </span>\n");
}
void
FreeCredentials(speakers *S)
{
FreeAndResetCount(S->Speaker, S->Count);
}
void
WaitForInput()
{
fprintf(stderr, "Press Enter to continue...\n");
getchar();
}
void
ErrorCredentials(string Actor, role Role)
{
Colourise(CS_ERROR);
fprintf(stderr, "No credentials for %s %.*s\n", RoleStrings[Role], (int)Actor.Length, Actor.Base);
Colourise(CS_END);
fprintf(stderr, "Perhaps you'd like to add a new person to your config file, e.g.:\n"
" person = \"%.*s\"\n"
" {\n"
" name = \"Jane Doe\";\n"
" homepage = \"https://example.com/\";\n"
" support = \"their_support_platform\";\n"
" }\n", (int)Actor.Length, Actor.Base);
WaitForInput();
}
int
BuildCredits(buffer *CreditsMenu, HMML_VideoMetaData *Metadata, speakers *Speakers, bool *RequiresCineraJS)
{
person *Host = GetPersonFromConfig(Wrap0(Metadata->member));
if(Host)
{
PushCredentials(CreditsMenu, Speakers, Host, R_HOST, RequiresCineraJS);
}
else
{
ErrorCredentials(Wrap0(Metadata->member), R_HOST);
return CreditsError_NoCredentials;
}
for(int i = 0; i < Metadata->co_host_count; ++i)
{
person *CoHost = GetPersonFromConfig(Wrap0(Metadata->co_hosts[i]));
if(CoHost)
{
PushCredentials(CreditsMenu, Speakers, CoHost, R_COHOST, RequiresCineraJS);
}
else
{
ErrorCredentials(Wrap0(Metadata->co_hosts[i]), R_COHOST);
return CreditsError_NoCredentials;
}
}
for(int i = 0; i < Metadata->guest_count; ++i)
{
person *Guest = GetPersonFromConfig(Wrap0(Metadata->guests[i]));
if(Guest)
{
PushCredentials(CreditsMenu, Speakers, Guest, R_GUEST, RequiresCineraJS);
}
else
{
ErrorCredentials(Wrap0(Metadata->guests[i]), R_GUEST);
return CreditsError_NoCredentials;
}
}
if(Speakers->Count > 1)
{
SortAndAbbreviateSpeakers(Speakers);
}
if(Metadata->annotator_count > 0)
{
for(int i = 0; i < Metadata->annotator_count; ++i)
{
person *Indexer = GetPersonFromConfig(Wrap0(Metadata->annotators[i]));
if(Indexer)
{
PushCredentials(CreditsMenu, Speakers, Indexer, R_INDEXER, RequiresCineraJS);
}
else
{
ErrorCredentials(Wrap0(Metadata->annotators[i]), R_INDEXER);
return CreditsError_NoCredentials;
}
}
}
else
{
if(CreditsMenu->Ptr > CreditsMenu->Location)
{
CopyStringToBuffer(CreditsMenu,
" </div>\n"
" </div>\n");
}
fprintf(stderr, "Missing \"indexer\" in the [video] node\n");
return CreditsError_NoIndexer;
}
if(CreditsMenu->Ptr > CreditsMenu->Location)
{
CopyStringToBuffer(CreditsMenu,
" </div>\n"
" </div>\n");
}
return RC_SUCCESS;
}
enum
{
REF_SITE = 1 << 0,
REF_PAGE = 1 << 1,
REF_URL = 1 << 2,
REF_TITLE = 1 << 3,
REF_ARTICLE = 1 << 4,
REF_AUTHOR = 1 << 5,
REF_EDITOR = 1 << 6,
REF_PUBLISHER = 1 << 7,
REF_ISBN = 1 << 8,
} reference_fields;
int
BuildReference(ref_info *ReferencesArray, int RefIdentifier, int UniqueRefs, HMML_Reference *Ref, HMML_Annotation *Anno)
{
if(Ref->isbn)
{
CopyString(ReferencesArray[UniqueRefs].ID, sizeof(ReferencesArray[UniqueRefs].ID), "%s", Ref->isbn);
if(!Ref->url) { CopyString(ReferencesArray[UniqueRefs].URL, sizeof(ReferencesArray[UniqueRefs].URL), "https://isbndb.com/book/%s", Ref->isbn); }
else { CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, sizeof(ReferencesArray[UniqueRefs].URL), Wrap0(Ref->url)); }
}
else if(Ref->url)
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].ID, sizeof(ReferencesArray[UniqueRefs].ID), Wrap0(Ref->url));
CopyStringNoFormat(ReferencesArray[UniqueRefs].URL, sizeof(ReferencesArray[UniqueRefs].URL), Wrap0(Ref->url));
}
else { return RC_INVALID_REFERENCE; }
int Mask = 0;
if(Ref->site) { Mask |= REF_SITE; }
if(Ref->page) { Mask |= REF_PAGE; }
if(Ref->title) { Mask |= REF_TITLE; }
if(Ref->article) { Mask |= REF_ARTICLE; }
if(Ref->author) { Mask |= REF_AUTHOR; }
if(Ref->editor) { Mask |= REF_EDITOR; }
if(Ref->publisher) { Mask |= REF_PUBLISHER; }
// TODO(matt): Consider handling the various combinations more flexibly, unless we defer this stuff until we have the
// reference store, in which we could optionally customise the display of each reference entry
switch(Mask)
{
case (REF_TITLE | REF_AUTHOR | REF_PUBLISHER):
{
CopyString(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), "%s (%s)", Ref->author, Ref->publisher);
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Wrap0(Ref->title));
} break;
case (REF_AUTHOR | REF_SITE | REF_PAGE):
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Wrap0(Ref->site));
CopyString(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), "%s: \"%s\"", Ref->author, Ref->page);
} break;
case (REF_PAGE | REF_TITLE):
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Wrap0(Ref->title));
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Wrap0(Ref->page));
} break;
case (REF_SITE | REF_PAGE):
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Wrap0(Ref->site));
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Wrap0(Ref->page));
} break;
case (REF_SITE | REF_TITLE):
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Wrap0(Ref->site));
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Wrap0(Ref->title));
} break;
case (REF_TITLE | REF_AUTHOR):
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Wrap0(Ref->author));
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Wrap0(Ref->title));
} break;
case (REF_ARTICLE | REF_AUTHOR):
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Wrap0(Ref->author));
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Wrap0(Ref->article));
} break;
case (REF_TITLE | REF_PUBLISHER):
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].Source, sizeof(ReferencesArray[UniqueRefs].Source), Wrap0(Ref->publisher));
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Wrap0(Ref->title));
} break;
case REF_TITLE:
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Wrap0(Ref->title));
} break;
case REF_SITE:
{
CopyStringNoFormat(ReferencesArray[UniqueRefs].RefTitle, sizeof(ReferencesArray[UniqueRefs].RefTitle), Wrap0(Ref->site));
} break;
default: return RC_INVALID_REFERENCE; break;
}
CopyString(ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Timecode, sizeof(ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Timecode), "%s", Anno->time);
ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Identifier = RefIdentifier;
return RC_SUCCESS;
}
void
InsertCategory(categories *GlobalTopics, categories *LocalTopics, categories *GlobalMedia, categories *LocalMedia, string Marker)
{
medium *Medium = GetMediumFromProject(CurrentProject, Marker);
if(Medium)
{
int MediumIndex;
for(MediumIndex = 0; MediumIndex < LocalMedia->Count; ++MediumIndex)
{
if(!StringsDifferLv0(Medium->ID, LocalMedia->Category[MediumIndex].Marker))
{
return;
}
if((StringsDifferLv0(Medium->Name, LocalMedia->Category[MediumIndex].WrittenText)) < 0)
{
int CategoryCount;
for(CategoryCount = LocalMedia->Count; CategoryCount > MediumIndex; --CategoryCount)
{
ClearCopyString(LocalMedia->Category[CategoryCount].Marker, sizeof(LocalMedia->Category[CategoryCount].Marker), "%s", LocalMedia->Category[CategoryCount-1].Marker);
ClearCopyString(LocalMedia->Category[CategoryCount].WrittenText, sizeof(LocalMedia->Category[CategoryCount].WrittenText), "%s", LocalMedia->Category[CategoryCount-1].WrittenText);
}
ClearCopyString(LocalMedia->Category[CategoryCount].Marker, sizeof(LocalMedia->Category[CategoryCount].Marker), "%.*s", (int)Medium->ID.Length, Medium->ID.Base);
ClearCopyString(LocalMedia->Category[CategoryCount].WrittenText, sizeof(LocalMedia->Category[CategoryCount].WrittenText), "%.*s", (int)Medium->Name.Length, Medium->Name.Base);
break;
}
}
if(MediumIndex == LocalMedia->Count)
{
CopyString(LocalMedia->Category[MediumIndex].Marker, sizeof(LocalMedia->Category[MediumIndex].Marker), "%.*s", (int)Medium->ID.Length, Medium->ID.Base);
CopyString(LocalMedia->Category[MediumIndex].WrittenText, sizeof(LocalMedia->Category[MediumIndex].WrittenText), "%.*s", (int)Medium->Name.Length, Medium->Name.Base);
}
++LocalMedia->Count;
for(MediumIndex = 0; MediumIndex < GlobalMedia->Count; ++MediumIndex)
{
if(!StringsDifferLv0(Medium->ID, GlobalMedia->Category[MediumIndex].Marker))
{
return;
}
if((StringsDifferLv0(Medium->Name, GlobalMedia->Category[MediumIndex].WrittenText)) < 0)
{
int CategoryCount;
for(CategoryCount = GlobalMedia->Count; CategoryCount > MediumIndex; --CategoryCount)
{
ClearCopyString(GlobalMedia->Category[CategoryCount].Marker, sizeof(GlobalMedia->Category[CategoryCount].Marker), "%s", GlobalMedia->Category[CategoryCount-1].Marker);
ClearCopyString(GlobalMedia->Category[CategoryCount].WrittenText, sizeof(GlobalMedia->Category[CategoryCount].WrittenText), "%s", GlobalMedia->Category[CategoryCount-1].WrittenText);
}
ClearCopyString(GlobalMedia->Category[CategoryCount].Marker, sizeof(GlobalMedia->Category[CategoryCount].Marker), "%.*s", (int)Medium->ID.Length, Medium->ID.Base);
ClearCopyString(GlobalMedia->Category[CategoryCount].WrittenText, sizeof(GlobalMedia->Category[CategoryCount].WrittenText), "%.*s", (int)Medium->Name.Length, Medium->Name.Base);
break;
}
}
if(MediumIndex == GlobalMedia->Count)
{
CopyString(GlobalMedia->Category[MediumIndex].Marker, sizeof(GlobalMedia->Category[MediumIndex].Marker), "%.*s", (int)Medium->ID.Length, Medium->ID.Base);
CopyString(GlobalMedia->Category[MediumIndex].WrittenText, sizeof(GlobalMedia->Category[MediumIndex].WrittenText), "%.*s", (int)Medium->Name.Length, Medium->Name.Base);
}
++GlobalMedia->Count;
}
else
{
int TopicIndex;
for(TopicIndex = 0; TopicIndex < LocalTopics->Count; ++TopicIndex)
{
if(!StringsDifferLv0(Marker, LocalTopics->Category[TopicIndex].Marker))
{
return;
}
if((StringsDifferLv0(Marker, LocalTopics->Category[TopicIndex].Marker)) < 0)
{
int CategoryCount;
for(CategoryCount = LocalTopics->Count; CategoryCount > TopicIndex; --CategoryCount)
{
ClearCopyString(LocalTopics->Category[CategoryCount].Marker, sizeof(LocalTopics->Category[CategoryCount].Marker), "%s", LocalTopics->Category[CategoryCount-1].Marker);
}
ClearCopyString(LocalTopics->Category[CategoryCount].Marker, sizeof(LocalTopics->Category[CategoryCount].Marker), "%.*s", (int)Marker.Length, Marker.Base);
break;
}
}
if(TopicIndex == LocalTopics->Count)
{
CopyString(LocalTopics->Category[TopicIndex].Marker, sizeof(LocalTopics->Category[TopicIndex].Marker), "%.*s", (int)Marker.Length, Marker.Base);
}
++LocalTopics->Count;
for(TopicIndex = 0; TopicIndex < GlobalTopics->Count; ++TopicIndex)
{
if(!StringsDifferLv0(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(((StringsDifferLv0(Marker, GlobalTopics->Category[TopicIndex].Marker)) < 0 || !StringsDiffer0(GlobalTopics->Category[TopicIndex].Marker, "nullTopic")))
{
if(StringsDifferLv0(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)
{
ClearCopyString(GlobalTopics->Category[CategoryCount].Marker, sizeof(GlobalTopics->Category[CategoryCount].Marker), "%s", GlobalTopics->Category[CategoryCount-1].Marker);
}
ClearCopyString(GlobalTopics->Category[CategoryCount].Marker, sizeof(GlobalTopics->Category[CategoryCount].Marker), "%.*s", (int)Marker.Length, Marker.Base);
break;
}
}
}
if(TopicIndex == GlobalTopics->Count)
{
CopyString(GlobalTopics->Category[TopicIndex].Marker, sizeof(GlobalTopics->Category[TopicIndex].Marker), "%.*s", (int)Marker.Length, Marker.Base);
}
++GlobalTopics->Count;
}
}
void
BuildTimestampClass(buffer *TimestampClass, categories *LocalTopics, categories *LocalMedia, string DefaultMedium)
{
if(LocalTopics->Count == 1 && !StringsDiffer0(LocalTopics->Category[0].Marker, "nullTopic"))
{
// NOTE(matt): Stack-string
char SanitisedMarker[StringLength(LocalTopics->Category[0].Marker) + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalTopics->Category[0].Marker);
SanitisePunctuation(SanitisedMarker);
CopyStringToBuffer(TimestampClass, " cat_%s", SanitisedMarker);
}
else
{
for(int i = 0; i < LocalTopics->Count; ++i)
{
// NOTE(matt): Stack-string
char SanitisedMarker[StringLength(LocalTopics->Category[i].Marker) + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalTopics->Category[i].Marker);
SanitisePunctuation(SanitisedMarker);
CopyStringToBuffer(TimestampClass, " cat_%s",
SanitisedMarker);
}
}
if(LocalMedia->Count == 1 && !StringsDifferLv0(DefaultMedium, LocalMedia->Category[0].Marker))
{
// NOTE(matt): Stack-string
char SanitisedMarker[StringLength(LocalMedia->Category[0].Marker) + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalMedia->Category[0].Marker);
SanitisePunctuation(SanitisedMarker);
CopyStringToBuffer(TimestampClass, " %s", SanitisedMarker);
}
else
{
for(int i = 0; i < LocalMedia->Count; ++i)
{
// NOTE(matt): Stack-string
char SanitisedMarker[StringLength(LocalMedia->Category[i].Marker) + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalMedia->Category[i].Marker);
SanitisePunctuation(SanitisedMarker);
medium *Medium = GetMediumFromProject(CurrentProject, Wrap0i(LocalMedia->Category[i].Marker, sizeof(LocalMedia->Category[i].Marker)));
if(Medium)
{
if(Medium->Hidden)
{
CopyStringToBuffer(TimestampClass, " off_%s skip", SanitisedMarker);
}
else
{
CopyStringToBuffer(TimestampClass, " %s", SanitisedMarker);
}
}
}
}
CopyStringToBuffer(TimestampClass, "\"");
}
void
BuildCategoryIcons(buffer *CategoryIcons, categories *LocalTopics, categories *LocalMedia, string DefaultMedium, bool *RequiresCineraJS)
{
bool CategoriesSpan = FALSE;
if(!(LocalTopics->Count == 1 && !StringsDiffer0(LocalTopics->Category[0].Marker, "nullTopic")
&& LocalMedia->Count == 1 && !StringsDifferLv0(DefaultMedium, LocalMedia->Category[0].Marker)))
{
CategoriesSpan = TRUE;
CopyStringToBuffer(CategoryIcons, "<span class=\"cineraCategories\">");
}
if(!(LocalTopics->Count == 1 && !StringsDiffer0(LocalTopics->Category[0].Marker, "nullTopic")))
{
for(int i = 0; i < LocalTopics->Count; ++i)
{
// NOTE(matt): Stack-string
char SanitisedMarker[StringLength(LocalTopics->Category[i].Marker) + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalTopics->Category[i].Marker);
SanitisePunctuation(SanitisedMarker);
CopyStringToBuffer(CategoryIcons, "<div title=\"%s\" class=\"category %s\"></div>",
LocalTopics->Category[i].Marker,
SanitisedMarker);
}
}
if(!(LocalMedia->Count == 1 && !StringsDifferLv0(DefaultMedium, LocalMedia->Category[0].Marker)))
{
for(int i = 0; i < LocalMedia->Count; ++i)
{
// NOTE(matt): Stack-string
char SanitisedMarker[StringLength(LocalMedia->Category[i].Marker) + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", LocalMedia->Category[i].Marker);
SanitisePunctuation(SanitisedMarker);
medium *Medium = GetMediumFromProject(CurrentProject, Wrap0i(LocalMedia->Category[i].Marker, sizeof(LocalMedia->Category[i].Marker)));
if(Medium && !Medium->Hidden)
{
CopyStringToBuffer(CategoryIcons, "<div title=\"%s\" class=\"categoryMedium %s\">", LocalMedia->Category[i].WrittenText, LocalMedia->Category[i].Marker);
PushIcon(CategoryIcons, FALSE, Medium->IconType, Medium->Icon, Medium->IconAsset, Medium->IconVariants, PAGE_PLAYER, RequiresCineraJS);
CopyStringToBuffer(CategoryIcons, "</div>");
}
}
}
if(CategoriesSpan)
{
CopyStringToBuffer(CategoryIcons, "</span>");
}
}
int64_t
String0ToInt(char *String)
{
int64_t 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;
};
CURLcode
CurlQuotes(buffer *QuoteStaging, char *QuotesURL)
{
fprintf(stderr, "%sFetching%s quotes: %s\n", ColourStrings[CS_ONGOING], ColourStrings[CS_END], QuotesURL);
CURLcode Result = CURLE_FAILED_INIT;
CURL *curl = curl_easy_init();
if(curl) {
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &QuoteStaging->Ptr);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlIntoBuffer);
curl_easy_setopt(curl, CURLOPT_URL, QuotesURL);
if((Result = curl_easy_perform(curl)))
{
fprintf(stderr, "%s\n", curl_easy_strerror(Result));
}
curl_easy_cleanup(curl);
}
return Result;
}
string
GetStringFromBufferT(buffer *B, char Terminator)
{
// NOTE(matt): This just straight up assumes success
// We may want to make it report a failure if, e.g. B->Ptr != Terminator after the loop
string Result = { .Base = B->Ptr };
char *Ptr = B->Ptr;
while(Ptr - B->Location < B->Size && *Ptr != Terminator)
{
++Ptr;
++Result.Length;
}
return Result;
}
rc
SearchQuotes(buffer *QuoteStaging, int CacheSize, quote_info *Info, int ID)
{
rc Result = RC_UNFOUND;
QuoteStaging->Ptr = QuoteStaging->Location;
while(QuoteStaging->Ptr - QuoteStaging->Location < CacheSize)
{
string InID = GetStringFromBufferT(QuoteStaging, ',');
QuoteStaging->Ptr += InID.Length + 1; // Skip past the ','
if(StringToInt(InID) == ID)
{
string InTime = GetStringFromBufferT(QuoteStaging, ',');
QuoteStaging->Ptr += InTime.Length + 1; // Skip past the ','
long int Time = StringToInt(InTime);
// NOTE(matt): Stack-string
char DayString[3] = { 0 };
strftime(DayString, 3, "%d", gmtime(&Time));
int Day = String0ToInt(DayString);
// NOTE(matt): Stack-string
char DaySuffix[3]; if(DayString[1] == '1' && Day != 11) { CopyString(DaySuffix, sizeof(DaySuffix), "st"); }
else if(DayString[1] == '2' && Day != 12) { CopyString(DaySuffix, sizeof(DaySuffix), "nd"); }
else if(DayString[1] == '3' && Day != 13) { CopyString(DaySuffix, sizeof(DaySuffix), "rd"); }
else { CopyString(DaySuffix, sizeof(DaySuffix), "th"); }
// NOTE(matt): Stack-string
char MonthYear[32];
strftime(MonthYear, 32, "%B, %Y", gmtime(&Time));
CopyString(Info->Date, sizeof(Info->Date), "%d%s %s", Day, DaySuffix, MonthYear);
CopyStringNoFormatT(Info->Text, sizeof(Info->Text), QuoteStaging->Ptr, '\n');
Result = RC_FOUND;
break;
}
else
{
while(*QuoteStaging->Ptr != '\n')
{
++QuoteStaging->Ptr;
}
++QuoteStaging->Ptr;
}
}
return Result;
}
rc
BuildQuote(quote_info *Info, string Speaker, int ID, bool ShouldFetchQuotes)
{
rc Result = RC_SUCCESS;
// TODO(matt): Generally sanitise this function, e.g. using MakeString0(), curling in to a growing buffer, etc.
// NOTE(matt): Stack-string
char QuoteCacheDir[256] = {};
CopyString(QuoteCacheDir, sizeof(QuoteCacheDir), "%.*s/quotes", (int)Config->CacheDir.Length, Config->CacheDir.Base);
// NOTE(matt): Stack-string
char QuoteCachePath[256] = {};
CopyString(QuoteCachePath, sizeof(QuoteCachePath), "%s/%.*s", QuoteCacheDir, (int)Speaker.Length, Speaker.Base);
FILE *QuoteCache;
// NOTE(matt): Stack-string
char QuotesURL[256] = {};
// TODO(matt): Make the URL configurable and also handle the case in which the .raw isn't available
CopyString(QuotesURL, sizeof(QuotesURL), "https://dev.abaines.me.uk/quotes/%.*s.raw", (int)Speaker.Length, Speaker.Base);
bool CacheAvailable = FALSE;
if(!(QuoteCache = fopen(QuoteCachePath, "a+")))
{
if(MakeDir(Wrap0i(QuoteCacheDir, sizeof(QuoteCacheDir))))
{
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 = BID_QUOTE_STAGING;
QuoteStaging.Size = Kilobytes(256);
if(!(QuoteStaging.Location = malloc(QuoteStaging.Size)))
{
fclose(QuoteCache);
Result = RC_ERROR_MEMORY;
}
if(Result != RC_ERROR_MEMORY)
{
#if DEBUG_MEM
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, " Allocated QuoteStaging (%ld)\n", QuoteStaging.Size);
fclose(MemLog);
printf(" Allocated QuoteStaging (%ld)\n", QuoteStaging.Size);
#endif
QuoteStaging.Ptr = QuoteStaging.Location;
if(CacheAvailable)
{
fseek(QuoteCache, 0, SEEK_END);
int FileSize = ftell(QuoteCache);
fseek(QuoteCache, 0, SEEK_SET);
fread(QuoteStaging.Location, FileSize, 1, QuoteCache);
fclose(QuoteCache);
if(ShouldFetchQuotes || SearchQuotes(&QuoteStaging, FileSize, Info, ID) == RC_UNFOUND)
{
if(CurlQuotes(&QuoteStaging, QuotesURL) == CURLE_OK)
{
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;
Result = SearchQuotes(&QuoteStaging, CacheSize, Info, ID);
}
else
{
Result = RC_UNFOUND;
}
}
}
else
{
if(CurlQuotes(&QuoteStaging, QuotesURL) == CURLE_OK)
{
int CacheSize = QuoteStaging.Ptr - QuoteStaging.Location;
QuoteStaging.Ptr = QuoteStaging.Location;
Result = SearchQuotes(&QuoteStaging, CacheSize, Info, ID);
}
else
{
Result = RC_UNFOUND;
}
}
FreeBuffer(&QuoteStaging);
}
return Result;
}
int
GenerateTopicColours(neighbourhood *N, string Topic)
{
// NOTE(matt): Stack-string
char SanitisedTopic[Topic.Length + 1];
CopyString(SanitisedTopic, sizeof(SanitisedTopic), "%.*s", (int)Topic.Length, Topic.Base);
SanitisePunctuation(SanitisedTopic);
medium *Medium = GetMediumFromProject(CurrentProject, Topic);
if(Medium)
{
return RC_NOOP;
}
file Topics = {};
Topics.Buffer.ID = BID_TOPICS;
if(Config->CSSDir.Length > 0)
{
Topics.Path = MakeString0("lslss", &Config->AssetsRootDir, "/", &Config->CSSDir, "/", BuiltinAssets[ASSET_CSS_TOPICS].Filename);
}
else
{
Topics.Path = MakeString0("lss", &Config->AssetsRootDir, "/", BuiltinAssets[ASSET_CSS_TOPICS].Filename);
}
char *Ptr = Topics.Path + StringLength(Topics.Path) - 1;
while(*Ptr != '/')
{
--Ptr;
}
*Ptr = '\0';
DIR *CSSDirHandle; // TODO(matt): open()
if(!(CSSDirHandle = opendir(Topics.Path)))
{
if(!MakeDir(Wrap0(Topics.Path)))
{
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.Buffer.Size = ftell(Topics.Handle);
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 (%ld)\n", Topics.Buffer.Size);
fclose(MemLog);
printf(" Allocated Topics (%ld)\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(!StringsDifferLv0(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);
#if DEBUG_MEM
MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, " Freed Topics (%ld)\n", Topics.Buffer.Size);
fclose(MemLog);
printf(" Freed Topics (%ld)\n", Topics.Buffer.Size);
#endif
FreeBuffer(&Topics.Buffer);
asset *Asset = GetPlaceInBook(&Assets.Asset, ASSET_CSS_TOPICS);
if(Asset->Known)
{
// NOTE(matt): We may index this out directly because InitBuiltinAssets() places it in its own known slot
UpdateAsset(Asset, TRUE);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
}
else
{
asset *CSSTopics = BuiltinAssets + ASSET_CSS_TOPICS;
PlaceAsset(Wrap0(CSSTopics->Filename), CSSTopics->Type, CSSTopics->Variants, CSSTopics->Associated, ASSET_CSS_TOPICS);
}
return RC_SUCCESS;
}
else
{
// NOTE(matt): Maybe it shouldn't be possible to hit this case now that we MakeDir the actual dir...
perror(Topics.Path);
return RC_ERROR_FILE;
}
}
/*
* NOTE(matt);
*
* Documentation structure:
*
* section
*
* option <argument>
* description
*/
void
ResetConfigIdentifierDescriptionDisplayedBools(void)
{
for(int i = 0; i < IDENT_COUNT; ++i)
{
ConfigIdentifiers[i].IdentifierDescriptionDisplayed = FALSE;
ConfigIdentifiers[i].IdentifierDescription_MediumDisplayed = FALSE;
ConfigIdentifiers[i].LocalVariableDescriptionDisplayed = FALSE;
}
}
void
PrintHelpConfig(void)
{
ResetConfigIdentifierDescriptionDisplayedBools();
// Config Syntax
int IndentationLevel = 0;
NewSection("Configuration", &IndentationLevel);
NewSection("Assigning values", &IndentationLevel);
PrintC(CS_YELLOW_BOLD, "identifier");
fprintf(stderr, " = ");
PrintC(CS_GREEN_BOLD, "\"string\"");
fprintf(stderr, ";");
IndentedCarriageReturn(IndentationLevel);
PrintC(CS_YELLOW_BOLD, "identifier");
fprintf(stderr, " = ");
PrintC(CS_BLUE_BOLD, "number");
fprintf(stderr, ";");
IndentedCarriageReturn(IndentationLevel);
PrintC(CS_YELLOW_BOLD, "identifier");
fprintf(stderr, " = ");
PrintC(CS_GREEN_BOLD, "\"boolean\"");
fprintf(stderr, ";");
++IndentationLevel;
IndentedCarriageReturn(IndentationLevel);
TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0("(valid booleans: true, True, TRUE, yes, false, False, FALSE, no)"));
--IndentationLevel;
IndentedCarriageReturn(IndentationLevel);
PrintC(CS_YELLOW_BOLD, "identifier");
fprintf(stderr, " = ");
PrintC(CS_GREEN_BOLD, "\"scope\"");
fprintf(stderr, " {");
IndentedCarriageReturn(IndentationLevel);
fprintf(stderr, "}\n");
EndSection(&IndentationLevel);
NewSection("Identifiers", &IndentationLevel);
TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0("We may set identifiers singly or multiple times in a given scope, as specified below for each identifier. \
If we set a \"single\" identifier multiple times then each setting overwrites the previous one, and Cinera will warn us. If we set a \"multi\" identifier multiple times \
then all of the settings will take effect."));
NewParagraph(IndentationLevel);
TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0("All settings get \"absorbed\" by child scopes, if that child scope may contain the identifier. For example, if we \
set base_dir at the root scope (i.e. not within a project scope) then the base_dir will automatically be set in all project scopes. Similarly if we set the owner in a project \
scope, then the owner will also be set in all child project scopes. Naturally we may need this setting to vary, while also wanting the concision of writing it once. The \
base_dir is a prime example of this. To facilitate this variance, we may use variables, notably the $lineage variable, as described below. A variable written in a setting at the root \
scope, which is absorbed by a project, only gets resolved as if it had been written in the project scope."));
config_type_specs TypeSpecs = InitTypeSpecs();
PrintTypeSpecs(&TypeSpecs, IndentationLevel);
//FreeTypeSpecs(&TypeSpecs);
EndSection(&IndentationLevel);
fprintf(stderr, "\n");
NewSection("Variables", &IndentationLevel);
TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0("We write variables the same way as in shell scripts like bash, zsh, etc."));
++IndentationLevel;
IndentedCarriageReturn(IndentationLevel);
PrintC(CS_CYAN, "$variable_name");
IndentedCarriageReturn(IndentationLevel);
PrintC(CS_CYAN, "${variable_name_within_valid_variable_characters}");
--IndentationLevel;
IndentedCarriageReturn(IndentationLevel);
NewSection("Config local variables", &IndentationLevel);
for(int i = 0; i < IDENT_COUNT; ++i)
{
if(ConfigIdentifiers[i].LocalVariableDescription && !ConfigIdentifiers[i].LocalVariableDescriptionDisplayed)
{
IndentedCarriageReturn(IndentationLevel);
PrintStringC(CS_YELLOW_BOLD, Wrap0(ConfigIdentifiers[i].String));
++IndentationLevel;
IndentedCarriageReturn(IndentationLevel);
TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0(ConfigIdentifiers[i].LocalVariableDescription));
ConfigIdentifiers[i].LocalVariableDescriptionDisplayed = TRUE;
--IndentationLevel;
}
}
EndSection(&IndentationLevel);
fprintf(stderr, "\n");
NewSection("Environment variables", &IndentationLevel);
TypesetString(INDENT_WIDTH *IndentationLevel, Wrap0("Run `export` to see all available environment variables"));
EndSection(&IndentationLevel);
EndSection(&IndentationLevel); // Variables
fprintf(stderr, "\n");
NewSection("Miscellaneous", &IndentationLevel);
TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0("We may use C-style comments, single-line with // and multi-line with /* and */"));
NewParagraph(IndentationLevel);
TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0("We may edit the config file(s) while Cinera is running, and it will pick up the changes."));
NewParagraph(IndentationLevel);
TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0("Tip: Since the syntax is very C-like, vim users may put the following line somewhere to \
enable syntax highlighting:"));
NewSection(0, &IndentationLevel);
TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0("// vim:ft=c:"));
EndSection(&IndentationLevel);
EndSection(&IndentationLevel);
fprintf(stderr, "\n");
#if 1
// numbering_scheme
// linear
// 1, 2, 3, ...
// calendrical
// 2020-02-07, 2020-03-08, 2020-12-25, ...
// seasonal
// S01E01
// Miscellaneous
//
// Defaults
//
NewSection("Defaults", &IndentationLevel);
scope_tree *ScopeTree = calloc(1, sizeof(scope_tree));
SetTypeSpec(ScopeTree, &TypeSpecs);
SetDefaults(ScopeTree, &TypeSpecs);
PrintScopeTree(ScopeTree, IndentationLevel);
FreeScopeTree(ScopeTree);
FreeTypeSpecs(&TypeSpecs);
#endif
fprintf(stderr, "\n");
}
void
PrintHelp(char *BinaryLocation, char *DefaultConfigPath)
{
// Options
fprintf(stderr,
"Usage: %s [option(s)]\n"
"\n"
"Options:\n"
" -c <config file path>\n"
" Set the main config file path\n"
" Defaults to: %s\n"
" -0\n"
" Dry-run mode. Parse and print the config, but do not modify the filesystem.\n"
" -e\n"
" Display (examine) database and exit\n"
" -v\n"
" Display version and exit\n"
" -h\n"
" Display this help\n", BinaryLocation, DefaultConfigPath);
PrintHelpConfig();
}
void
PrintHelp_(char *BinaryLocation)
{
#if AFE
fprintf(stderr,
"Usage: %s [option(s)] filename(s)\n"
"\n"
"Options:\n"
" Paths: %s(advisedly universal, but may be set per-(sub)project as required)%s\n"
" -r <assets root directory>\n"
" Override default assets root directory (\"%s\")\n"
" -R <assets root URL>\n"
" Override default assets root URL (\"%s\")\n"
" %sIMPORTANT%s: -r and -R must correspond to the same location\n"
" %sUNSUPPORTED: If you move files from RootDir, the RootURL should\n"
" correspond to the resulting location%s\n"
"\n"
" -c <CSS directory path>\n"
" Override default CSS directory (\"%s\"), relative to root\n"
" -i <images directory path>\n"
" Override default images directory (\"%s\"), relative to root\n"
" -j <JS directory path>\n"
" Override default JS directory (\"%s\"), relative to root\n"
" -Q <revved resources query string>\n"
" Override default query string (\"%s\")\n"
" To disable revved resources, set an empty string with -Q \"\"\n"
"\n"
" Project Settings:\n"
" -p <project ID>\n"
" Set the project ID, triggering PROJECT EDITION\n"
" -m <default medium>\n"
" Override default default medium (\"%s\")\n"
" %sKnown project defaults:\n",
BinaryLocation,
ColourStrings[CS_COMMENT], ColourStrings[CS_END], DefaultConfig->RootDir,
DefaultConfig->RootURL,
ColourStrings[CS_ERROR], ColourStrings[CS_END],
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
DefaultConfig->CSSDir,
DefaultConfig->ImagesDir,
DefaultConfig->JSDir,
DefaultConfig->QueryString,
DefaultConfig->DefaultMedium,
ColourStrings[CS_COMMENT]);
for(int ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex)
{
fprintf(stderr, " %s:",
ProjectInfo[ProjectIndex].ProjectID);
// NOTE(matt): This kind of thing really needs to loop over the dudes once to find the longest one
for(int i = 11; i > StringLength(ProjectInfo[ProjectIndex].ProjectID); i -= 4)
{
fprintf(stderr, "\t");
}
fprintf(stderr, "%s\n",
ProjectInfo[ProjectIndex].Medium);
}
fprintf(stderr,
"%s -s <style>\n"
" Set the style / theme, corresponding to a cinera__*.css file\n"
" This is equal to the \"project\" field in the HMML files by default\n"
"\n"
" Project Input Paths\n"
" -d <annotations directory>\n"
" Override default annotations directory (\"%s\")\n"
" -t <templates directory>\n"
" Override default templates directory (\"%s\")\n"
"\n"
" -x <search template location>\n"
" Set search template file path, either absolute or relative to\n"
" template directory, and enable integration\n"
" -y <player template location>\n"
" Set player template file path, either absolute or relative\n"
" to template directory, and enable integration\n"
"\n"
" Project Output Paths\n"
" -b <base output directory>\n"
" Override project's default base output directory (\"%s\")\n"
" -B <base URL>\n"
" Override default base URL (\"%s\")\n"
" NOTE: This must be set, if -n or -a are to be used\n"
"\n"
" -n <search location>\n"
" Override default search location (\"%s\"), relative to base\n"
" -a <player location>\n"
" Override default player location (\"%s\"), relative to base\n"
" NOTE: The PlayerURLPrefix is currently hardcoded in cinera.c but\n"
" will be configurable in the full configuration system\n"
"\n"
" Single Edition Output Path\n"
" -o <output location>\n"
" Override default output player location (\"%s\")\n"
"\n"
" -1\n"
" Open search result links in the same browser tab\n"
" NOTE: Ideal for a guide embedded in an iframe\n"
" -f\n"
" Force integration with an incomplete template\n"
" -g\n"
" Ignore video privacy status\n"
" NOTE: For use with projects whose videos are known to all be public,\n"
" to save us having to check their privacy status\n"
" -q\n"
" Quit after syncing with annotation files in project input directory\n"
" %sUNSUPPORTED: This is likely to be removed in the future%s\n"
" -w\n"
" Force quote cache rebuild %s(memory aid: \"wget\")%s\n"
"\n"
" -l <n>\n"
" Override default log level (%d), where n is from 0 (terse) to 7 (verbose)\n"
" -u <seconds>\n"
" Override default update interval (%d)\n"
//" -c config location\n"
"\n"
" -e\n"
" Display (examine) database and exit\n"
" -v\n"
" Display version and exit\n"
" -h\n"
" Display this help\n"
"\n"
"Template:\n"
" A valid Search Template shall contain exactly one each of the following tags:\n"
" <!-- __CINERA_INCLUDES__ -->\n"
" to put inside your own <head></head>\n"
" <!-- __CINERA_SEARCH__ -->\n"
"\n"
" A valid Player Template shall contain exactly one each of the following tags:\n"
" <!-- __CINERA_INCLUDES__ -->\n"
" to put inside your own <head></head>\n"
" <!-- __CINERA_MENUS__ -->\n"
" <!-- __CINERA_PLAYER__ -->\n"
" <!-- __CINERA_SCRIPT__ -->\n"
" must come after <!-- __CINERA_MENUS__ --> and <!-- __CINERA_PLAYER__ -->\n"
"\n"
" Optional tags available for use in your Player Template:\n"
" <!-- __CINERA_TITLE__ -->\n"
" <!-- __CINERA_VIDEO_ID__ -->\n"
" <!-- __CINERA_VOD_PLATFORM__ -->\n"
"\n"
" Other tags available for use in either template:\n"
" Asset tags:\n"
" <!-- __CINERA_ASSET__ path.ext -->\n"
" General purpose tag that outputs the URL of the specified asset\n"
" relative to the Asset Root URL %s(-R)%s\n"
" <!-- __CINERA_IMAGE__ path.ext -->\n"
" General purpose tag that outputs the URL of the specified asset\n"
" relative to the Images Directory %s(-i)%s\n"
" <!-- __CINERA_CSS__ path.ext -->\n"
" Convenience tag that outputs a <link rel=\"stylesheet\"...> node\n"
" for the specified asset relative to the CSS Directory %s(-c)%s, for\n"
" use inside your <head> block\n"
" <!-- __CINERA_JS__ path.ext -->\n"
" Convenience tag that outputs a <script type=\"text/javascript\"...>\n"
" node for the specified asset relative to the JS Directory %s(-j)%s,\n"
" for use wherever a <script> node is valid\n"
" The path.ext in these tags supports parent directories to locate the\n"
" asset file relative to its specified type directory (generic, CSS, image\n"
" or JS), including the \"../\" directory, and paths containing spaces must\n"
" be surrounded with double-quotes (\\-escapable if the quoted path itself\n"
" contains double-quotes).\n"
"\n"
" All these asset tags additionally enable revving, appending a query\n"
" string %s(-Q)%s and the file's checksum to the URL. Changes to a file\n"
" trigger a rehash and edit of all HTML pages citing this asset.\n"
"\n"
" <!-- __CINERA_PROJECT__ -->\n"
" <!-- __CINERA_PROJECT_ID__ -->\n"
" <!-- __CINERA_SEARCH_URL__ -->\n"
" <!-- __CINERA_THEME__ -->\n"
" <!-- __CINERA_URL__ -->\n"
" Only really usable if BaseURL is set %s(-B)%s\n"
" <!-- __CINERA_CUSTOM0__ -->\n"
" <!-- __CINERA_CUSTOM1__ -->\n"
" <!-- __CINERA_CUSTOM2__ -->\n"
"\n"
" <!-- __CINERA_CUSTOM15__ -->\n"
" Freeform buffers for small snippets of localised information, e.g. a\n"
" single <a> element or perhaps a <!-- comment -->\n"
" They correspond to the custom0 to custom15 attributes in the [video]\n"
" node in your .hmml files\n"
" 0 to 11 may hold up to 255 characters\n"
" 12 to 15 may hold up to 1023 characters\n"
"\n"
"HMML Specification:\n"
" https://git.handmade.network/Annotation-Pushers/Annotation-System/wikis/hmmlspec\n",
ColourStrings[CS_END],
DefaultConfig->HMMLDir,
DefaultConfig->TemplatesDir,
DefaultConfig->BaseDir,
DefaultConfig->BaseURL,
DefaultConfig->SearchLocation,
DefaultConfig->PlayerLocation,
DefaultConfig->OutLocation,
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
DefaultConfig->LogLevel,
DefaultConfig->UpdateInterval,
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
ColourStrings[CS_COMMENT], ColourStrings[CS_END]);
#endif
}
void
DepartComment(buffer *Template)
{
while(Template->Ptr - Template->Location < Template->Size)
{
if(!StringsDifferT("-->", Template->Ptr, 0))
{
Template->Ptr += StringLength("-->");
break;
}
++Template->Ptr;
}
}
char *
StripTrailingSlash(char *String) // NOTE(matt): For absolute paths
{
int Length = StringLength(String);
while(Length > 0 && String[Length - 1] == '/')
{
String[Length - 1] = '\0';
--Length;
}
return String;
}
char *
StripSurroundingSlashes(char *String) // NOTE(matt): For relative paths
{
char *Ptr = String;
int Length = StringLength(Ptr);
if(Length > 0)
{
while((Ptr[0]) == '/')
{
++Ptr;
--Length;
}
while(Ptr[Length - 1] == '/')
{
Ptr[Length - 1] = '\0';
--Length;
}
}
return Ptr;
}
bool
IsWhitespace(char C)
{
return (C == ' ' || C == '\t' || C == '\n');
}
void
ConsumeWhitespace(buffer *B)
{
while(B->Ptr - B->Location < B->Size && IsWhitespace(*B->Ptr))
{
++B->Ptr;
}
}
string
StripPWDIndicators(string Path)
{
// NOTE(matt): Must modify the string content, so performs MakeString() and returns an allocated string
//
// Please call FreeString() afterwards
buffer B = {};
ClaimBuffer(&B, BID_PWD_STRIPPED_PATH, (Path.Length + 1) * 2);
CopyStringToBufferNoFormat(&B, Path);
B.Ptr = B.Location;
if(B.Size >= 2 && B.Ptr[0] == '.' && B.Ptr[1] == '/')
{
char *NextComponentHead = B.Ptr + 2;
int RemainingChars = StringLength(NextComponentHead);
CopyStringToBufferNoFormat(&B, Wrap0(NextComponentHead));
Clear(B.Ptr, B.Size - (B.Ptr - B.Location));
B.Ptr -= RemainingChars;
}
while(SeekBufferForString(&B, "/./", C_SEEK_FORWARDS, C_SEEK_END) == RC_SUCCESS)
{
char *NextComponentHead = B.Ptr;
int RemainingChars = StringLength(NextComponentHead);
--B.Ptr;
SeekBufferForString(&B, "/", C_SEEK_BACKWARDS, C_SEEK_START);
CopyStringToBufferNoFormat(&B, Wrap0(NextComponentHead));
Clear(B.Ptr, B.Size - (B.Ptr - B.Location));
B.Ptr -= RemainingChars;
}
string BufferedResult = Wrap0(B.Location);
string Result = MakeString("l", &BufferedResult);
DeclaimBuffer(&B);
return Result;
}
string
ParseString(buffer *B, rc *ReturnCode)
{
*ReturnCode = RC_SUCCESS;
char *Ptr = B->Ptr;
bool Quoted = FALSE;
string Result = {};
if(*Ptr == '\"')
{
++Ptr;
Quoted = TRUE;
}
Result.Base = Ptr;
while(Ptr - B->Location < B->Size && (Quoted ? *Ptr != '\"' : !IsWhitespace(*Ptr)))
{
++Ptr;
++Result.Length;
}
if(Quoted && Ptr - B->Location == B->Size)
{
*ReturnCode = RC_ERROR_PARSING_UNCLOSED_QUOTED_STRING;
}
return Result;
}
bool
AtHTMLCommentCloser(buffer *B)
{
bool Result = FALSE;
if((B->Ptr + 3) - B->Location < B->Size && B->Ptr[0] == '-' && B->Ptr[1] == '-' && B->Ptr[2] == '>')
{
Result = TRUE;
}
return Result;
}
asset *
ParseAssetString(template *Template, enum8(template_tag_codes) TagIndex, rc *ReturnCode)
{
asset *Result = 0;
buffer *B = &Template->File.Buffer;
B->Ptr += StringLength(TemplateTags[TagIndex]);
ConsumeWhitespace(B);
if(AtHTMLCommentCloser(B))
{
*ReturnCode = RC_ERROR_MISSING_PARAMETER;
fprintf(stderr, "\n"
"┌─ ");
PrintC(CS_ERROR, "Template error");
fprintf(stderr, " in ");
PrintC(CS_CYAN, Template->File.Path);
fprintf(stderr, "\n"
"└─╼ ");
fprintf(stderr, "%s is missing a file path\n", TemplateTags[TagIndex]);
}
else
{
string AssetString = ParseString(B, ReturnCode);
if(*ReturnCode == RC_SUCCESS)
{
if(AssetString.Length > MAX_ASSET_FILENAME_LENGTH)
{
fprintf(stderr, "\n"
"┌─ ");
PrintC(CS_ERROR, "Template error");
fprintf(stderr, " in ");
PrintC(CS_CYAN, Template->File.Path);
fprintf(stderr, "\n"
"└─╼ ");
fprintf(stderr, "Asset file path is too long (we support paths up to %d characters):\n"
" ", MAX_ASSET_FILENAME_LENGTH);
PrintStringC(CS_MAGENTA_BOLD, AssetString);
fprintf(stderr, "\n");
*ReturnCode = RC_ERROR_CAPACITY;
}
else
{
int AssetType = 0;
switch(TagIndex)
{
case TAG_ASSET: AssetType = ASSET_GENERIC; break;
case TAG_CSS: AssetType = ASSET_CSS; break;
case TAG_IMAGE: AssetType = ASSET_IMG; break;
case TAG_JS: AssetType = ASSET_JS; break;
}
string FinalAssetString = StripPWDIndicators(StripSlashes(AssetString, P_REL));
if(!(Result = GetAsset(FinalAssetString, AssetType)))
{
// TODO(matt): If we add a __CINERA_SPRITE__ template tag, we must pass the actual Variants here
// NOTE(matt): Associating this basically prepares us for when we're storing templates in the db
Result = PushAsset(FinalAssetString, AssetType, CAV_DEFAULT_UNSET, TRUE);
}
FreeString(&FinalAssetString);
}
}
else
{
switch(*ReturnCode)
{
case RC_ERROR_PARSING_UNCLOSED_QUOTED_STRING:
{
fprintf(stderr, "\n"
"┌─ ");
PrintC(CS_ERROR, "Template error");
fprintf(stderr, " in ");
PrintC(CS_CYAN, Template->File.Path);
fprintf(stderr, "\n"
"└─╼ ");
fprintf(stderr, "Unclosed asset quoted string: \"");
string UnclosedString = { .Base = AssetString.Base, .Length = MIN(16, AssetString.Length) };
PrintStringC(CS_MAGENTA_BOLD, UnclosedString);
fprintf(stderr, "\"\n");
} break;
default: break;
}
}
}
return Result;
}
navigation_type
ParseNavigationTag(template *Template, template_tag_code TagIndex)
{
navigation_type Result = NT_NULL;
buffer *B = &Template->File.Buffer;
B->Ptr += StringLength(TemplateTags[TagIndex]);
ConsumeWhitespace(B);
if(AtHTMLCommentCloser(B))
{
Result = NT_PLAIN;
}
else
{
rc ReturnCode = RC_SUCCESS;
string NavigationString = ParseString(B, &ReturnCode);
if(ReturnCode == RC_SUCCESS)
{
for(int NavigationType = 1; NavigationType < NT_COUNT; ++NavigationType)
{
if(!StringsDifferLv0(NavigationString, NavigationTypes[NavigationType]))
{
Result = NavigationType;
break;
}
}
if(Result == NT_NULL)
{
fprintf(stderr, "\n"
"┌─ ");
PrintC(CS_ERROR, "Template error");
fprintf(stderr, " in ");
PrintC(CS_CYAN, Template->File.Path);
fprintf(stderr, "\n"
"└─╼ ");
fprintf(stderr, "Invalid navigation type \"");
PrintStringC(CS_MAGENTA_BOLD, NavigationString);
fprintf(stderr, "\"\n");
}
}
else
{
switch(ReturnCode)
{
case RC_ERROR_PARSING_UNCLOSED_QUOTED_STRING:
{
fprintf(stderr, "\n"
"┌─ ");
PrintC(CS_ERROR, "Template error");
fprintf(stderr, " in ");
PrintC(CS_CYAN, Template->File.Path);
fprintf(stderr, "\n"
"└─╼ ");
fprintf(stderr, "Unclosed navigation type quoted string: \"");
string UnclosedString = { .Base = NavigationString.Base, .Length = MIN(16, NavigationString.Length) };
PrintStringC(CS_MAGENTA_BOLD, UnclosedString);
fprintf(stderr, "\"\n");
} break;
default: break;
}
}
}
return Result;
}
int
PackTemplate(template *Template, string Location, template_type Type)
{
// TODO(matt): Record line numbers and contextual information:
// <? ?>
// <!-- -->
// < >
// <script </script>
InitTemplate(Template, Location, Type);
buffer Errors;
if(ClaimBuffer(&Errors, BID_ERRORS, Kilobytes(1)) == RC_ARENA_FULL) { FreeTemplate(Template); return RC_ARENA_FULL; };
bool HaveErrors = FALSE;
bool HaveAssetParsingErrors = FALSE;
bool HaveNavParsingErrors = FALSE;
bool HaveGlobalValidityErrors = FALSE;
bool FoundIncludes = FALSE;
bool FoundPlayer = FALSE;
bool FoundSearch = FALSE;
char *Previous = Template->File.Buffer.Location;
while(Template->File.Buffer.Ptr - Template->File.Buffer.Location < Template->File.Buffer.Size)
{
NextTagSearch:
if(*Template->File.Buffer.Ptr == '!' && (Template->File.Buffer.Ptr > Template->File.Buffer.Location && !StringsDifferT("<!--", &Template->File.Buffer.Ptr[-1], 0)))
{
char *CommentStart = &Template->File.Buffer.Ptr[-1];
Template->File.Buffer.Ptr += StringLength("!--");
while(Template->File.Buffer.Ptr - Template->File.Buffer.Location < Template->File.Buffer.Size && StringsDifferT("-->", Template->File.Buffer.Ptr, 0))
{
for(int TagIndex = 0; TagIndex < TEMPLATE_TAG_COUNT; ++TagIndex)
{
if(!(StringsDifferT(TemplateTags[TagIndex], Template->File.Buffer.Ptr, 0)))
{
// TODO(matt): Pack up this data for BuffersToHTML() to use
/*
* Potential ways to compress these cases
*
* bool Found[TAG_COUNT]
* -Asaf
*
* int* flags[] = { [TAG_INCLUDES] = &FoundIncludes, [TAG_MENUS] = &FoundMenus } flags[Tags[i].Code] = true;
* -insofaras
*
*/
asset *Asset = 0;
navigation_type NavigationType = NT_NULL;
switch(TagIndex)
{
case TAG_SEARCH:
FoundSearch = TRUE;
goto RecordTag;
#if AFD
case TAG_INCLUDES:
if(
#if 0
!(CurrentProject->Mode & MODE_FORCEINTEGRATION) &&
#endif
FoundIncludes == TRUE)
{
CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", TemplateTags[TagIndex]);
HaveErrors = TRUE;
}
FoundIncludes = TRUE;
goto RecordTag;
case TAG_PLAYER:
if(
#if 0
!(CurrentProject->Mode & MODE_FORCEINTEGRATION) &&
#endif
FoundPlayer == TRUE)
{
CopyStringToBuffer(&Errors, "Template contains more than one <!-- %s --> tag\n", TemplateTags[TagIndex]);
HaveErrors = TRUE;
}
FoundPlayer = TRUE;
goto RecordTag;
#endif
case TAG_ASSET:
case TAG_CSS:
case TAG_IMAGE:
case TAG_JS:
{
rc ReturnCode;
Asset = ParseAssetString(Template, TagIndex, &ReturnCode);
if(ReturnCode != RC_SUCCESS)
{
HaveErrors = TRUE;
HaveAssetParsingErrors = TRUE;
}
} goto RecordTag;
case TAG_NAV:
{
NavigationType = ParseNavigationTag(Template, TagIndex);
if(NavigationType == NT_NULL)
{
HaveErrors = TRUE;
HaveNavParsingErrors = TRUE;
}
else if(NavigationType == NT_DROPDOWN)
{
Template->Metadata.RequiresCineraJS = TRUE;
}
} goto RecordTag;
case TAG_PROJECT:
case TAG_PROJECT_ID:
case TAG_PROJECT_PLAIN:
{
if(Type == TEMPLATE_GLOBAL_SEARCH)
{
CopyStringToBuffer(&Errors, "Global search template may not contain <!-- %s --> tags\n", TemplateTags[TagIndex]);
HaveErrors = TRUE;
HaveGlobalValidityErrors = TRUE;
}
} // NOTE(matt): Intentional fall-through, for project templates
default: // NOTE(matt): All freely usable tags should hit this case
RecordTag:
{
int Offset = CommentStart - Previous;
PushTemplateTag(Template, Offset, TagIndex, Asset, NavigationType);
DepartComment(&Template->File.Buffer);
Previous = Template->File.Buffer.Ptr;
goto NextTagSearch;
}
};
}
}
++Template->File.Buffer.Ptr;
}
}
else
{
++Template->File.Buffer.Ptr;
}
}
if(HaveAssetParsingErrors || HaveNavParsingErrors)
{
DeclaimBuffer(&Errors);
FreeTemplate(Template);
return RC_INVALID_TEMPLATE;
}
if(!HaveErrors && FoundIncludes && FoundSearch)
{
Template->Metadata.Validity |= PAGE_SEARCH;
}
if(!HaveErrors && FoundIncludes && FoundPlayer)
{
Template->Metadata.Validity |= PAGE_PLAYER;
}
#if 0
if(!(CurrentProject->Mode & MODE_FORCEINTEGRATION))
{
#endif
if((Type == TEMPLATE_GLOBAL_SEARCH || Type == TEMPLATE_SEARCH) && !(Template->Metadata.Validity & PAGE_SEARCH))
{
Colourise(CS_ERROR);
if(!FoundIncludes) { CopyStringToBuffer(&Errors, "%sSearch template%s must include one <!-- __CINERA_INCLUDES__ --> tag\n", ColourStrings[CS_ERROR], ColourStrings[CS_END]); };
if(!FoundSearch) { CopyStringToBuffer(&Errors, "%sSearch template%s must include one <!-- __CINERA_SEARCH__ --> tag\n", ColourStrings[CS_ERROR], ColourStrings[CS_END]); };
fprintf(stderr, "%s", Errors.Location);
DeclaimBuffer(&Errors);
FreeTemplate(Template);
Colourise(CS_END);
WaitForInput();
return RC_INVALID_TEMPLATE;
}
else if((Type == TEMPLATE_PLAYER || Type == TEMPLATE_BESPOKE) && !(Template->Metadata.Validity & PAGE_PLAYER))
{
Colourise(CS_ERROR);
if(!FoundIncludes){ CopyStringToBuffer(&Errors, "%s%slayer template%s must include one <!-- __CINERA_INCLUDES__ --> tag\n", ColourStrings[CS_ERROR], Type == TEMPLATE_BESPOKE ? "Bespoke p" : "P", ColourStrings[CS_END]); };
if(!FoundPlayer){ CopyStringToBuffer(&Errors, "%s%slayer template%s must include one <!-- __CINERA_PLAYER__ --> tag\n", ColourStrings[CS_ERROR], Type == TEMPLATE_BESPOKE ? "Bespoke p" : "P", ColourStrings[CS_END]); };
fprintf(stderr, "%s", Errors.Location);
DeclaimBuffer(&Errors);
FreeTemplate(Template);
Colourise(CS_END);
WaitForInput();
return RC_INVALID_TEMPLATE;
}
else if(Type == TEMPLATE_GLOBAL_SEARCH && HaveGlobalValidityErrors)
{
Colourise(CS_ERROR);
fprintf(stderr, "%s", Errors.Location);
DeclaimBuffer(&Errors);
FreeTemplate(Template);
Colourise(CS_END);
WaitForInput();
return RC_INVALID_TEMPLATE;
}
#if 0
}
#endif
DeclaimBuffer(&Errors);
return RC_SUCCESS;
}
void
ConstructSearchURL(buffer *SearchURL, project *P)
{
RewindBuffer(SearchURL);
if(P->BaseURL.Length > 0)
{
CopyStringToBuffer(SearchURL, "%.*s/", (int)P->BaseURL.Length, P->BaseURL.Base);
if(P->SearchLocation.Length > 0)
{
CopyStringToBuffer(SearchURL, "%.*s/", (int)P->SearchLocation.Length, P->SearchLocation.Base);
}
}
}
void
ConstructPlayerURL(buffer *PlayerURL, db_header_project *P, string EntryOutput)
{
RewindBuffer(PlayerURL);
string BaseURL = Wrap0i(P->BaseURL, sizeof(P->BaseURL));
string PlayerLocation = Wrap0i(P->PlayerLocation, sizeof(P->PlayerLocation));
if(BaseURL.Length > 0)
{
CopyStringToBuffer(PlayerURL, "%.*s/", (int)BaseURL.Length, BaseURL.Base);
if(PlayerLocation.Length > 0)
{
CopyStringToBuffer(PlayerURL, "%.*s/", (int)PlayerLocation.Length, PlayerLocation.Base);
}
}
CopyStringToBuffer(PlayerURL, "%.*s/", (int)EntryOutput.Length, EntryOutput.Base);
}
medium *
MediumExists(string Medium)
{
medium *ParsedMedium = GetMediumFromProject(CurrentProject, Medium);
if(ParsedMedium)
{
return ParsedMedium;
}
fprintf(stderr, "Specified default medium \"%.*s\" not available. Valid media are:\n", (int)Medium.Length, Medium.Base);
for(int i = 0; i < CurrentProject->MediumCount; ++i)
{
typography Typography =
{
.UpperLeftCorner = "",
.UpperLeft = "",
.Horizontal = "",
.UpperRight = "",
.Vertical = "",
.LowerLeftCorner = "",
.LowerLeft = "",
.Margin = "",
.Delimiter = ": ",
.Separator = "",
};
PrintMedium(&Typography, &CurrentProject->Medium[i], ": ", 2, TRUE);
}
fprintf(stderr, "Perhaps you'd like to add a new medium to your config file, e.g.:\n"
" medium = \"%.*s\"\n"
" {\n"
" name = \"Name of Medium\";\n"
" icon = \"&#129514;\";\n"
" }\n", (int)Medium.Length, Medium.Base);
return 0;
}
typedef struct
{
char *BaseFilename;
char *Title;
} neighbour;
typedef struct
{
neighbour Prev;
neighbour Next;
} neighbours;
void
ExamineDB1(file File)
{
database1 LocalDB;
LocalDB.Header = *(db_header1 *)File.Buffer.Location;
printf("Current:\n"
"\tDBVersion: %d\n"
"\tAppVersion: %d.%d.%d\n"
"\tHMMLVersion: %d.%d.%d\n"
"\n"
"Entries: %d\n",
LocalDB.Header.DBVersion,
LocalDB.Header.AppVersion.Major, LocalDB.Header.AppVersion.Minor, LocalDB.Header.AppVersion.Patch,
LocalDB.Header.HMMLVersion.Major, LocalDB.Header.HMMLVersion.Minor, LocalDB.Header.HMMLVersion.Patch,
LocalDB.Header.EntryCount);
File.Buffer.Ptr = File.Buffer.Location + sizeof(LocalDB.Header);
for(int EntryIndex = 0; EntryIndex < LocalDB.Header.EntryCount; ++EntryIndex)
{
LocalDB.Entry = *(db_entry1 *)File.Buffer.Ptr;
printf(" %3d\t%s%sSize: %4d\n",
EntryIndex + 1, LocalDB.Entry.BaseFilename,
StringLength(LocalDB.Entry.BaseFilename) > 8 ? "\t" : "\t\t", // NOTE(matt): Janktasm
LocalDB.Entry.Size);
File.Buffer.Ptr += sizeof(LocalDB.Entry);
}
}
void
ExamineDB2(file File)
{
database2 LocalDB;
LocalDB.Header = *(db_header2 *)File.Buffer.Location;
printf("Current:\n"
"\tDBVersion: %d\n"
"\tAppVersion: %d.%d.%d\n"
"\tHMMLVersion: %d.%d.%d\n"
"\n"
"Relative Search Page Location: %s\n"
"Relative Player Page Location: %s\n"
"\n"
"Entries: %d\n",
LocalDB.Header.DBVersion,
LocalDB.Header.AppVersion.Major, LocalDB.Header.AppVersion.Minor, LocalDB.Header.AppVersion.Patch,
LocalDB.Header.HMMLVersion.Major, LocalDB.Header.HMMLVersion.Minor, LocalDB.Header.HMMLVersion.Patch,
LocalDB.Header.SearchLocation[0] != '\0' ? LocalDB.Header.SearchLocation : "(same as Base URL)",
LocalDB.Header.PlayerLocation[0] != '\0' ? LocalDB.Header.PlayerLocation : "(directly descended from Base URL)",
LocalDB.Header.EntryCount);
File.Buffer.Ptr = File.Buffer.Location + sizeof(LocalDB.Header);
for(int EntryIndex = 0; EntryIndex < LocalDB.Header.EntryCount; ++EntryIndex)
{
LocalDB.Entry = *(db_entry2 *)File.Buffer.Ptr;
printf(" %3d\t%s%sSize: %4d\n",
EntryIndex + 1, LocalDB.Entry.BaseFilename,
StringLength(LocalDB.Entry.BaseFilename) > 8 ? "\t" : "\t\t", // NOTE(matt): Janktasm
LocalDB.Entry.Size);
File.Buffer.Ptr += sizeof(LocalDB.Entry);
}
}
void
ExamineDB3(file File)
{
database3 LocalDB;
LocalDB.Header = *(db_header3 *)File.Buffer.Location;
printf("Current:\n"
"\tDBVersion: %d\n"
"\tAppVersion: %d.%d.%d\n"
"\tHMMLVersion: %d.%d.%d\n"
"\n"
"Initial:\n"
"\tDBVersion: %d\n"
"\tAppVersion: %d.%d.%d\n"
"\tHMMLVersion: %d.%d.%d\n"
"\n"
"Project ID: %s\n"
"Project Full Name: %s\n"
"\n"
"Base URL: %s\n"
"Relative Search Page Location: %s\n"
"Relative Player Page Location: %s\n"
"Player Page URL Prefix: %s\n"
"\n"
"Entries: %d\n",
LocalDB.Header.CurrentDBVersion,
LocalDB.Header.CurrentAppVersion.Major, LocalDB.Header.CurrentAppVersion.Minor, LocalDB.Header.CurrentAppVersion.Patch,
LocalDB.Header.CurrentHMMLVersion.Major, LocalDB.Header.CurrentHMMLVersion.Minor, LocalDB.Header.CurrentHMMLVersion.Patch,
LocalDB.Header.InitialDBVersion,
LocalDB.Header.InitialAppVersion.Major, LocalDB.Header.InitialAppVersion.Minor, LocalDB.Header.InitialAppVersion.Patch,
LocalDB.Header.InitialHMMLVersion.Major, LocalDB.Header.InitialHMMLVersion.Minor, LocalDB.Header.InitialHMMLVersion.Patch,
LocalDB.Header.ProjectID,
LocalDB.Header.ProjectName,
LocalDB.Header.BaseURL,
LocalDB.Header.SearchLocation[0] != '\0' ? LocalDB.Header.SearchLocation : "(same as Base URL)",
LocalDB.Header.PlayerLocation[0] != '\0' ? LocalDB.Header.PlayerLocation : "(directly descended from Base URL)",
LocalDB.Header.PlayerURLPrefix[0] != '\0' ? LocalDB.Header.PlayerURLPrefix : "(no special prefix, the player page URLs equal their entry's base filename)",
LocalDB.Header.EntryCount);
File.Buffer.Ptr = File.Buffer.Location + sizeof(LocalDB.Header);
for(int EntryIndex = 0; EntryIndex < LocalDB.Header.EntryCount; ++EntryIndex)
{
LocalDB.Entry = *(db_entry3 *)File.Buffer.Ptr;
printf(" %3d\t%s%sSize: %4d\t%d\t%d\t%d\t%d\n"
"\t %s\n",
EntryIndex + 1, LocalDB.Entry.BaseFilename,
StringLength(LocalDB.Entry.BaseFilename) > 8 ? "\t" : "\t\t", // NOTE(matt): Janktasm
LocalDB.Entry.Size,
LocalDB.Entry.LinkOffsets.PrevStart,
LocalDB.Entry.LinkOffsets.PrevEnd,
LocalDB.Entry.LinkOffsets.NextStart,
LocalDB.Entry.LinkOffsets.NextEnd,
LocalDB.Entry.Title);
File.Buffer.Ptr += sizeof(LocalDB.Entry);
}
}
void
ExamineDB4(file File)
{
#if AFE
database4 LocalDB;
LocalDB.Header = *(db_header4 *)File.Buffer.Location;
LocalDB.EntriesHeader = *(db_header_entries4 *)(File.Buffer.Location + sizeof(LocalDB.Header));
printf("Current:\n"
"\tDBVersion: %d\n"
"\tAppVersion: %d.%d.%d\n"
"\tHMMLVersion: %d.%d.%d\n"
"\n"
"Initial:\n"
"\tDBVersion: %d\n"
"\tAppVersion: %d.%d.%d\n"
"\tHMMLVersion: %d.%d.%d\n"
"\n"
"Database File Location: %s/%s.metadata\n"
"Search File Location: %s/%s.index\n"
"\n"
"Project ID: %s\n"
"Project Full Name: %s\n"
"\n"
"Base URL: %s\n"
"Relative Search Page Location: %s\n"
"Relative Player Page Location: %s\n"
"Player Page URL Prefix: %s\n"
"\n"
"Entries: %d\n",
LocalDB.Header.CurrentDBVersion,
LocalDB.Header.CurrentAppVersion.Major, LocalDB.Header.CurrentAppVersion.Minor, LocalDB.Header.CurrentAppVersion.Patch,
LocalDB.Header.CurrentHMMLVersion.Major, LocalDB.Header.CurrentHMMLVersion.Minor, LocalDB.Header.CurrentHMMLVersion.Patch,
LocalDB.Header.InitialDBVersion,
LocalDB.Header.InitialAppVersion.Major, LocalDB.Header.InitialAppVersion.Minor, LocalDB.Header.InitialAppVersion.Patch,
LocalDB.Header.InitialHMMLVersion.Major, LocalDB.Header.InitialHMMLVersion.Minor, LocalDB.Header.InitialHMMLVersion.Patch,
CurrentProject->BaseDir, CurrentProject->ID,
CurrentProject->BaseDir, CurrentProject->ID,
LocalDB.EntriesHeader.ProjectID,
LocalDB.EntriesHeader.ProjectName,
LocalDB.EntriesHeader.BaseURL,
LocalDB.EntriesHeader.SearchLocation[0] != '\0' ? LocalDB.EntriesHeader.SearchLocation : "(same as Base URL)",
LocalDB.EntriesHeader.PlayerLocation[0] != '\0' ? LocalDB.EntriesHeader.PlayerLocation : "(directly descended from Base URL)",
LocalDB.EntriesHeader.PlayerURLPrefix[0] != '\0' ? LocalDB.EntriesHeader.PlayerURLPrefix : "(no special prefix, the player page URLs equal their entry's base filename)",
LocalDB.EntriesHeader.Count);
File.Buffer.Ptr = File.Buffer.Location + sizeof(LocalDB.Header) + sizeof(LocalDB.EntriesHeader);
for(int EntryIndex = 0; EntryIndex < LocalDB.EntriesHeader.Count; ++EntryIndex)
{
LocalDB.Entry = *(db_entry4 *)File.Buffer.Ptr;
printf(" %3d\t%s%sSize: %4d\t%d\t%d\t%d\t%d\n"
"\t %s\n",
EntryIndex, LocalDB.Entry.BaseFilename,
StringLength(LocalDB.Entry.BaseFilename) > 8 ? "\t" : "\t\t", // NOTE(matt): Janktasm
LocalDB.Entry.Size,
LocalDB.Entry.LinkOffsets.PrevStart,
LocalDB.Entry.LinkOffsets.PrevEnd,
LocalDB.Entry.LinkOffsets.NextStart,
LocalDB.Entry.LinkOffsets.NextEnd,
LocalDB.Entry.Title);
File.Buffer.Ptr += sizeof(LocalDB.Entry);
}
LocalDB.AssetsHeader = *(db_header_assets4 *)File.Buffer.Ptr;
File.Buffer.Ptr += sizeof(LocalDB.AssetsHeader);
printf( "\n"
"Asset Root Directory: %s\n"
"Asset Root URL: %s\n"
" CSS Directory: %s\n"
" Images Directory: %s\n"
" JavaScript Directory: %s\n"
"Assets: %d\n"
"\n"
"%sLandmarks are displayed%s i•%sp%s %swhere i is the Entry Index and p is the Position\n"
"in bytes into the HTML file. Entry Index%s -1 %scorresponds to the Search Page%s\n",
LocalDB.AssetsHeader.RootDir,
LocalDB.AssetsHeader.RootURL,
StringsDiffer(LocalDB.AssetsHeader.CSSDir, "") ? LocalDB.AssetsHeader.CSSDir : "(same as root)",
StringsDiffer(LocalDB.AssetsHeader.ImagesDir, "") ? LocalDB.AssetsHeader.ImagesDir : "(same as root)",
StringsDiffer(LocalDB.AssetsHeader.JSDir, "") ? LocalDB.AssetsHeader.JSDir : "(same as root)",
LocalDB.AssetsHeader.Count,
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
ColourStrings[CS_MAGENTA], ColourStrings[CS_END],
ColourStrings[CS_COMMENT], ColourStrings[CS_END],
ColourStrings[CS_COMMENT], ColourStrings[CS_END]);
for(int AssetIndex = 0; AssetIndex < LocalDB.AssetsHeader.Count; ++AssetIndex)
{
LocalDB.Asset = *(db_asset4*)File.Buffer.Ptr;
printf("\n"
"%s\n"
"Type: %s\n"
"Checksum: %08x\n"
"Landmarks: %d\n",
LocalDB.Asset.Filename,
AssetTypeNames[LocalDB.Asset.Type],
LocalDB.Asset.Hash,
LocalDB.Asset.LandmarkCount);
File.Buffer.Ptr += sizeof(LocalDB.Asset);
for(int LandmarkIndex = 0; LandmarkIndex < LocalDB.Asset.LandmarkCount; ++LandmarkIndex)
{
LocalDB.Landmark = *(db_landmark *)File.Buffer.Ptr;
File.Buffer.Ptr += sizeof(LocalDB.Landmark);
printf(" %d•%s%d%s", LocalDB.Landmark.EntryIndex, ColourStrings[CS_MAGENTA], LocalDB.Landmark.Position, ColourStrings[CS_END]);
}
printf("\n");
}
#endif
}
char *
GetAssetStringFromIndex(special_asset_index Index)
{
char *Result = 0;
switch(Index)
{
case SAI_TEXTUAL:
case SAI_UNSET:
{
Result = SpecialAssetIndexStrings[Index + 2];
} break;
}
return Result;
}
void
PrintAssetIndex(int32_t Index)
{
Colourise(CS_BLUE_BOLD);
if(Index >= 0)
{
fprintf(stderr, "%i", Index);
}
else
{
fprintf(stderr, "%s", GetAssetStringFromIndex(Index));
}
Colourise(CS_END);
}
void *
PrintProjectAndChildren(db_header_project *P, typography T)
{
string ID = Wrap0i(P->ID, sizeof(P->ID));
string Title = Wrap0i(P->Title, sizeof(P->Title));
string BaseDir = Wrap0i(P->BaseDir, sizeof(P->BaseDir));
string BaseURL = Wrap0i(P->BaseURL, sizeof(P->BaseURL));
string SearchLocation = Wrap0i(P->SearchLocation, sizeof(P->SearchLocation));
string PlayerLocation = Wrap0i(P->PlayerLocation, sizeof(P->PlayerLocation));
string Theme = Wrap0i(P->Theme, sizeof(P->Theme));
string Unit = Wrap0i(P->Unit, sizeof(P->Unit));
fprintf(stderr, "\n");
PrintC(CS_YELLOW_BOLD, "\nID"); fprintf(stderr, ": "); PrintStringC(CS_GREEN_BOLD, ID);
PrintC(CS_YELLOW_BOLD, "\nTitle"); fprintf(stderr, ": "); PrintStringC(CS_GREEN_BOLD, Title);
PrintC(CS_YELLOW_BOLD, "\nBaseDir"); fprintf(stderr, ": "); PrintStringC(CS_GREEN_BOLD, BaseDir);
PrintC(CS_YELLOW_BOLD, "\nBaseURL"); fprintf(stderr, ": "); PrintStringC(CS_GREEN_BOLD, BaseURL);
PrintC(CS_YELLOW_BOLD, "\nSearchLocation"); fprintf(stderr, ": "); PrintStringC(CS_GREEN_BOLD, SearchLocation);
PrintC(CS_YELLOW_BOLD, "\nPlayerLocation"); fprintf(stderr, ": "); PrintStringC(CS_GREEN_BOLD, PlayerLocation);
PrintC(CS_YELLOW_BOLD, "\nTheme"); fprintf(stderr, ": "); PrintStringC(CS_GREEN_BOLD, Theme);
PrintC(CS_YELLOW_BOLD, "\nArt asset index"); fprintf(stderr, ": "); PrintAssetIndex(P->ArtIndex);
PrintC(CS_YELLOW_BOLD, "\nIcon asset index"); fprintf(stderr, ": "); PrintAssetIndex(P->IconIndex);
PrintC(CS_YELLOW_BOLD, "\nUnit"); fprintf(stderr, ": "); PrintStringC(CS_GREEN_BOLD, Unit);
char *Ptr = (char *)P;
Ptr += sizeof(db_header_project);
PrintC(CS_YELLOW_BOLD, "\nEntries");
fprintf(stderr, " (");
Colourise(CS_BLUE_BOLD);
fprintf(stderr, "%ld", P->EntryCount);
Colourise(CS_END);
fprintf(stderr, "):");
for(int EntryIndex = 0; EntryIndex < P->EntryCount; ++EntryIndex)
{
db_entry *E = (db_entry *)Ptr;
string EntryHMMLBaseFilename = Wrap0i(E->HMMLBaseFilename, sizeof(E->HMMLBaseFilename));
string EntryOutputLocation = Wrap0i(E->OutputLocation, sizeof(E->OutputLocation));
string EntryTitle = Wrap0i(E->Title, sizeof(E->Title));
fprintf(stderr, "\n"
"%s%s%s%s%s ", T.UpperLeftCorner,
T.Horizontal, T.Horizontal, T.Horizontal,
T.UpperRight);
Colourise(CS_BLACK_BOLD); fprintf(stderr, "[%d] ", EntryIndex); Colourise(CS_END);
PrintStringC(CS_GREEN_BOLD, EntryHMMLBaseFilename);
fprintf(stderr, "\n%s%s", T.Vertical, T.Margin); PrintC(CS_YELLOW_BOLD, "OutputLocation"); fprintf(stderr, ": "); PrintStringC(CS_GREEN_BOLD, EntryOutputLocation);
fprintf(stderr, "\n%s%s", T.Vertical, T.Margin); PrintC(CS_YELLOW_BOLD, "Title"); fprintf(stderr, ": "); PrintStringC(CS_GREEN_BOLD, EntryTitle);
fprintf(stderr, "\n%s%s", T.Vertical, T.Margin); PrintC(CS_YELLOW_BOLD, "Size"); fprintf(stderr, ": ");
Colourise(CS_BLUE_BOLD);
fprintf(stderr, "%d", E->Size);
Colourise(CS_END);
fprintf(stderr, "\n%s%s", T.Vertical, T.Margin); PrintC(CS_YELLOW_BOLD, "Link offsets"); fprintf(stderr, ": ");
Colourise(CS_BLUE_BOLD);
fprintf(stderr, "%d\t%d\t%d\t%d",
E->LinkOffsets.PrevStart,
E->LinkOffsets.PrevEnd,
E->LinkOffsets.NextStart,
E->LinkOffsets.NextEnd);
Colourise(CS_END);
fprintf(stderr, "\n%s%s", T.LowerLeft, T.Margin); PrintC(CS_YELLOW_BOLD, "Art asset index"); fprintf(stderr, ": "); PrintAssetIndex(E->ArtIndex);
Ptr += sizeof(db_entry);
}
if(P->EntryCount > 0) { fprintf(stderr, "\n"); }
PrintC(CS_YELLOW_BOLD, "\nChildren");
fprintf(stderr, " (");
Colourise(CS_BLUE_BOLD);
fprintf(stderr, "%ld", P->ChildCount);
Colourise(CS_END);
fprintf(stderr, "):");
for(int ChildIndex = 0; ChildIndex < P->ChildCount; ++ChildIndex)
{
db_header_project *Child = (db_header_project *)Ptr;
Ptr = PrintProjectAndChildren(Child, T);
}
return Ptr;
}
void *
PrintProjectsBlock(db_block_projects *B)
{
if(!B)
{
B = LocateBlock(B_PROJ);
}
fprintf(stderr, "\n"
"\n");
PrintC(CS_BLUE_BOLD, "Projects Block (PROJ)");
typography Typography =
{
.UpperLeftCorner = "",
.UpperLeft = "",
.Horizontal = "",
.UpperRight = "",
.Vertical = "",
.LowerLeftCorner = "",
.LowerLeft = "",
.Margin = " ",
.Delimiter = ": ",
.Separator = "",
};
char *Ptr = (char *)B;
Ptr += sizeof(db_block_projects);
db_header_project *P = (db_header_project *)Ptr;
for(int i = 0; i < B->Count; ++i)
{
P = PrintProjectAndChildren(P, Typography);
}
return P;
}
void
ExamineDB5(file File)
{
File.Buffer.Ptr = File.Buffer.Location;
database5 LocalDB;
if(File.Buffer.Size >= sizeof(LocalDB.Header))
{
LocalDB.Header = *(db_header5 *)File.Buffer.Ptr;
fprintf(stderr, "\n");
PrintC(CS_BLUE_BOLD, "Versions");
fprintf(stderr,
"\n"
"\n"
" Current:\n"
" Cinera: %d.%d.%d\n"
" Database: %d\n"
" hmmlib: %d.%d.%d\n"
"\n"
" Initial:\n"
" Cinera: %d.%d.%d\n"
" Database: %d\n"
" hmmlib: %d.%d.%d",
LocalDB.Header.CurrentAppVersion.Major, LocalDB.Header.CurrentAppVersion.Minor, LocalDB.Header.CurrentAppVersion.Patch,
LocalDB.Header.CurrentDBVersion,
LocalDB.Header.CurrentHMMLVersion.Major, LocalDB.Header.CurrentHMMLVersion.Minor, LocalDB.Header.CurrentHMMLVersion.Patch,
LocalDB.Header.InitialAppVersion.Major, LocalDB.Header.InitialAppVersion.Minor, LocalDB.Header.InitialAppVersion.Patch,
LocalDB.Header.InitialDBVersion,
LocalDB.Header.InitialHMMLVersion.Major, LocalDB.Header.InitialHMMLVersion.Minor, LocalDB.Header.InitialHMMLVersion.Patch);
File.Buffer.Ptr += sizeof(db_header5);
for(int i = 0; i < LocalDB.Header.BlockCount; ++i)
{
int Four = *(int *)File.Buffer.Ptr;
if(Four == FOURCC("ASET"))
{
db_block_assets *AssetsBlock = (db_block_assets *)File.Buffer.Ptr;
File.Buffer.Ptr = PrintAssetsBlock(AssetsBlock);
}
else if(Four == FOURCC("PROJ"))
{
db_block_projects *ProjectsBlock = (db_block_projects *)File.Buffer.Ptr;
File.Buffer.Ptr = PrintProjectsBlock(ProjectsBlock);
}
else
{
fprintf(stderr, "\n"
"Invalid database file: %s", File.Path);
break;
}
}
fprintf(stderr, "\n");
}
}
void
ExamineDB(void)
{
DB.Metadata.File.Buffer.ID = BID_DATABASE;
DB.Metadata.File = InitFile(0, &Config->DatabaseLocation, EXT_NULL);
ReadFileIntoBuffer(&DB.Metadata.File); // NOTE(matt): Could we actually catch errors (permissions?) here and bail?
if(DB.Metadata.File.Buffer.Location)
{
uint32_t FirstInt = *(uint32_t *)DB.Metadata.File.Buffer.Location;
if(FirstInt != FOURCC("CNRA"))
{
switch(FirstInt)
{
case 1: ExamineDB1(DB.Metadata.File); break;
case 2: ExamineDB2(DB.Metadata.File); break;
case 3: ExamineDB3(DB.Metadata.File); break;
default: printf("Invalid database file: %s\n", DB.Metadata.File.Path); break;
}
}
else
{
uint32_t SecondInt = *(uint32_t *)(DB.Metadata.File.Buffer.Location + sizeof(uint32_t));
switch(SecondInt)
{
case 4: ExamineDB4(DB.Metadata.File); break;
case 5: ExamineDB5(DB.Metadata.File); break;
default: printf("Invalid database file: %s\n", DB.Metadata.File.Path); break;
}
}
}
else
{
fprintf(stderr, "Unable to open database file %s: %s\n", DB.Metadata.File.Path, strerror(errno));
}
FreeFile(&DB.Metadata.File);
}
#define HMMLCleanup() \
DeclaimPlayerBuffers(&PlayerBuffers); \
DeclaimIndexBuffers(&IndexBuffers); \
DeclaimMenuBuffers(&MenuBuffers); \
hmml_free(&HMML)
bool
VideoIsPrivate(char *VideoID)
{
// NOTE(matt): Currently only supports YouTube
// NOTE(matt): Stack-string
char Message[128];
CopyString(Message, sizeof(Message), "%sChecking%s privacy status of: https://youtube.com/watch?v=%s", ColourStrings[CS_ONGOING], ColourStrings[CS_END], VideoID);
fprintf(stderr, "%s", Message);
int MessageLength = StringLength(Message);
buffer VideoAPIResponse;
ClaimBuffer(&VideoAPIResponse, BID_VIDEO_API_RESPONSE, Kilobytes(1));
CURL *curl = curl_easy_init();
if(curl) {
LastPrivacyCheck = time(0);
#define APIKey "AIzaSyAdV2U8ivPk8PHMaPMId0gynksw_gdzr9k"
// NOTE(matt): Stack-string
char URL[1024] = {0};
CopyString(URL, sizeof(URL), "https://www.googleapis.com/youtube/v3/videos?key=%s&part=status&id=%s", APIKey, VideoID);
CURLcode CurlReturnCode;
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &VideoAPIResponse.Ptr);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlIntoBuffer);
curl_easy_setopt(curl, CURLOPT_URL, URL);
if((CurlReturnCode = curl_easy_perform(curl)))
{
fprintf(stderr, "%s\n", curl_easy_strerror(CurlReturnCode));
}
curl_easy_cleanup(curl);
VideoAPIResponse.Ptr = VideoAPIResponse.Location;
// TODO(matt): Parse this JSON
SeekBufferForString(&VideoAPIResponse, "{", C_SEEK_FORWARDS, C_SEEK_AFTER);
SeekBufferForString(&VideoAPIResponse, "\"totalResults\": ", C_SEEK_FORWARDS, C_SEEK_AFTER);
if(*VideoAPIResponse.Ptr == '0')
{
DeclaimBuffer(&VideoAPIResponse);
// printf("Private video: https://youtube.com/watch?v=%s\n", VideoID);
ClearTerminalRow(MessageLength);
return TRUE;
}
SeekBufferForString(&VideoAPIResponse, "{", C_SEEK_FORWARDS, C_SEEK_AFTER);
SeekBufferForString(&VideoAPIResponse, "\"privacyStatus\": \"", C_SEEK_FORWARDS, C_SEEK_AFTER);
// NOTE(matt): Stack-string
char Status[16];
CopyStringNoFormatT(Status, sizeof(Status), VideoAPIResponse.Ptr, '\"');
if(!StringsDiffer0(Status, "public"))
{
DeclaimBuffer(&VideoAPIResponse);
ClearTerminalRow(MessageLength);
return FALSE;
}
}
DeclaimBuffer(&VideoAPIResponse);
// printf("Unlisted video: https://youtube.com/watch?v=%s\n", VideoID);
ClearTerminalRow(MessageLength);
return TRUE;
}
speaker *
GetSpeaker(speakers Speakers, string Username)
{
for(int i = 0; i < Speakers.Count; ++i)
{
if(!StringsDifferCaseInsensitive(Speakers.Speaker[i].Person->ID, Username))
{
return &Speakers.Speaker[i];
}
}
return 0;
}
bool
IsCategorisedAFK(HMML_Annotation *Anno)
{
for(int i = 0; i < Anno->marker_count; ++i)
{
if(!StringsDiffer0(Anno->markers[i].marker, "afk"))
{
return TRUE;
}
}
return FALSE;
}
bool
IsCategorisedAuthored(HMML_Annotation *Anno)
{
for(int i = 0; i < Anno->marker_count; ++i)
{
if(!StringsDiffer0(Anno->markers[i].marker, "authored"))
{
return TRUE;
}
}
return FALSE;
}
typedef struct
{
buffer Quote;
buffer Reference;
buffer Filter;
buffer FilterTopics;
buffer FilterMedia;
buffer Credits;
} menu_buffers;
void
DeclaimMenuBuffers(menu_buffers *B)
{
DeclaimBuffer(&B->Credits);
DeclaimBuffer(&B->FilterMedia);
DeclaimBuffer(&B->FilterTopics);
DeclaimBuffer(&B->Filter);
DeclaimBuffer(&B->Reference);
DeclaimBuffer(&B->Quote);
}
typedef struct
{
buffer Master;
buffer Header;
buffer Class;
buffer Data;
buffer Text;
buffer CategoryIcons;
} index_buffers;
void
DeclaimIndexBuffers(index_buffers *B)
{
DeclaimBuffer(&B->CategoryIcons);
DeclaimBuffer(&B->Text);
DeclaimBuffer(&B->Data);
DeclaimBuffer(&B->Class);
DeclaimBuffer(&B->Header);
DeclaimBuffer(&B->Master);
}
typedef struct
{
buffer Menus;
buffer Main;
buffer Script;
} player_buffers;
void
DeclaimPlayerBuffers(player_buffers *B)
{
DeclaimBuffer(&B->Script);
DeclaimBuffer(&B->Main);
DeclaimBuffer(&B->Menus);
}
char *
ConstructIndexFilePath(db_header_project *Project)
{
string SearchLocation = Wrap0i(Project->SearchLocation, sizeof(Project->SearchLocation));
char *Result = ConstructDirectoryPath(Project, &SearchLocation, 0);
ExtendString0(&Result, Wrap0("/"));
ExtendString0(&Result, Wrap0i(Project->ID, sizeof(Project->ID)));
ExtendString0(&Result, ExtensionStrings[EXT_INDEX]);
return Result;
}
void
DeleteSearchPageFromFilesystem(db_header_project *Project) // NOTE(matt): Do we need to handle relocating, like the PlayerPage function?
{
string SearchLocationL = Wrap0i(Project->SearchLocation, sizeof(Project->SearchLocation));
char *SearchPagePath = ConstructHTMLIndexFilePath(Project, &SearchLocationL, 0);
remove(SearchPagePath);
Free(SearchPagePath);
char *IndexFilePath = ConstructIndexFilePath(Project);
remove(IndexFilePath);
Free(IndexFilePath);
// TODO(matt): Consider the correctness of this
FreeFile(&DB.File);
char *SearchDirectory = ConstructDirectoryPath(Project, &SearchLocationL, 0);
remove(SearchDirectory);
Free(SearchDirectory);
}
void
DeleteGlobalSearchPageFromFilesystem(char *Location)
{
string LocationL = Wrap0(Location);
char *SearchPagePath = ConstructHTMLIndexFilePath(0, &LocationL, 0);
remove(SearchPagePath);
Free(SearchPagePath);
remove(Location);
}
void
PrintLineageAndEntryID(string Lineage, string EntryID, bool AppendNewline)
{
PrintLineage(Lineage, FALSE);
fprintf(stderr, "/");//%.*s - %s\n",
PrintStringC(CS_MAGENTA, EntryID);
if(AppendNewline) { fprintf(stderr, "\n"); }
}
void
PrintLineageAndEntry(string Lineage, string EntryID, string EntryTitle, bool AppendNewline)
{
PrintLineageAndEntryID(Lineage, EntryID, FALSE);
Colourise(CS_MAGENTA);
fprintf(stderr, " - ");
PrintString(EntryTitle);
Colourise(CS_END);
if(AppendNewline) { fprintf(stderr, "\n"); }
}
rc
DeletePlayerPageFromFilesystem(db_header_project *P, string EntryOutput, bool Relocating, bool Echo)
{
string PlayerLocation = Wrap0i(P->PlayerLocation, sizeof(P->PlayerLocation));
char *OutputDirectoryPath = ConstructDirectoryPath(P, &PlayerLocation, &EntryOutput);
DIR *PlayerDir;
if((PlayerDir = opendir(OutputDirectoryPath))) // There is a directory for the Player, which there probably should be if not for manual intervention
{
char *PlayerPagePath = MakeString0("ss", OutputDirectoryPath, "/index.html");
FILE *PlayerPage;
if((PlayerPage = fopen(PlayerPagePath, "r")))
{
fclose(PlayerPage);
remove(PlayerPagePath);
}
Free(PlayerPagePath);
closedir(PlayerDir);
int64_t RemovalSuccess = remove(OutputDirectoryPath);
if(Echo)
{
if(RemovalSuccess == -1)
{
LogError(LOG_NOTICE, "Mostly deleted %.*s/%.*s. Unable to remove directory %s: %s", (int)CurrentProject->Lineage.Length, CurrentProject->Lineage.Base, (int)EntryOutput.Length, EntryOutput.Base, OutputDirectoryPath, strerror(errno));
fprintf(stderr, "%sMostly deleted%s ", ColourStrings[EditTypes[EDIT_DELETION].Colour], ColourStrings[CS_END]);
PrintLineageAndEntryID(CurrentProject->Lineage, EntryOutput, TRUE);
fprintf(stderr, " %sUnable to remove directory%s %s: %s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], OutputDirectoryPath, strerror(errno));
}
else
{
if(!Relocating)
{
LogError(LOG_INFORMATIONAL, "Deleted %.*s/%.*s", (int)CurrentProject->Lineage.Length, CurrentProject->Lineage.Base, (int)EntryOutput.Length, EntryOutput.Base);
fprintf(stderr, "%s%s%s ", ColourStrings[EditTypes[EDIT_DELETION].Colour], EditTypes[EDIT_DELETION].Name, ColourStrings[CS_END]);
PrintLineageAndEntryID(CurrentProject->Lineage, EntryOutput, TRUE);
}
}
}
}
Free(OutputDirectoryPath);
return RC_SUCCESS;
}
int
HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseFilename, neighbourhood *N)
{
RewindCollationBuffers(CollationBuffers);
Clear(CollationBuffers->Custom0, sizeof(CollationBuffers->Custom0));
Clear(CollationBuffers->Custom1, sizeof(CollationBuffers->Custom1));
Clear(CollationBuffers->Custom2, sizeof(CollationBuffers->Custom2));
Clear(CollationBuffers->Custom3, sizeof(CollationBuffers->Custom3));
Clear(CollationBuffers->Custom4, sizeof(CollationBuffers->Custom4));
Clear(CollationBuffers->Custom5, sizeof(CollationBuffers->Custom5));
Clear(CollationBuffers->Custom6, sizeof(CollationBuffers->Custom6));
Clear(CollationBuffers->Custom7, sizeof(CollationBuffers->Custom7));
Clear(CollationBuffers->Custom8, sizeof(CollationBuffers->Custom8));
Clear(CollationBuffers->Custom9, sizeof(CollationBuffers->Custom9));
Clear(CollationBuffers->Custom10, sizeof(CollationBuffers->Custom10));
Clear(CollationBuffers->Custom11, sizeof(CollationBuffers->Custom11));
Clear(CollationBuffers->Custom12, sizeof(CollationBuffers->Custom12));
Clear(CollationBuffers->Custom13, sizeof(CollationBuffers->Custom13));
Clear(CollationBuffers->Custom14, sizeof(CollationBuffers->Custom14));
Clear(CollationBuffers->Custom15, sizeof(CollationBuffers->Custom15));
Clear(CollationBuffers->Title, sizeof(CollationBuffers->Title));
Clear(CollationBuffers->URLPlayer, sizeof(CollationBuffers->URLPlayer));
Clear(CollationBuffers->URLSearch, sizeof(CollationBuffers->URLSearch));
Clear(CollationBuffers->VODPlatform, sizeof(CollationBuffers->VODPlatform));
// TODO(matt): Throughout this function dispense with Filename, in favour of the passed in BaseFilename, or Filepath?
//
// TODO(matt): A "MakeString0OnStack()" sort of function?
// NOTE(matt): Stack-string
int NullTerminationBytes = 1;
char Filename[BaseFilename.Length + ExtensionStrings[EXT_HMML].Length + NullTerminationBytes];
char *P = Filename;
P += CopyStringToBarePtr(P, BaseFilename);
P += CopyStringToBarePtr(P, ExtensionStrings[EXT_HMML]);
*P = '\0';
// NOTE(matt): Stack-string
char Filepath[CurrentProject->HMMLDir.Length + StringLength("/") + BaseFilename.Length + ExtensionStrings[EXT_HMML].Length + NullTerminationBytes];
P = Filepath;
P += CopyStringToBarePtr(P, CurrentProject->HMMLDir);
P += CopyStringToBarePtr(P, Wrap0("/"));
P += CopyStringToBarePtr(P, Wrap0(Filename));
*P = '\0';
FILE *InFile = fopen(Filepath, "r");
if(!InFile)
{
LogError(LOG_ERROR, "Unable to open %s: %s", Filename, strerror(errno));
fprintf(stderr, "Unable to open %s: %s\n", Filename, strerror(errno));
return RC_ERROR_FILE;
}
HMML_Output HMML = hmml_parse_file(InFile);
fclose(InFile);
// TODO(matt): Use the HMML.output here if it is set, else do the GetBaseFilename0() thing
//
// Once we have the idea of an HMML.output, exhaustively make sure we're using the right thing in the right
// place, i.e. BaseFilename vs Output
if(HMML.well_formed)
{
bool HaveErrors = FALSE;
if(BaseFilename.Length > MAX_BASE_FILENAME_LENGTH)
{
fprintf(stderr, "%sBase filename \"%.*s\" is too long (%ld/%d characters)%s\n", ColourStrings[CS_ERROR], (int)BaseFilename.Length, BaseFilename.Base, BaseFilename.Length, MAX_BASE_FILENAME_LENGTH, ColourStrings[CS_END]);
HaveErrors = TRUE;
}
if(!HMML.metadata.title)
{
fprintf(stderr, "Please set the title attribute in the [video] node of your .hmml file\n");
HaveErrors = TRUE;
}
else if(StringLength(HMML.metadata.title) > MAX_TITLE_LENGTH)
{
fprintf(stderr, "%sVideo title \"%s\" is too long (%ld/%d characters)%s\n", ColourStrings[CS_ERROR], HMML.metadata.title, StringLength(HMML.metadata.title), MAX_TITLE_LENGTH, ColourStrings[CS_END]);
HaveErrors = TRUE;
}
else
{
ClearCopyStringNoFormat(CollationBuffers->Title, sizeof(CollationBuffers->Title), Wrap0(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;
}
else if(!GetPersonFromConfig(Wrap0(HMML.metadata.member)))
{
ErrorCredentials(Wrap0(HMML.metadata.member), R_HOST);
HaveErrors = TRUE;
}
if(!HMML.metadata.id)
{
fprintf(stderr, "Please set the id attribute in the [video] node of your .hmml file\n");
HaveErrors = TRUE;
}
else
{
CopyString(CollationBuffers->VideoID, sizeof(CollationBuffers->VideoID), "%s", HMML.metadata.id);
}
string VODPlatform = {};
if(HMML.metadata.vod_platform)
{
VODPlatform = Wrap0(HMML.metadata.vod_platform);
}
else if(CurrentProject->VODPlatform.Length > 0)
{
VODPlatform = CurrentProject->VODPlatform;
}
if(VODPlatform.Length == 0)
{
fprintf(stderr, "Please configure or set the vod_platform attribute in the [video] node of your .hmml file\n");
HaveErrors = TRUE;
}
else
{
CopyString(CollationBuffers->VODPlatform, sizeof(CollationBuffers->VODPlatform), "%s", HMML.metadata.vod_platform);
}
buffer URLPlayer;
ClaimBuffer(&URLPlayer, BID_URL_PLAYER, MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_BASE_FILENAME_LENGTH);
ConstructPlayerURL(&URLPlayer, N->Project, HMML.metadata.output ? Wrap0(HMML.metadata.output) : BaseFilename);
CopyString(CollationBuffers->URLPlayer, sizeof(CollationBuffers->URLPlayer), "%s", URLPlayer.Location);
DeclaimBuffer(&URLPlayer);
medium *DefaultMedium = CurrentProject->DefaultMedium;
if(HMML.metadata.medium && !(DefaultMedium = MediumExists(Wrap0(HMML.metadata.medium))))
{
HaveErrors = TRUE;
}
if(!DefaultMedium)
{
fprintf(stderr, "No default_medium set in config, or medium set in the .hmml video node\n");
HaveErrors = TRUE;
}
string ProjectTitle;
if(CurrentProject->HTMLTitle.Length)
{
ProjectTitle = CurrentProject->HTMLTitle;
}
else
{
ProjectTitle = CurrentProject->Title;
}
// TODO(matt): Handle the art and art_variants once .hmml supports them
// TODO(matt): Consider simply making these as buffers and claiming the necessary amount for them
// The nice thing about doing it this way, though, is that it encourages bespoke template use, which should
// usually be the more convenient way for people to write greater amounts of localised information
for(int CustomIndex = 0; CustomIndex < HMML_CUSTOM_ATTR_COUNT; ++CustomIndex)
{
if(HMML.metadata.custom[CustomIndex])
{
int LengthOfString = StringLength(HMML.metadata.custom[CustomIndex]);
if(LengthOfString > (CustomIndex < 12 ? MAX_CUSTOM_SNIPPET_SHORT_LENGTH : MAX_CUSTOM_SNIPPET_LONG_LENGTH))
{
fprintf(stderr, "%sCustom string %d \"%s%s%s\" is too long (%d/%d characters)%s\n",
ColourStrings[CS_ERROR], CustomIndex, ColourStrings[CS_END], HMML.metadata.custom[CustomIndex], ColourStrings[CS_ERROR],
LengthOfString, CustomIndex < 12 ? MAX_CUSTOM_SNIPPET_SHORT_LENGTH : MAX_CUSTOM_SNIPPET_LONG_LENGTH, ColourStrings[CS_END]);
if(LengthOfString < MAX_CUSTOM_SNIPPET_LONG_LENGTH)
{
fprintf(stderr, "Consider using custom12 to custom15, which can hold %d characters\n", MAX_CUSTOM_SNIPPET_LONG_LENGTH);
}
else
{
fprintf(stderr, "Consider using a bespoke template for longer amounts of localised information\n");
}
HaveErrors = TRUE;
}
else
{
switch(CustomIndex)
{
case 0: CopyStringNoFormat(CollationBuffers->Custom0, sizeof(CollationBuffers->Custom0), Wrap0(HMML.metadata.custom[CustomIndex])); break;
case 1: CopyStringNoFormat(CollationBuffers->Custom1, sizeof(CollationBuffers->Custom1), Wrap0(HMML.metadata.custom[CustomIndex])); break;
case 2: CopyStringNoFormat(CollationBuffers->Custom2, sizeof(CollationBuffers->Custom2), Wrap0(HMML.metadata.custom[CustomIndex])); break;
case 3: CopyStringNoFormat(CollationBuffers->Custom3, sizeof(CollationBuffers->Custom3), Wrap0(HMML.metadata.custom[CustomIndex])); break;
case 4: CopyStringNoFormat(CollationBuffers->Custom4, sizeof(CollationBuffers->Custom4), Wrap0(HMML.metadata.custom[CustomIndex])); break;
case 5: CopyStringNoFormat(CollationBuffers->Custom5, sizeof(CollationBuffers->Custom5), Wrap0(HMML.metadata.custom[CustomIndex])); break;
case 6: CopyStringNoFormat(CollationBuffers->Custom6, sizeof(CollationBuffers->Custom6), Wrap0(HMML.metadata.custom[CustomIndex])); break;
case 7: CopyStringNoFormat(CollationBuffers->Custom7, sizeof(CollationBuffers->Custom7), Wrap0(HMML.metadata.custom[CustomIndex])); break;
case 8: CopyStringNoFormat(CollationBuffers->Custom8, sizeof(CollationBuffers->Custom8), Wrap0(HMML.metadata.custom[CustomIndex])); break;
case 9: CopyStringNoFormat(CollationBuffers->Custom9, sizeof(CollationBuffers->Custom9), Wrap0(HMML.metadata.custom[CustomIndex])); break;
case 10: CopyStringNoFormat(CollationBuffers->Custom10, sizeof(CollationBuffers->Custom10), Wrap0(HMML.metadata.custom[CustomIndex])); break;
case 11: CopyStringNoFormat(CollationBuffers->Custom11, sizeof(CollationBuffers->Custom11), Wrap0(HMML.metadata.custom[CustomIndex])); break;
case 12: CopyStringNoFormat(CollationBuffers->Custom12, sizeof(CollationBuffers->Custom12), Wrap0(HMML.metadata.custom[CustomIndex])); break;
case 13: CopyStringNoFormat(CollationBuffers->Custom13, sizeof(CollationBuffers->Custom13), Wrap0(HMML.metadata.custom[CustomIndex])); break;
case 14: CopyStringNoFormat(CollationBuffers->Custom14, sizeof(CollationBuffers->Custom14), Wrap0(HMML.metadata.custom[CustomIndex])); break;
case 15: CopyStringNoFormat(CollationBuffers->Custom15, sizeof(CollationBuffers->Custom15), Wrap0(HMML.metadata.custom[CustomIndex])); break;
}
}
}
}
if(!HaveErrors)
{
if(HMML.metadata.template)
{
switch(PackTemplate(BespokeTemplate, Wrap0(HMML.metadata.template), TEMPLATE_BESPOKE))
{
case RC_ARENA_FULL:
case RC_INVALID_TEMPLATE: // Invalid template
case RC_ERROR_FILE: // Could not load template
case RC_ERROR_MEMORY: // Could not allocate memory for template
HaveErrors = TRUE;
case RC_SUCCESS:
break;
}
}
}
if(HaveErrors)
{
fprintf(stderr, "%sSkipping%s %.*s", ColourStrings[CS_ERROR], ColourStrings[CS_END], (int)BaseFilename.Length, BaseFilename.Base);
if(HMML.metadata.title) { fprintf(stderr, " - %s", HMML.metadata.title); }
fprintf(stderr, "\n");
hmml_free(&HMML);
return RC_ERROR_HMML;
}
string OutputLocation = {};
if(HMML.metadata.output)
{
OutputLocation = Wrap0(HMML.metadata.output);
}
else
{
OutputLocation = BaseFilename;
}
ClearCopyStringNoFormat(N->WorkingThis.OutputLocation, sizeof(N->WorkingThis.OutputLocation), OutputLocation);
if(N->This)
{
string OldOutputLocation = Wrap0i(N->This->OutputLocation, sizeof(N->This->OutputLocation));
string NewOutputLocation = Wrap0i(N->WorkingThis.OutputLocation, sizeof(N->WorkingThis.OutputLocation));
if(StringsDiffer(OldOutputLocation, NewOutputLocation))
{
DeletePlayerPageFromFilesystem(N->Project, OldOutputLocation, FALSE, TRUE);
}
}
// TODO(matt): Handle art and art_variants, once .hmml supports them
#if DEBUG
printf(
"================================================================================\n"
"%s\n"
"================================================================================\n",
Filename);
#endif
// NOTE(matt): Tree structure of "global" buffer dependencies
// Master
// IncludesPlayer
// Menus
// MenuBuffers->Quote
// MenuBuffers->Reference
// MenuBuffers->Filter
// MenuBuffers->FilterTopics
// MenuBuffers->FilterMedia
// MenuBuffers->Credits
// Player
// Script
menu_buffers MenuBuffers = {};
index_buffers IndexBuffers = {};
player_buffers PlayerBuffers = {};
if(ClaimBuffer(&MenuBuffers.Quote, BID_MENU_BUFFERS_QUOTE, Kilobytes(32)) == RC_ARENA_FULL) { HMMLCleanup(); return RC_ARENA_FULL; };
if(ClaimBuffer(&MenuBuffers.Reference, BID_MENU_BUFFERS_REFERENCE, Kilobytes(32)) == RC_ARENA_FULL) { HMMLCleanup(); return RC_ARENA_FULL; };
if(ClaimBuffer(&MenuBuffers.Filter, BID_MENU_BUFFERS_FILTER, Kilobytes(16)) == RC_ARENA_FULL) { HMMLCleanup(); return RC_ARENA_FULL; };
if(ClaimBuffer(&MenuBuffers.FilterTopics, BID_MENU_BUFFERS_FILTER_TOPICS, Kilobytes(8)) == RC_ARENA_FULL) { HMMLCleanup(); return RC_ARENA_FULL; };
if(ClaimBuffer(&MenuBuffers.FilterMedia, BID_MENU_BUFFERS_FILTER_MEDIA, Kilobytes(8)) == RC_ARENA_FULL) { HMMLCleanup(); return RC_ARENA_FULL; };
if(ClaimBuffer(&MenuBuffers.Credits, BID_MENU_BUFFERS_CREDITS, Kilobytes(8)) == RC_ARENA_FULL) { HMMLCleanup(); return RC_ARENA_FULL; };
// NOTE(matt): Tree structure of IndexBuffers dependencies
// Master
// Header
// Class
// Data
// Text
// CategoryIcons
if(ClaimBuffer(&IndexBuffers.Master, BID_INDEX_BUFFERS_MASTER, Kilobytes(8)) == RC_ARENA_FULL) { HMMLCleanup(); return RC_ARENA_FULL; };
if(ClaimBuffer(&IndexBuffers.Header, BID_INDEX_BUFFERS_HEADER, Kilobytes(1)) == RC_ARENA_FULL) { HMMLCleanup(); return RC_ARENA_FULL; };
if(ClaimBuffer(&IndexBuffers.Class, BID_INDEX_BUFFERS_CLASS, 256) == RC_ARENA_FULL) { HMMLCleanup(); return RC_ARENA_FULL; };
if(ClaimBuffer(&IndexBuffers.Data, BID_INDEX_BUFFERS_DATA, Kilobytes(1)) == RC_ARENA_FULL) { HMMLCleanup(); return RC_ARENA_FULL; };
if(ClaimBuffer(&IndexBuffers.Text, BID_INDEX_BUFFERS_TEXT, Kilobytes(4)) == RC_ARENA_FULL) { HMMLCleanup(); return RC_ARENA_FULL; };
if(ClaimBuffer(&IndexBuffers.CategoryIcons, BID_INDEX_BUFFERS_CATEGORY_ICONS, Kilobytes(1)) == RC_ARENA_FULL) { HMMLCleanup(); return RC_ARENA_FULL; };
if(ClaimBuffer(&PlayerBuffers.Menus, BID_PLAYER_BUFFERS_MENUS, Kilobytes(32)) == RC_ARENA_FULL) { HMMLCleanup(); return RC_ARENA_FULL; };
if(ClaimBuffer(&PlayerBuffers.Main, BID_PLAYER_BUFFERS_MAIN, Kilobytes(512)) == RC_ARENA_FULL) { HMMLCleanup(); return RC_ARENA_FULL; };
if(ClaimBuffer(&PlayerBuffers.Script, BID_PLAYER_BUFFERS_SCRIPT, Kilobytes(8)) == RC_ARENA_FULL) { HMMLCleanup(); return RC_ARENA_FULL; };
ref_info ReferencesArray[200] = { };
categories Topics = { };
categories Media = { };
bool HasQuoteMenu = FALSE;
bool HasReferenceMenu = FALSE;
bool HasFilterMenu = FALSE;
int QuoteIdentifier = 0x3b1;
int RefIdentifier = 1;
int UniqueRefs = 0;
CopyStringToBuffer(&PlayerBuffers.Menus,
"<div class=\"cineraMenus %.*s\">\n"
" <span class=\"episode_name\">", (int)CurrentProject->Theme.Length, CurrentProject->Theme.Base);
CopyStringToBufferHTMLSafe(&PlayerBuffers.Menus, Wrap0(HMML.metadata.title));
CopyStringToBuffer(&PlayerBuffers.Menus, "</span>\n");
CopyStringToBuffer(&PlayerBuffers.Main,
"<div class=\"cineraPlayerContainer\">\n"
" <div class=\"video_container\" data-videoId=\"%s\"></div>\n"
" <div class=\"markers_container %.*s\">\n", HMML.metadata.id, (int)CurrentProject->Theme.Length, CurrentProject->Theme.Base);
if(N)
{
N->WorkingThis.LinkOffsets.PrevStart = (PlayerBuffers.Main.Ptr - PlayerBuffers.Main.Location);
if((N->Prev && N->Prev->Size) || (N->Next && N->Next->Size))
{
if(N->Prev && N->Prev->Size)
{
// TODO(matt): Once we have a more rigorous notion of "Day Numbers", perhaps also use them here
buffer PreviousPlayerURL;
ClaimBuffer(&PreviousPlayerURL, BID_PREVIOUS_PLAYER_URL, MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_BASE_FILENAME_LENGTH);
ConstructPlayerURL(&PreviousPlayerURL, N->Project, Wrap0i(N->Prev->OutputLocation, sizeof(N->Prev->OutputLocation)));
CopyStringToBuffer(&PlayerBuffers.Main,
" <a class=\"episodeMarker prev\" href=\"%s\"><div>&#9195;</div><div>Previous: '%s'</div><div>&#9195;</div></a>\n",
PreviousPlayerURL.Location,
N->Prev->Title);
DeclaimBuffer(&PreviousPlayerURL);
}
else
{
CopyStringToBuffer(&PlayerBuffers.Main,
" <div class=\"episodeMarker first\"><div>&#8226;</div><div>Welcome to <cite>%.*s</cite></div><div>&#8226;</div></div>\n", (int)ProjectTitle.Length, ProjectTitle.Base);
}
}
N->WorkingThis.LinkOffsets.PrevEnd = (PlayerBuffers.Main.Ptr - PlayerBuffers.Main.Location - N->WorkingThis.LinkOffsets.PrevStart);
}
CopyStringToBuffer(&PlayerBuffers.Main,
" <div class=\"markers\">\n");
speakers Speakers = { };
bool RequiresCineraJS = FALSE;
switch(BuildCredits(&MenuBuffers.Credits, &HMML.metadata, &Speakers, &RequiresCineraJS))
{
case CreditsError_NoHost:
case CreditsError_NoIndexer:
case CreditsError_NoCredentials:
fprintf(stderr, "%sSkipping%s %.*s - %s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], (int)BaseFilename.Length, BaseFilename.Base, HMML.metadata.title);
HMMLCleanup();
return RC_ERROR_HMML;
}
bool PrivateVideo = FALSE;
if(N->WorkingThis.Size == 0 && !CurrentProject->IgnorePrivacy)
{
if(VideoIsPrivate(HMML.metadata.id))
{
// TODO(matt): Actually generate these guys, just putting them in a secret location
N->WorkingThis.LinkOffsets.PrevStart = 0;
N->WorkingThis.LinkOffsets.PrevEnd = 0;
PrivateVideo = TRUE;
HMMLCleanup();
return RC_PRIVATE_VIDEO;
}
}
if(!PrivateVideo)
{
CopyStringToBuffer(&CollationBuffers->SearchEntry, "name: \"");
CopyStringToBuffer(&CollationBuffers->SearchEntry, "%.*s", (int)OutputLocation.Length, OutputLocation.Base);
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\n"
"title: \"");
CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, Wrap0(HMML.metadata.title));
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\n"
"markers:\n");
}
#if DEBUG
printf("\n\n --- Entering Timestamps Loop ---\n\n\n\n");
#endif
int PreviousTimecode = 0;
for(int TimestampIndex = 0; TimestampIndex < HMML.annotation_count; ++TimestampIndex)
{
#if DEBUG
printf("%d\n", TimestampIndex);
#endif
HMML_Annotation *Anno = HMML.annotations + TimestampIndex;
if(TimecodeToSeconds(Anno->time) < PreviousTimecode)
{
fprintf(stderr, "%s:%d: Timecode %s is chronologically before previous timecode\n"
"%sSkipping%s %.*s - %s\n",
Filename, Anno->line, Anno->time,
ColourStrings[CS_ERROR], ColourStrings[CS_END], (int)BaseFilename.Length, BaseFilename.Base, HMML.metadata.title);
HMMLCleanup();
return RC_ERROR_HMML;
}
PreviousTimecode = TimecodeToSeconds(Anno->time);
categories LocalTopics = { };
categories LocalMedia = { };
bool HasQuote = FALSE;
bool HasReference = FALSE;
quote_info QuoteInfo = { };
RewindBuffer(&IndexBuffers.Master);
RewindBuffer(&IndexBuffers.Header);
RewindBuffer(&IndexBuffers.Class);
RewindBuffer(&IndexBuffers.Data);
RewindBuffer(&IndexBuffers.Text);
RewindBuffer(&IndexBuffers.CategoryIcons);
CopyStringToBuffer(&IndexBuffers.Header,
" <div data-timestamp=\"%d\"",
TimecodeToSeconds(Anno->time));
CopyStringToBuffer(&IndexBuffers.Class,
" class=\"marker");
speaker *Speaker = GetSpeaker(Speakers, Anno->author ? Wrap0(Anno->author) : CurrentProject->StreamUsername.Length > 0 ? CurrentProject->StreamUsername : CurrentProject->Owner->ID);
if(!IsCategorisedAFK(Anno))
{
if(Speakers.Count > 1 && Speaker && !IsCategorisedAuthored(Anno))
{
string DisplayName = !Speaker->Seen ? Speaker->Person->Name : Wrap0(Speaker->Abbreviation);
CopyStringToBuffer(&IndexBuffers.Text,
"<span class=\"author\" data-hue=\"%d\" data-saturation=\"%d%%\">%.*s</span>: ",
Speaker->Colour.Hue,
Speaker->Colour.Saturation,
(int)DisplayName.Length, DisplayName.Base);
Speaker->Seen = TRUE;
}
else if(Anno->author)
{
if(!HasFilterMenu)
{
HasFilterMenu = TRUE;
}
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, Wrap0("authored"));
hsl_colour AuthorColour;
StringToColourHash(&AuthorColour, Wrap0(Anno->author));
// TODO(matt): That EDITION_NETWORK site database API-polling stuff
CopyStringToBuffer(&IndexBuffers.Text,
"<span class=\"author\" data-hue=\"%d\" data-saturation=\"%d%%\">%s</span> ",
AuthorColour.Hue, AuthorColour.Saturation,
Anno->author);
}
}
char *InPtr = Anno->text;
int MarkerIndex = 0, RefIndex = 0;
while(*InPtr || RefIndex < Anno->reference_count)
{
if(MarkerIndex < Anno->marker_count &&
InPtr - Anno->text == Anno->markers[MarkerIndex].offset)
{
char *Readable = Anno->markers[MarkerIndex].parameter
? Anno->markers[MarkerIndex].parameter
: Anno->markers[MarkerIndex].marker;
switch(Anno->markers[MarkerIndex].type)
{
case HMML_MEMBER:
case HMML_PROJECT:
{
// TODO(matt): That EDITION_NETWORK site database API-polling stuff
hsl_colour Colour;
StringToColourHash(&Colour, Wrap0(Anno->markers[MarkerIndex].marker));
CopyStringToBuffer(&IndexBuffers.Text,
"<span class=\"%s\" data-hue=\"%d\" data-saturation=\"%d%%\">%.*s</span>",
Anno->markers[MarkerIndex].type == HMML_MEMBER ? "member" : "project",
Colour.Hue, Colour.Saturation,
(int)StringLength(Readable), InPtr);
} break;
case HMML_CATEGORY:
{
switch(GenerateTopicColours(N, Wrap0(Anno->markers[MarkerIndex].marker)))
{
case RC_SUCCESS:
case RC_NOOP:
break;
case RC_ERROR_FILE:
case RC_ERROR_MEMORY:
HMMLCleanup();
return RC_ERROR_FATAL;
};
if(!HasFilterMenu)
{
HasFilterMenu = TRUE;
}
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, Wrap0(Anno->markers[MarkerIndex].marker));
CopyStringToBuffer(&IndexBuffers.Text, "%.*s", (int)StringLength(Readable), InPtr);
} break;
case HMML_MARKER_COUNT: break;
}
InPtr += StringLength(Readable);
++MarkerIndex;
}
while(RefIndex < Anno->reference_count &&
InPtr - Anno->text == Anno->references[RefIndex].offset)
{
HMML_Reference *CurrentRef = Anno->references + RefIndex;
if(!HasReferenceMenu)
{
CopyStringToBuffer(&MenuBuffers.Reference,
" <div class=\"menu references\">\n"
" <span>References &#9660;</span>\n"
" <div class=\"refs references_container\">\n");
if(BuildReference(ReferencesArray, RefIdentifier, UniqueRefs, CurrentRef, Anno) == RC_INVALID_REFERENCE)
{
LogError(LOG_ERROR, "Reference combination processing failed: %s:%d", Filename, Anno->line);
fprintf(stderr, "%s:%d: Cannot process new combination of reference info\n"
"\n"
"Either tweak your timestamp, or contact miblodelcarpio@gmail.com\n"
"mentioning the ref node you want to write and how you want it to\n"
"appear in the references menu\n", Filename, Anno->line);
HMMLCleanup();
return RC_INVALID_REFERENCE;
}
++ReferencesArray[RefIdentifier - 1].IdentifierCount;
++UniqueRefs;
HasReferenceMenu = TRUE;
}
else
{
for(int i = 0; i < UniqueRefs; ++i)
{
if(ReferencesArray[i].IdentifierCount == MAX_REF_IDENTIFIER_COUNT)
{
LogError(LOG_EMERGENCY, "REF_MAX_IDENTIFIER (%d) reached. Contact miblodelcarpio@gmail.com", MAX_REF_IDENTIFIER_COUNT);
fprintf(stderr, "%s:%d: Too many timecodes associated with one reference (increase REF_MAX_IDENTIFIER)\n", Filename, Anno->line);
HMMLCleanup();
return RC_ERROR_MAX_REFS;
}
if(CurrentRef->isbn)
{
if(!StringsDiffer0(CurrentRef->isbn, ReferencesArray[i].ID))
{
CopyString(ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Timecode, sizeof(ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Timecode), "%s", Anno->time);
ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Identifier = RefIdentifier;
++ReferencesArray[i].IdentifierCount;
goto AppendedIdentifier;
}
}
else if(CurrentRef->url)
{
if(!StringsDiffer0(CurrentRef->url, ReferencesArray[i].ID))
{
CopyString(ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Timecode, sizeof(ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Timecode), "%s", Anno->time);
ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Identifier = RefIdentifier;
++ReferencesArray[i].IdentifierCount;
goto AppendedIdentifier;
}
}
else
{
LogError(LOG_ERROR, "Reference missing ISBN or URL: %s:%d", Filename, Anno->line);
fprintf(stderr, "%s:%d: Reference must have an ISBN or URL\n", Filename, Anno->line);
HMMLCleanup();
return RC_INVALID_REFERENCE;
}
}
if(BuildReference(ReferencesArray, RefIdentifier, UniqueRefs, CurrentRef, Anno) == RC_INVALID_REFERENCE)
{
LogError(LOG_ERROR, "Reference combination processing failed: %s:%d", Filename, Anno->line);
fprintf(stderr, "%s:%d: Cannot process new combination of reference info\n"
"\n"
"Either tweak your timestamp, or contact miblodelcarpio@gmail.com\n"
"mentioning the ref node you want to write and how you want it to\n"
"appear in the references menu\n", Filename, Anno->line);
HMMLCleanup();
return RC_INVALID_REFERENCE;
}
++ReferencesArray[UniqueRefs].IdentifierCount;
++UniqueRefs;
}
AppendedIdentifier:
if(!HasReference)
{
if(CurrentRef->isbn)
{
CopyStringToBuffer(&IndexBuffers.Data, " data-ref=\"%s", CurrentRef->isbn);
}
else if(CurrentRef->url)
{
CopyStringToBuffer(&IndexBuffers.Data, " 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);
HMMLCleanup();
return RC_INVALID_REFERENCE;
}
HasReference = TRUE;
}
else
{
if(CurrentRef->isbn)
{
CopyStringToBuffer(&IndexBuffers.Data, ",%s", CurrentRef->isbn);
}
else if(CurrentRef->url)
{
CopyStringToBuffer(&IndexBuffers.Data, ",%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);
HMMLCleanup();
return RC_INVALID_REFERENCE;
}
}
CopyStringToBuffer(&IndexBuffers.Text, "<sup>%s%d</sup>",
Anno->references[RefIndex].offset == Anno->references[RefIndex-1].offset ? "," : "",
RefIdentifier);
++RefIndex;
++RefIdentifier;
}
if(*InPtr)
{
switch(*InPtr)
{
case '<':
CopyStringToBuffer(&IndexBuffers.Text, "&lt;");
break;
case '>':
CopyStringToBuffer(&IndexBuffers.Text, "&gt;");
break;
case '&':
CopyStringToBuffer(&IndexBuffers.Text, "&amp;");
break;
case '\"':
CopyStringToBuffer(&IndexBuffers.Text, "&quot;");
break;
case '\'':
CopyStringToBuffer(&IndexBuffers.Text, "&#39;");
break;
default:
*IndexBuffers.Text.Ptr++ = *InPtr;
*IndexBuffers.Text.Ptr = '\0';
break;
}
++InPtr;
}
}
if(Anno->is_quote)
{
if(!HasQuoteMenu)
{
CopyStringToBuffer(&MenuBuffers.Quote,
" <div class=\"menu quotes\">\n"
" <span>Quotes &#9660;</span>\n"
" <div class=\"refs quotes_container\">\n");
HasQuoteMenu = TRUE;
}
if(!HasReference)
{
CopyStringToBuffer(&IndexBuffers.Data, " data-ref=\"&#%d;", QuoteIdentifier);
}
else
{
CopyStringToBuffer(&IndexBuffers.Data, ",&#%d;", QuoteIdentifier);
}
HasQuote = TRUE;
string Speaker = {};
if(Anno->quote.author) { Speaker = Wrap0(Anno->quote.author); }
else if(HMML.metadata.stream_username) { Speaker = Wrap0(HMML.metadata.stream_username); }
else if(CurrentProject->StreamUsername.Length > 0) { Speaker = CurrentProject->StreamUsername; }
else { Speaker = CurrentProject->Owner->ID; }
bool ShouldFetchQuotes = FALSE;
if(Config->CacheDir.Length == 0 || 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", (int)Speaker.Length, Speaker.Base, Anno->quote.id, Filename, Anno->line);
fprintf(stderr, "Quote #%.*s %d not found\n"
"%sSkipping%s %.*s - %s\n",
(int)Speaker.Length, Speaker.Base, Anno->quote.id,
ColourStrings[CS_ERROR], ColourStrings[CS_END], (int)BaseFilename.Length, BaseFilename.Base, HMML.metadata.title);
HMMLCleanup();
return RC_ERROR_QUOTE;
}
CopyStringToBuffer(&MenuBuffers.Quote,
" <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\">",
(int)Speaker.Length, Speaker.Base,
Anno->quote.id,
QuoteIdentifier,
Anno->quote.id);
CopyStringToBufferHTMLSafe(&MenuBuffers.Quote, Wrap0i(QuoteInfo.Text, sizeof(QuoteInfo.Text)));
CopyStringToBuffer(&MenuBuffers.Quote, "</div>\n"
" <div class=\"quote_byline\">&mdash;%.*s, %s</div>\n"
" </span>\n"
" <div class=\"ref_indices\">\n"
" <span data-timestamp=\"%d\" class=\"timecode\"><span class=\"ref_index\">[&#%d;]</span><span class=\"time\">%s</span></span>\n"
" </div>\n"
" </span>\n"
" </a>\n",
(int)Speaker.Length, Speaker.Base,
QuoteInfo.Date,
TimecodeToSeconds(Anno->time),
QuoteIdentifier,
Anno->time);
if(!Anno->text[0])
{
CopyStringToBuffer(&IndexBuffers.Text, "&#8220;");
CopyStringToBufferHTMLSafe(&IndexBuffers.Text, Wrap0i(QuoteInfo.Text, sizeof(QuoteInfo.Text)));
CopyStringToBuffer(&IndexBuffers.Text, "&#8221;");
}
CopyStringToBuffer(&IndexBuffers.Text, "<sup>&#%d;</sup>", QuoteIdentifier);
++QuoteIdentifier;
}
if(!PrivateVideo)
{
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"%d\": \"", TimecodeToSeconds(Anno->time));
if(Anno->is_quote && !Anno->text[0])
{
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\u201C");
CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, Wrap0i(QuoteInfo.Text, sizeof(QuoteInfo.Text)));
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\u201D");
}
else
{
CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, Wrap0(Anno->text));
}
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\n");
}
while(MarkerIndex < Anno->marker_count)
{
switch(GenerateTopicColours(N, Wrap0(Anno->markers[MarkerIndex].marker)))
{
case RC_SUCCESS:
case RC_NOOP:
break;
case RC_ERROR_FILE:
case RC_ERROR_MEMORY:
HMMLCleanup();
return RC_ERROR_FATAL;
}
if(!HasFilterMenu)
{
HasFilterMenu = TRUE;
}
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, Wrap0(Anno->markers[MarkerIndex].marker));
++MarkerIndex;
}
if(LocalTopics.Count == 0)
{
switch(GenerateTopicColours(N, Wrap0("nullTopic")))
{
case RC_SUCCESS:
case RC_NOOP:
break;
case RC_ERROR_FILE:
case RC_ERROR_MEMORY:
HMMLCleanup();
return RC_ERROR_FATAL;
};
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, Wrap0("nullTopic"));
}
if(LocalMedia.Count == 0)
{
InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, DefaultMedium->ID);
}
BuildTimestampClass(&IndexBuffers.Class, &LocalTopics, &LocalMedia, DefaultMedium->ID);
CopyLandmarkedBuffer(&IndexBuffers.Header, &IndexBuffers.Class, 0, PAGE_PLAYER);
if(HasQuote || HasReference)
{
CopyStringToBuffer(&IndexBuffers.Data, "\"");
CopyLandmarkedBuffer(&IndexBuffers.Header, &IndexBuffers.Data, 0, PAGE_PLAYER);
}
CopyStringToBuffer(&IndexBuffers.Header, ">\n");
CopyLandmarkedBuffer(&IndexBuffers.Master, &IndexBuffers.Header, 0, PAGE_PLAYER);
CopyStringToBuffer(&IndexBuffers.Master,
" <div class=\"cineraContent\"><span class=\"timecode\">%s</span>",
Anno->time);
CopyLandmarkedBuffer(&IndexBuffers.Master, &IndexBuffers.Text, 0, PAGE_PLAYER);
if(LocalTopics.Count > 0)
{
BuildCategoryIcons(&IndexBuffers.Master, &LocalTopics, &LocalMedia, DefaultMedium->ID, &RequiresCineraJS);
}
CopyStringToBuffer(&IndexBuffers.Master, "</div>\n"
" <div class=\"progress faded\">\n"
" <div class=\"cineraContent\"><span class=\"timecode\">%s</span>",
Anno->time);
CopyLandmarkedBuffer(&IndexBuffers.Master, &IndexBuffers.Text, 0, PAGE_PLAYER);
if(LocalTopics.Count > 0)
{
BuildCategoryIcons(&IndexBuffers.Master, &LocalTopics, &LocalMedia, DefaultMedium->ID, &RequiresCineraJS);
}
CopyStringToBuffer(&IndexBuffers.Master, "</div>\n"
" </div>\n"
" <div class=\"progress main\">\n"
" <div class=\"cineraContent\"><span class=\"timecode\">%s</span>",
Anno->time);
CopyLandmarkedBuffer(&IndexBuffers.Master, &IndexBuffers.Text, 0, PAGE_PLAYER);
if(LocalTopics.Count > 0)
{
//CopyLandmarkedBuffer(&IndexBuffers.Master, &IndexBuffers.CategoryIcons, PAGE_PLAYER);
BuildCategoryIcons(&IndexBuffers.Master, &LocalTopics, &LocalMedia, DefaultMedium->ID, &RequiresCineraJS);
}
CopyStringToBuffer(&IndexBuffers.Master, "</div>\n"
" </div>\n"
" </div>\n");
CopyLandmarkedBuffer(&PlayerBuffers.Main, &IndexBuffers.Master, 0, PAGE_PLAYER);
}
DeclaimIndexBuffers(&IndexBuffers);
FreeCredentials(&Speakers);
if(!PrivateVideo)
{
CopyStringToBuffer(&CollationBuffers->SearchEntry, "---\n");
N->WorkingThis.Size = CollationBuffers->SearchEntry.Ptr - CollationBuffers->SearchEntry.Location;
}
#if DEBUG
printf("\n\n --- End of Timestamps Loop ---\n\n\n\n");
#endif
if(HasQuoteMenu)
{
CopyStringToBuffer(&MenuBuffers.Quote,
" </div>\n"
" </div>\n");
CopyLandmarkedBuffer(&PlayerBuffers.Menus, &MenuBuffers.Quote, 0, PAGE_PLAYER);
}
if(HasReferenceMenu)
{
for(int i = 0; i < UniqueRefs; ++i)
{
CopyStringToBuffer(&MenuBuffers.Reference,
" <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(&MenuBuffers.Reference,
" <div class=\"source\">");
CopyStringToBufferHTMLSafeBreakingOnSlash(&MenuBuffers.Reference, ReferencesArray[i].Source);
CopyStringToBuffer(&MenuBuffers.Reference, "</div>\n"
" <div class=\"ref_title\">");
CopyStringToBufferHTMLSafeBreakingOnSlash(&MenuBuffers.Reference, ReferencesArray[i].RefTitle);
CopyStringToBuffer(&MenuBuffers.Reference, "</div>\n");
}
else
{
CopyStringToBuffer(&MenuBuffers.Reference,
" <div class=\"ref_title\">");
CopyStringToBufferHTMLSafeBreakingOnSlash(&MenuBuffers.Reference, ReferencesArray[i].RefTitle);
CopyStringToBuffer(&MenuBuffers.Reference, "</div>\n");
}
CopyStringToBuffer(&MenuBuffers.Reference,
" </span>\n");
for(int j = 0; j < ReferencesArray[i].IdentifierCount;)
{
CopyStringToBuffer(&MenuBuffers.Reference,
" <div class=\"ref_indices\">\n ");
for(int k = 0; k < 3 && j < ReferencesArray[i].IdentifierCount; ++k, ++j)
{
CopyStringToBuffer(&MenuBuffers.Reference,
"<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(&MenuBuffers.Reference, "\n"
" </div>\n");
}
CopyStringToBuffer(&MenuBuffers.Reference,
" </a>\n");
}
CopyStringToBuffer(&MenuBuffers.Reference,
" </div>\n"
" </div>\n");
CopyLandmarkedBuffer(&PlayerBuffers.Menus, &MenuBuffers.Reference, 0, PAGE_PLAYER);
}
if(HasFilterMenu)
{
buffer URL;
asset *FilterImage = GetAsset(Wrap0(BuiltinAssets[ASSET_IMG_FILTER].Filename), ASSET_IMG);
ConstructResolvedAssetURL(&URL, FilterImage, PAGE_PLAYER);
CopyStringToBuffer(&MenuBuffers.Filter,
" <div class=\"menu filter\">\n"
" <span><img src=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&MenuBuffers.Filter, FilterImage, PAGE_PLAYER, 0);
CopyStringToBuffer(&MenuBuffers.Filter,
"\"></span>\n"
" <div class=\"filter_container\">\n"
" <div class=\"filter_mode exclusive\">Filter mode: </div>\n"
" <div class=\"filters\">\n");
if(Topics.Count > 0)
{
CopyStringToBuffer(&MenuBuffers.Filter,
" <div class=\"filter_topics\">\n"
" <div class=\"filter_title\">Topics</div>\n");
for(int i = 0; i < Topics.Count; ++i)
{
// NOTE(matt): Stack-string
char SanitisedMarker[StringLength(Topics.Category[i].Marker) + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", Topics.Category[i].Marker);
SanitisePunctuation(SanitisedMarker);
bool NullTopic = !StringsDiffer0(Topics.Category[i].Marker, "nullTopic");
CopyStringToBuffer(&MenuBuffers.FilterTopics,
" <div %sclass=\"filter_content %s\">\n"
" <span class=\"icon category %s\"></span><span class=\"cineraText\">%s</span>\n"
" </div>\n",
NullTopic ? "title=\"Timestamps 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(&MenuBuffers.FilterTopics,
" </div>\n");
CopyLandmarkedBuffer(&MenuBuffers.Filter, &MenuBuffers.FilterTopics, 0, PAGE_PLAYER);
}
if(Media.Count > 0)
{
CopyStringToBuffer(&MenuBuffers.FilterMedia,
" <div class=\"filter_media\">\n"
" <div class=\"filter_title\">Media</div>\n");
for(int i = 0; i < Media.Count; ++i)
{
// NOTE(matt): Stack-string
char SanitisedMarker[StringLength(Media.Category[i].Marker) + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%s", Media.Category[i].Marker);
SanitisePunctuation(SanitisedMarker);
medium *Medium = GetMediumFromProject(CurrentProject, Wrap0i(Media.Category[i].Marker, sizeof(Media.Category[i].Marker)));
CopyStringToBuffer(&MenuBuffers.FilterMedia,
" <div class=\"filter_content %s%s\">\n"
" <span class=\"icon\">",
SanitisedMarker, Medium->Hidden ? " off" : "");
PushIcon(&MenuBuffers.FilterMedia, FALSE, Medium->IconType, Medium->Icon, Medium->IconAsset, Medium->IconVariants, PAGE_PLAYER, &RequiresCineraJS);
CopyStringToBuffer(&MenuBuffers.FilterMedia, "</span><span class=\"cineraText\">%.*s%s</span>\n"
" </div>\n",
(int)Medium->Name.Length, Medium->Name.Base,
Medium == DefaultMedium ? "</span><span class=\"cineraDefaultMediumIndicator\" title=\"Default medium\n"
"Timestamps lacking a media icon are in this medium\">&#128969;" : "");
}
CopyStringToBuffer(&MenuBuffers.FilterMedia,
" </div>\n");
CopyLandmarkedBuffer(&MenuBuffers.Filter, &MenuBuffers.FilterMedia, 0, PAGE_PLAYER);
}
CopyStringToBuffer(&MenuBuffers.Filter,
" </div>\n"
" </div>\n"
" </div>\n");
CopyLandmarkedBuffer(&PlayerBuffers.Menus, &MenuBuffers.Filter, 0, PAGE_PLAYER);
}
CopyStringToBuffer(&PlayerBuffers.Menus,
" <div class=\"menu views\">\n"
" <div class=\"view\" data-id=\"theatre\" title=\"Theatre mode\">&#127917;</div>\n"
" <div class=\"views_container\">\n"
" <div class=\"view\" data-id=\"super\" title=\"SUPERtheatre mode\">&#127967;</div>\n"
" </div>\n"
" </div>\n"
" <div class=\"menu link\">\n"
" <span>&#128279;</span>\n"
" <div class=\"link_container\">\n"
" <div id=\"cineraLinkMode\">Link to: current timestamp</div>\n"
" <textarea title=\"Click to copy to clipboard\" id=\"cineraLink\" readonly spellcheck=\"false\"></textarea>\n"
" </div>\n"
" </div>\n");
bool HasCreditsMenu = MenuBuffers.Credits.Ptr > MenuBuffers.Credits.Location;
if(HasCreditsMenu)
{
CopyLandmarkedBuffer(&PlayerBuffers.Menus, &MenuBuffers.Credits, 0, PAGE_PLAYER);
}
CopyStringToBuffer(&PlayerBuffers.Menus,
" <div class=\"help\">\n"
" <span>?</span>\n"
" <div class=\"help_container\">\n"
" <span class=\"help_key\">?</span><h1>Keyboard Navigation</h1>\n"
"\n"
" <h2>Global Keys</h2>\n"
" <span class=\"help_key\">[</span>, <span class=\"help_key\">&lt;</span> / <span class=\"help_key\">]</span>, <span class=\"help_key\">&gt;</span> <span class=\"help_text\">Jump to previous / next episode</span><br>\n"
" <span class=\"help_key\">W</span>, <span class=\"help_key\">K</span>, <span class=\"help_key\">P</span> / <span class=\"help_key\">S</span>, <span class=\"help_key\">J</span>, <span class=\"help_key\">N</span> <span class=\"help_text\">Jump to previous / next marker</span><br>\n"
" <span class=\"help_key\">t</span> / <span class=\"help_key\">T</span> <span class=\"help_text\">Toggle theatre / SUPERtheatre mode</span><br>\n"
" <span class=\"help_key%s\">V</span> <span class=\"help_text%s\">Revert filter to original state</span> <span class=\"help_key\">Y</span> <span class=\"help_text\">Select link (requires manual Ctrl-c)</span>\n",
HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable");
CopyStringToBuffer(&PlayerBuffers.Menus,
"\n"
" <h2>Menu toggling</h2>\n"
" <span class=\"help_key%s\">q</span> <span class=\"help_text%s\">Quotes</span>\n"
" <span class=\"help_key%s\">r</span> <span class=\"help_text%s\">References</span>\n"
" <span class=\"help_key%s\">f</span> <span class=\"help_text%s\">Filter</span>\n"
" <span class=\"help_key\">y</span> <span class=\"help_text\">Link</span>\n"
" <span class=\"help_key%s\">c</span> <span class=\"help_text%s\">Credits</span>\n",
HasQuoteMenu ? "" : " unavailable", HasQuoteMenu ? "" : " unavailable",
HasReferenceMenu ? "" : " unavailable", HasReferenceMenu ? "" : " unavailable",
HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable",
HasCreditsMenu ? "" : " unavailable", HasCreditsMenu ? "" : " unavailable");
CopyStringToBuffer(&PlayerBuffers.Menus,
"\n"
" <h2>In-Menu Movement</h2>\n"
" <div class=\"help_paragraph\">\n"
" <div class=\"key_block\">\n"
" <div>\n"
" <span class=\"help_key\">a</span>\n"
" </div>\n"
" <div>\n"
" <span class=\"help_key\">w</span><br>\n"
" <span class=\"help_key\">s</span>\n"
" </div>\n"
" <div>\n"
" <span class=\"help_key\">d</span>\n"
" </div>\n"
" </div>\n"
" <div class=\"key_block\">\n"
" <span class=\"help_key\">h</span>\n"
" <span class=\"help_key\">j</span>\n"
" <span class=\"help_key\">k</span>\n"
" <span class=\"help_key\">l</span>\n"
" </div>\n"
" <div class=\"key_block\">\n"
" <div>\n"
" <span class=\"help_key\">←</span>\n"
" </div>\n"
" <div>\n"
" <span class=\"help_key\">↑</span><br>\n"
" <span class=\"help_key\">↓</span>\n"
" </div>\n"
" <div>\n"
" <span class=\"help_key\">→</span>\n"
" </div>\n"
" </div>\n"
" </div>\n"
" <br>\n");
CopyStringToBuffer(&PlayerBuffers.Menus,
" <h2>%sQuotes %sand%s References%s Menus%s</h2>\n"
" <span class=\"help_key word%s\">Enter</span> <span class=\"help_text%s\">Jump to timecode</span><br>\n",
// Q R
//
// 0 0 <h2><span off>Quotes and References Menus</span></h2>
// 0 1 <h2><span off>Quotes and</span> References Menus</h2>
// 1 0 <h2>Quotes <span off>and References</span> Menus</h2>
// 1 1 <h2>Quotes and References Menus</h2>
HasQuoteMenu ? "" : "<span class=\"unavailable\">",
HasQuoteMenu && !HasReferenceMenu ? "<span class=\"unavailable\">" : "",
!HasQuoteMenu && HasReferenceMenu ? "</span>" : "",
HasQuoteMenu && !HasReferenceMenu ? "</span>" : "",
!HasQuoteMenu && !HasReferenceMenu ? "</span>" : "",
HasQuoteMenu || HasReferenceMenu ? "" : " unavailable", HasQuoteMenu || HasReferenceMenu ? "" : " unavailable");
CopyStringToBuffer(&PlayerBuffers.Menus,
"\n"
" <h2>%sQuotes%s,%s References %sand%s Credits%s Menus%s</h2>"
" <span class=\"help_key%s\">o</span> <span class=\"help_text%s\">Open URL (in new tab)</span>\n",
// Q R C
//
// 0 0 0 <h2><span off>Quotes, References and Credits Menus</span></h2>
// 0 0 1 <h2><span off>Quotes, References and</span> Credits Menus</h2>
// 0 1 0 <h2><span off>Quotes,</span> References <span off>and Credits</span> Menus</h2>
// 0 1 1 <h2><span off>Quotes,</span> References and Credits Menus</h2>
// 1 0 0 <h2>Quotes<span off>, References and Credits</span> Menus</h2>
// 1 0 1 <h2>Quotes<span off>, References </span>and Credits Menus</h2>
// 1 1 0 <h2>Quotes, References <span off>and Credits</span> Menus</h2>
// 1 1 1 <h2>Quotes, References and Credits Menus</h2>
/* 0 */ HasQuoteMenu ? "" : "<span class=\"unavailable\">",
/* 1 */ HasQuoteMenu && !HasReferenceMenu ? "<span class=\"unavailable\">" : "",
/* 2 */ !HasQuoteMenu && HasReferenceMenu ? "</span>" : "",
/* 3 */ HasReferenceMenu && !HasCreditsMenu ? "<span class=\"unavailable\">" : HasQuoteMenu && !HasReferenceMenu && HasCreditsMenu ? "</span>" : "",
/* 4 */ !HasQuoteMenu && !HasReferenceMenu && HasCreditsMenu ? "</span>" : "",
/* 5 */ !HasCreditsMenu && (HasQuoteMenu || HasReferenceMenu) ? "</span>" : "",
/* 6 */ !HasQuoteMenu && !HasReferenceMenu && !HasCreditsMenu ? "</span>" : "",
HasQuoteMenu || HasReferenceMenu || HasCreditsMenu ? "" : " unavailable",
HasQuoteMenu || HasReferenceMenu || HasCreditsMenu ? "" : " unavailable");
CopyStringToBuffer(&PlayerBuffers.Menus,
"\n"
" <h2>%sFilter Menu%s</h2>\n"
" <span class=\"help_key%s\">x</span>, <span class=\"help_key word%s\">Space</span> <span class=\"help_text%s\">Toggle category and focus next</span><br>\n"
" <span class=\"help_key%s\">X</span>, <span class=\"help_key word modifier%s\">Shift</span><span class=\"help_key word%s\">Space</span> <span class=\"help_text%s\">Toggle category and focus previous</span><br>\n"
" <span class=\"help_key%s\">v</span> <span class=\"help_text%s\">Invert topics / media as per focus</span>\n"
"\n"
" <h2>%sFilter and%s Link Menus</h2>\n"
" <span class=\"help_key\">z</span> <span class=\"help_text\">Toggle %sfilter /%s linking mode</span>\n",
HasFilterMenu ? "" : "<span class=\"unavailable\">", HasFilterMenu ? "" : "</span>",
HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable",
HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable",
HasFilterMenu ? "" : " unavailable", HasFilterMenu ? "" : " unavailable",
HasFilterMenu ? "" : "<span class=\"unavailable\">", HasFilterMenu ? "" : "</span>",
HasFilterMenu ? "" : "<span class=\"help_text unavailable\">", HasFilterMenu ? "" : "</span>");
CopyStringToBuffer(&PlayerBuffers.Menus,
"\n"
" <h2>%sCredits Menu%s</h2>\n"
" <span class=\"help_key word%s\">Enter</span> <span class=\"help_text%s\">Open URL (in new tab)</span><br>\n",
HasCreditsMenu ? "" : "<span class=\"unavailable\">", HasCreditsMenu ? "" : "</span>",
HasCreditsMenu ? "" : " unavailable", HasCreditsMenu ? "" : " unavailable");
CopyStringToBuffer(&PlayerBuffers.Menus,
" </div>\n"
" </div>\n"
" </div>");
CopyStringToBuffer(&PlayerBuffers.Main,
" </div>\n");
if(N)
{
N->WorkingThis.LinkOffsets.NextStart = (PlayerBuffers.Main.Ptr - PlayerBuffers.Main.Location - (N->WorkingThis.LinkOffsets.PrevStart + N->WorkingThis.LinkOffsets.PrevEnd));
if((N->Prev && N->Prev->Size) || (N->Next && N->Next->Size))
{
if(N->Next && N->Next->Size > 0)
{
// TODO(matt): Once we have a more rigorous notion of "Day Numbers", perhaps also use them here
buffer NextPlayerURL;
ClaimBuffer(&NextPlayerURL, BID_NEXT_PLAYER_URL, MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_BASE_FILENAME_LENGTH);
ConstructPlayerURL(&NextPlayerURL, N->Project, Wrap0i(N->Next->OutputLocation, sizeof(N->Next->OutputLocation)));
CopyStringToBuffer(&PlayerBuffers.Main,
" <a class=\"episodeMarker next\" href=\"%s\"><div>&#9196;</div><div>Next: '%s'</div><div>&#9196;</div></a>\n",
NextPlayerURL.Location,
N->Next->Title);
DeclaimBuffer(&NextPlayerURL);
}
else
{
CopyStringToBuffer(&PlayerBuffers.Main,
" <div class=\"episodeMarker last\"><div>&#8226;</div><div>You have arrived at the (current) end of <cite>%.*s</cite></div><div>&#8226;</div></div>\n", (int)ProjectTitle.Length, ProjectTitle.Base);
}
}
N->WorkingThis.LinkOffsets.NextEnd = (PlayerBuffers.Main.Ptr - PlayerBuffers.Main.Location - (N->WorkingThis.LinkOffsets.PrevStart + N->WorkingThis.LinkOffsets.PrevEnd + N->WorkingThis.LinkOffsets.NextStart));
}
CopyStringToBuffer(&PlayerBuffers.Main,
" </div>\n"
" </div>");
buffer URLSearch;
ClaimBuffer(&URLSearch, BID_URL_SEARCH, MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1);
ConstructSearchURL(&URLSearch, CurrentProject);
CopyString(CollationBuffers->URLSearch, sizeof(CollationBuffers->URLSearch), "%s", URLSearch.Location);
DeclaimBuffer(&URLSearch);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"<meta charset=\"UTF-8\">\n"
" <meta name=\"generator\" content=\"Cinera %d.%d.%d\">\n",
CINERA_APP_VERSION.Major,
CINERA_APP_VERSION.Minor,
CINERA_APP_VERSION.Patch);
if(Topics.Count || Media.Count)
{
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
" <meta name=\"keywords\" content=\"");
if(Topics.Count > 0)
{
for(int i = 0; i < Topics.Count; ++i)
{
if(StringsDiffer0(Topics.Category[i].Marker, "nullTopic"))
{
CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "%s, ", Topics.Category[i].Marker);
}
}
}
if(Media.Count > 0)
{
for(int i = 0; i < Media.Count; ++i)
{
CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "%s, ", Media.Category[i].WrittenText);
}
}
CollationBuffers->IncludesPlayer.Ptr -= 2;
CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "\">\n");
}
buffer URL = {};
URL.ID = BID_URL;
asset *CSSCinera = GetAsset(Wrap0(BuiltinAssets[ASSET_CSS_CINERA].Filename), ASSET_CSS);
ConstructResolvedAssetURL(&URL, CSSCinera, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesPlayer, CSSCinera, PAGE_PLAYER, 0);
string ThemeFilename = MakeString("sls", "cinera__", &CurrentProject->Theme, ".css");
asset *CSSTheme = GetAsset(ThemeFilename, ASSET_CSS);
FreeString(&ThemeFilename);
ConstructResolvedAssetURL(&URL, CSSTheme, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"\">\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesPlayer, CSSTheme, PAGE_PLAYER, 0);
asset *CSSTopics = GetAsset(Wrap0(BuiltinAssets[ASSET_CSS_TOPICS].Filename), ASSET_CSS);
ConstructResolvedAssetURL(&URL, CSSTopics, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"\">\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesPlayer, CSSTopics, PAGE_PLAYER, 0);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"\">");
if(BespokeTemplate->Metadata.TagCount > 0)
{
if(BespokeTemplate->Metadata.RequiresCineraJS)
{
RequiresCineraJS = TRUE;
}
}
else
{
if(CurrentProject->PlayerTemplate.Metadata.RequiresCineraJS)
{
RequiresCineraJS = TRUE;
}
}
asset *JSCineraPre = GetAsset(Wrap0(BuiltinAssets[ASSET_JS_CINERA_PRE].Filename), ASSET_JS);
ConstructResolvedAssetURL(&URL, JSCineraPre, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"\n <script type=\"text/javascript\" src=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesPlayer, JSCineraPre, PAGE_PLAYER, 0);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"\"></script>");
if(RequiresCineraJS)
{
asset *JSCineraPost = GetAsset(Wrap0(BuiltinAssets[ASSET_JS_CINERA_POST].Filename), ASSET_JS);
ConstructResolvedAssetURL(&URL, JSCineraPost, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"\n <script type=\"text/javascript\" src=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesPlayer, JSCineraPost, PAGE_PLAYER, 0);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"\" defer></script>");
}
asset *JSPlayerPre = GetAsset(Wrap0(BuiltinAssets[ASSET_JS_PLAYER_PRE].Filename), ASSET_JS);
ConstructResolvedAssetURL(&URL, JSPlayerPre, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"\n <script type=\"text/javascript\" src=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesPlayer, JSPlayerPre, PAGE_PLAYER, 0);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"\"></script>");
asset *JSPlayerPost = GetAsset(Wrap0(BuiltinAssets[ASSET_JS_PLAYER_POST].Filename), ASSET_JS);
ConstructResolvedAssetURL(&URL, JSPlayerPost, PAGE_PLAYER);
CopyStringToBuffer(&PlayerBuffers.Script,
"<script type=\"text/javascript\" src=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&PlayerBuffers.Script, JSPlayerPost, PAGE_PLAYER, 0);
CopyStringToBuffer(&PlayerBuffers.Script,
"\"></script>");
CopyLandmarkedBuffer(&CollationBuffers->Player, &PlayerBuffers.Menus, 0, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->Player, "\n"
" ");
CopyLandmarkedBuffer(&CollationBuffers->Player, &PlayerBuffers.Main, &N->WorkingThis.LinkOffsets.PrevStart, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->Player, "\n"
" ");
CopyStringToBuffer(&CollationBuffers->Player, "</div>\n"
" ");
CopyLandmarkedBuffer(&CollationBuffers->Player, &PlayerBuffers.Script, 0, PAGE_PLAYER);
// NOTE(matt): Tree structure of "global" buffer dependencies
// MenuBuffers.Credits
// MenuBuffers.FilterMedia
// MenuBuffers.FilterTopics
// MenuBuffers.Filter
// MenuBuffers.Reference
// MenuBuffers.Quote
DeclaimMenuBuffers(&MenuBuffers);
DeclaimPlayerBuffers(&PlayerBuffers);
}
else
{
LogError(LOG_ERROR, "%s:%d: %s", Filename, HMML.error.line, HMML.error.message);
fprintf(stderr, "%sSkipping%s %s:%d: %s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], Filename, HMML.error.line, HMML.error.message);
hmml_free(&HMML);
return RC_ERROR_HMML;
}
hmml_free(&HMML);
return RC_SUCCESS;
}
int
BuffersToHTML(buffers *CollationBuffers, template *Template, char *OutputPath, page_type PageType, unsigned int *PlayerOffset)
{
MEM_TEST_INITIAL();
#if DEBUG
printf("\n\n --- Buffer Collation ---\n"
" %s\n\n\n", OutputPath ? OutputPath : CurrentProject->OutLocation);
#endif
#if DEBUG_MEM
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, "\nEntered BuffersToHTML(");
if(OutputPath)
{
fprintf(MemLog, "%s", OutputPath);
}
else
{
fprintf(MemLog, "%.*s", (int)CurrentProject->PlayerLocation.Length, CurrentProject->PlayerLocation.Base);
}
fprintf(MemLog, ")\n");
fclose(MemLog);
#endif
// TODO(matt): Consider straight up enforcing that a template is set and available, and that we do not attempt to
// generate a sort of bare default .HTML file
// TODO(matt): Redo this bit to use the AppendStringToBuffer() sort of stuff
if(Template->File.Buffer.Location)
{
#if AFD
if((Template->Metadata.Validity & PageType))// || CurrentProject->Mode & MODE_FORCEINTEGRATION)
{
buffer Master;
Master.Size = Template->File.Buffer.Size + (Kilobytes(512));
Master.ID = BID_MASTER;
if(!(Master.Location = malloc(Master.Size)))
{
LogError(LOG_ERROR, "BuffersToHTML(): %s",
strerror(errno));
MEM_TEST_AFTER("BuffersToHTML");
return RC_ERROR_MEMORY;
}
#if DEBUG_MEM
MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, " Allocated Master (%ld)\n", Master.Size);
fclose(MemLog);
printf(" Allocated Master (%ld)\n", Master.Size);
#endif
Master.Ptr = Master.Location;
Template->File.Buffer.Ptr = Template->File.Buffer.Location;
for(int i = 0; i < Template->Metadata.TagCount; ++i)
{
int j = 0;
while(Template->Metadata.Tags[i].Offset > j)
{
*Master.Ptr++ = *Template->File.Buffer.Ptr++;
++j;
}
// TODO(matt): Make this whole template stuff context-aware, so it can determine whether or not to HTML-encode
// or sanitise punctuation for CSS-safety
switch(Template->Metadata.Tags[i].TagCode)
{
case TAG_PROJECT:
{
if(CurrentProject->HTMLTitle.Length > 0) { CopyStringToBufferNoFormat(&Master, CurrentProject->HTMLTitle); } // NOTE(matt): Not HTML-safe
else { CopyStringToBufferHTMLSafe(&Master, CurrentProject->Title); }
} break;
case TAG_PROJECT_ID: CopyStringToBufferNoFormat(&Master, CurrentProject->ID); break; // NOTE(matt): Not HTML-safe
case TAG_PROJECT_PLAIN: CopyStringToBufferHTMLSafe(&Master, CurrentProject->Title); break;
case TAG_SEARCH_URL: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->URLSearch, sizeof(CollationBuffers->URLSearch))); break; // NOTE(matt): Not HTML-safe
case TAG_THEME: CopyStringToBufferNoFormat(&Master, CurrentProject->Theme); break; // NOTE(matt): Not HTML-safe
case TAG_TITLE: CopyStringToBufferHTMLSafe(&Master, Wrap0i(CollationBuffers->Title, sizeof(CollationBuffers->Title))); break;
case TAG_URL:
if(PageType == PAGE_PLAYER)
{
CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->URLPlayer, sizeof(CollationBuffers->URLPlayer)));
}
else
{
CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->URLSearch, sizeof(CollationBuffers->URLSearch)));
}
break;
case TAG_VIDEO_ID: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->VideoID, sizeof(CollationBuffers->VideoID))); break;
case TAG_VOD_PLATFORM: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->VODPlatform, sizeof(CollationBuffers->VODPlatform))); break;
case TAG_SEARCH:
{
CopyLandmarkedBuffer(&Master, &CollationBuffers->Search, 0, PageType);
} break;
case TAG_INCLUDES:
if(PageType == PAGE_PLAYER)
{
CopyLandmarkedBuffer(&Master, &CollationBuffers->IncludesPlayer, 0, PageType);
}
else
{
CopyLandmarkedBuffer(&Master, &CollationBuffers->IncludesSearch, 0, PageType);
}
break;
case TAG_PLAYER:
CopyLandmarkedBuffer(&Master, &CollationBuffers->Player, PlayerOffset, PageType);
break;
case TAG_ASSET:
{
buffer URL;
ConstructResolvedAssetURL(&URL, Template->Metadata.Tags[i].Asset, PageType);
CopyStringToBuffer(&Master, "%s", URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&Master, Template->Metadata.Tags[i].Asset, PageType, 0);
} break;
case TAG_CSS:
{
buffer URL;
ConstructResolvedAssetURL(&URL, Template->Metadata.Tags[i].Asset, PageType);
CopyStringToBuffer(&Master,
"<link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&Master, Template->Metadata.Tags[i].Asset, PageType, 0);
CopyStringToBuffer(&Master, "\">");
} break;
case TAG_IMAGE:
{
buffer URL;
ConstructResolvedAssetURL(&URL, Template->Metadata.Tags[i].Asset, PageType);
CopyStringToBuffer(&Master, "%s", URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&Master, Template->Metadata.Tags[i].Asset, PageType, 0);
} break;
case TAG_JS:
{
buffer URL;
ConstructResolvedAssetURL(&URL, Template->Metadata.Tags[i].Asset, PageType);
CopyStringToBuffer(&Master,
"<script type=\"text/javascript\" src=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&Master, Template->Metadata.Tags[i].Asset, PageType, 0);
CopyStringToBuffer(&Master, "\"></script>");
} break;
case TAG_NAV:
{
buffer *NavDropdownPre;
buffer *NavHorizontalPre;
buffer *NavPlainPre;
buffer *NavGeneric;
buffer *NavDropdownPost;
if(Template->Metadata.Type == TEMPLATE_GLOBAL_SEARCH)
{
NavDropdownPre = &Config->NavDropdownPre;
NavHorizontalPre = &Config->NavHorizontalPre;
NavPlainPre = &Config->NavPlainPre;
NavGeneric = &Config->NavGeneric;
NavDropdownPost = &Config->NavDropdownPost;
}
else
{
NavDropdownPre = &CurrentProject->NavDropdownPre;
NavHorizontalPre = &CurrentProject->NavHorizontalPre;
NavPlainPre = &CurrentProject->NavPlainPre;
NavGeneric = &CurrentProject->NavGeneric;
NavDropdownPost = &CurrentProject->NavDropdownPost;
}
switch(Template->Metadata.Tags[i].NavigationType)
{
case NT_DROPDOWN:
{
CopyLandmarkedBuffer(&Master, NavDropdownPre, 0, PageType);
} // NOTE(matt): Intentional fall-through
case NT_HORIZONTAL:
{
CopyLandmarkedBuffer(&Master, NavHorizontalPre, 0, PageType);
} break;
case NT_PLAIN:
{
CopyLandmarkedBuffer(&Master, NavPlainPre, 0, PageType);
} break;
default: break;
}
CopyLandmarkedBuffer(&Master, NavGeneric, 0, PageType);
if(Template->Metadata.Tags[i].NavigationType == NT_DROPDOWN)
{
CopyLandmarkedBuffer(&Master, NavDropdownPost, 0, PageType);
}
}
case TAG_CUSTOM0: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom0, sizeof(CollationBuffers->Custom0))); break;
case TAG_CUSTOM1: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom1, sizeof(CollationBuffers->Custom1))); break;
case TAG_CUSTOM2: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom2, sizeof(CollationBuffers->Custom2))); break;
case TAG_CUSTOM3: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom3, sizeof(CollationBuffers->Custom3))); break;
case TAG_CUSTOM4: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom4, sizeof(CollationBuffers->Custom4))); break;
case TAG_CUSTOM5: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom5, sizeof(CollationBuffers->Custom5))); break;
case TAG_CUSTOM6: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom6, sizeof(CollationBuffers->Custom6))); break;
case TAG_CUSTOM7: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom7, sizeof(CollationBuffers->Custom7))); break;
case TAG_CUSTOM8: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom8, sizeof(CollationBuffers->Custom8))); break;
case TAG_CUSTOM9: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom9, sizeof(CollationBuffers->Custom9))); break;
case TAG_CUSTOM10: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom10, sizeof(CollationBuffers->Custom10))); break;
case TAG_CUSTOM11: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom11, sizeof(CollationBuffers->Custom11))); break;
case TAG_CUSTOM12: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom12, sizeof(CollationBuffers->Custom12))); break;
case TAG_CUSTOM13: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom13, sizeof(CollationBuffers->Custom13))); break;
case TAG_CUSTOM14: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom14, sizeof(CollationBuffers->Custom14))); break;
case TAG_CUSTOM15: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom15, sizeof(CollationBuffers->Custom15))); break;
}
DepartComment(&Template->File.Buffer);
}
while(Template->File.Buffer.Ptr - Template->File.Buffer.Location < Template->File.Buffer.Size)
{
*Master.Ptr++ = *Template->File.Buffer.Ptr++;
}
FILE *OutFile;
if(!(OutFile = fopen(OutputPath, "w")))
{
LogError(LOG_ERROR, "Unable to open output file %s: %s", OutputPath, strerror(errno));
FreeBuffer(&Master);
#if DEBUG_MEM
MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, " Freed Master\n");
fclose(MemLog);
printf(" Freed Master\n");
#endif
MEM_TEST_AFTER("BuffersToHTML");
return RC_ERROR_FILE;
}
fwrite(Master.Location, Master.Ptr - Master.Location, 1, OutFile);
fclose(OutFile);
FreeBuffer(&Master);
#if DEBUG_MEM
MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, " Freed Master\n");
fclose(MemLog);
printf(" Freed Master\n");
#endif
MEM_TEST_AFTER("BuffersToHTML");
return RC_SUCCESS;
}
else
{
MEM_TEST_AFTER("BuffersToHTML");
return RC_INVALID_TEMPLATE;
}
#endif // AFE
MEM_TEST_AFTER("BuffersToHTML");
return RC_SUCCESS; // NOTE(matt): We added this simply to squash the "end of non-void function" warning
}
else
{
MEM_TEST_MID("BuffersToHTML1");
buffer Master = {};
Master.Size = Kilobytes(512);
Master.ID = BID_MASTER;
MEM_TEST_MID("BuffersToHTML2");
if(!(Master.Location = malloc(Master.Size)))
{
LogError(LOG_ERROR, "BuffersToHTML(): %s",
strerror(errno));
MEM_TEST_AFTER("BuffersToHTML");
return RC_ERROR_MEMORY;
}
MEM_TEST_MID("BuffersToHTML3");
Master.Ptr = Master.Location;
CopyStringToBuffer(&Master,
"<html>\n"
" <head>\n"
" ");
MEM_TEST_MID("BuffersToHTML4");
CopyLandmarkedBuffer(&Master, PageType == PAGE_PLAYER ? &CollationBuffers->IncludesPlayer : &CollationBuffers->IncludesSearch, 0, PageType);
MEM_TEST_MID("BuffersToHTML5");
CopyStringToBuffer(&Master, "\n");
CopyStringToBuffer(&Master,
" </head>\n"
" <body>\n"
" ");
if(PageType == PAGE_PLAYER)
{
CopyStringToBuffer(&Master, "<div>\n"
" ");
CopyLandmarkedBuffer(&Master, &CollationBuffers->Player, PlayerOffset, PageType);
MEM_TEST_MID("BuffersToHTML9");
CopyStringToBuffer(&Master, "\n");
}
else
{
MEM_TEST_MID("BuffersToHTML10");
CopyLandmarkedBuffer(&Master, &CollationBuffers->Search, 0, PageType);
MEM_TEST_MID("BuffersToHTML11");
}
CopyStringToBuffer(&Master,
" </body>\n"
"</html>\n");
FILE *OutFile;
MEM_TEST_MID("BuffersToHTML12");
if(!(OutFile = fopen(OutputPath, "w")))
{
LogError(LOG_ERROR, "Unable to open output file %s: %s", OutputPath, strerror(errno));
DeclaimBuffer(&Master);
MEM_TEST_AFTER("BuffersToHTML");
return RC_ERROR_FILE;
}
MEM_TEST_MID("BuffersToHTML13");
fwrite(Master.Location, Master.Ptr - Master.Location, 1, OutFile);
MEM_TEST_MID("BuffersToHTML13");
fclose(OutFile);
MEM_TEST_MID("BuffersToHTML14");
OutFile = 0;
FreeBuffer(&Master);
MEM_TEST_AFTER("BuffersToHTML");
return RC_SUCCESS;
}
}
int
BinarySearchForMetadataEntry(db_header_project *Header, db_entry **Entry, string SearchTerm)
{
char *Ptr = (char *)Header;
Ptr += sizeof(*Header);
db_entry *FirstEntry = (db_entry *)Ptr;
int Lower = 0;
db_entry *LowerEntry = FirstEntry + Lower;
if(StringsDiffer(SearchTerm, Wrap0i(LowerEntry->HMMLBaseFilename, sizeof(LowerEntry->HMMLBaseFilename))) < 0 ) { return -1; }
int Upper = Header->EntryCount - 1;
int Pivot = Upper - ((Upper - Lower) >> 1);
db_entry *UpperEntry;
db_entry *PivotEntry;
do {
LowerEntry = FirstEntry + Lower;
PivotEntry = FirstEntry + Pivot;
UpperEntry = FirstEntry + Upper;
if(!StringsDiffer(SearchTerm, Wrap0i(LowerEntry->HMMLBaseFilename, sizeof(LowerEntry->HMMLBaseFilename)))) { *Entry = LowerEntry; return Lower; }
if(!StringsDiffer(SearchTerm, Wrap0i(PivotEntry->HMMLBaseFilename, sizeof(PivotEntry->HMMLBaseFilename)))) { *Entry = PivotEntry; return Pivot; }
if(!StringsDiffer(SearchTerm, Wrap0i(UpperEntry->HMMLBaseFilename, sizeof(UpperEntry->HMMLBaseFilename)))) { *Entry = UpperEntry; return Upper; }
if(StringsDiffer(SearchTerm, Wrap0i(PivotEntry->HMMLBaseFilename, sizeof(PivotEntry->HMMLBaseFilename))) < 0) { Upper = Pivot; }
else { Lower = Pivot; }
Pivot = Upper - ((Upper - Lower) >> 1);
} while(Upper > Pivot);
return Upper;
}
void
InitIndexFile(project *P)
{
DB.File.Buffer.ID = BID_DATABASE;
DB.File = InitFile(&P->BaseDir, &P->ID, EXT_INDEX);
ReadFileIntoBuffer(&DB.File); // NOTE(matt): Could we actually catch errors (permissions?) here and bail?
if(!DB.File.Buffer.Location)
{
char *BaseDir0 = MakeString0("l", &P->BaseDir);
DIR *OutputDirectoryHandle = opendir(BaseDir0);
if(!OutputDirectoryHandle)
{
if(!MakeDir(P->BaseDir))
{
LogError(LOG_ERROR, "Unable to create directory %.*s: %s", (int)P->BaseDir.Length, P->BaseDir.Base, strerror(errno));
fprintf(stderr, "Unable to create directory %.*s: %s\n", (int)P->BaseDir.Length, P->BaseDir.Base, strerror(errno));
Free(BaseDir0);
return;
};
}
Free(BaseDir0);
closedir(OutputDirectoryHandle);
DB.File.Handle = fopen(DB.File.Path, "w");
fprintf(DB.File.Handle, "---\n");
fclose(DB.File.Handle);
ReadFileIntoBuffer(&DB.File);
}
}
uint64_t
AccumulateDBEntryInsertionOffset(db_header_project *Header, int EntryIndex)
{
uint64_t Result = 0;
char *Ptr = (char *)Header;
Ptr += sizeof(*Header);
db_entry *Entry = (db_entry *)Ptr + EntryIndex;
if(EntryIndex < Header->EntryCount >> 1)
{
--Entry;
for(; EntryIndex > 0; --EntryIndex)
{
Result += Entry->Size;
--Entry;
}
Result += StringLength("---\n");
}
else
{
for(; EntryIndex < Header->EntryCount; ++EntryIndex)
{
Result += Entry->Size;
++Entry;
}
if(!DB.File.Buffer.Location)
{
InitIndexFile(CurrentProject);
}
Result = DB.File.Buffer.Size - Result;
}
return Result;
}
void
ClearEntry(db_entry *Entry)
{
db_entry Zero = {};
*Entry = Zero;
Entry->ArtIndex = SAI_UNSET;
}
void
ResetNeighbourhood(neighbourhood *N)
{
N->Prev = 0;
N->This = 0;
N->Next = 0;
DB.Metadata.Signposts.Prev.Ptr = N->Prev;
DB.Metadata.Signposts.This.Ptr = N->This;
DB.Metadata.Signposts.Next.Ptr = N->Next;
ClearEntry(&N->WorkingThis);
N->PrevIndex = -1;
N->PreDeletionThisIndex = N->ThisIndex = 0;
N->NextIndex = -1;
N->PreLinkPrevOffsetTotal = N->PreLinkThisOffsetTotal = N->PreLinkNextOffsetTotal = 0;
N->PrevOffsetModifier = N->ThisOffsetModifier = N->NextOffsetModifier = 0;
N->FormerIsFirst = N->LatterIsFinal = FALSE;
N->DeletedEntryWasFirst = N->DeletedEntryWasFinal = FALSE;
}
void
InitNeighbourhood(neighbourhood *N)
{
ResetNeighbourhood(N);
// TODO(matt): Does this whole thing actually have to happen? Wouldn't it be enough simply to set N->PrevIndex and
// N->NextIndex to -1?
// TODO(matt): This is completely bogus! Probably take the project_index, so we can get the correct neighbourhood?
// AFD
// TODO(matt): To get us going, I think we want to make this first call LocateBlock(), and initialise a PROJ block if that
// fails.
//
// After having located / initialised that PROJ block, we should then try and locate the Project itself, in
// turn initialising one if we fail to locate it
//
// So we'll be doing some of the work of InitDB()
//PrintConfig(Config);
//CurrentProject = GetProjectFromProject(GetProjectFromProject(GetProjectFromConfig(Config, Wrap0("riscy")), Wrap0("book")), Wrap0("reader"));
// TODO(matt): Why is this Generation off by one? riscy/book/reader should be 2:1, but we're seeing it here as 3:1
#if DEBUG_PROJECT_INDICES
fprintf(stderr, "\n%u: %u\n", CurrentProject->Index.Generation, CurrentProject->Index.Index);
#endif
//_exit(0);
N->Project = LocateProject(CurrentProject->Index);
if(!N->Project)
{
Colourise(CS_ERROR);
fprintf(stderr, "Project %u:%u could not be located in DB\n", CurrentProject->Index.Generation, CurrentProject->Index.Index);
Colourise(CS_END);
_exit(1);
}
if(StringsDiffer(CurrentProject->ID, Wrap0i(N->Project->ID, sizeof(N->Project->ID))))
{
string StoredID = Wrap0i(N->Project->ID, sizeof(N->Project->ID));
Colourise(CS_ERROR);
fprintf(stderr, "Project in DB located by ProjectIndex (%.*s) does not match the project got from the config (%.*s)\n", (int)StoredID.Length, StoredID.Base, (int)CurrentProject->ID.Length, CurrentProject->ID.Base);
Colourise(CS_END);
_exit(1);
}
else
{
#if DEBUG_PROJECT_INDICES
Colourise(CS_SUCCESS);
fprintf(stderr, "Project \"%.*s\" [%u:%u] found. Proceeding!\n", (int)CurrentProject->Lineage.Length, CurrentProject->Lineage.Base, CurrentProject->Index.Generation, CurrentProject->Index.Index);
Colourise(CS_END);
#endif
}
//_exit(1);
DB.Metadata.Signposts.ProjectHeader.Ptr = N->Project;
}
char *NeighbourhoodMemberStrings[] =
{
"Prev",
"This",
"Working This",
"Next"
};
typedef enum
{
NM_PREV,
NM_THIS,
NM_WORKING_THIS,
NM_NEXT
} neighbourhood_member;
void
PrintEntry(db_entry *E, int16_t EIndex, neighbourhood_member M)
{
fprintf(stderr,
" %s [%d]: %s: %6d %6d %6d %6d\n",
NeighbourhoodMemberStrings[M],
EIndex,
E->HMMLBaseFilename,
E->LinkOffsets.PrevStart,
E->LinkOffsets.PrevEnd,
E->LinkOffsets.NextStart,
E->LinkOffsets.NextEnd);
}
#define PrintNeighbourhood(N) PrintNeighbourhood_(N, __LINE__)
void
PrintNeighbourhood_(neighbourhood *N, int Line)
{
printf( "\n"
" Neighbourhood (line %d):\n", Line);
if(N->Prev)
{
PrintEntry(N->Prev, N->PrevIndex, NM_PREV);
}
PrintEntry(&N->WorkingThis, N->ThisIndex, NM_WORKING_THIS);
if(N->This)
{
fprintf(stderr, "(Pre-deletion index %d) ", N->PreDeletionThisIndex);
PrintEntry(N->This, N->ThisIndex, NM_THIS);
}
if(N->Next)
{
PrintEntry(N->Next, N->NextIndex, NM_NEXT);
}
fprintf(stderr,
" OffsetModifiers: Prev %6d • This %6d • Next %6d\n"
" PreLinkOffsetTotals: Prev %6d • This %6d • Next %6d\n"
" DeletedEntryWasFirst: %s\n"
" DeletedEntryWasFinal: %s\n"
" FormerIsFirst: %s\n"
" LatterIsFinal: %s\n"
"\n",
N->PrevOffsetModifier, N->ThisOffsetModifier, N->NextOffsetModifier,
N->PreLinkPrevOffsetTotal, N->PreLinkThisOffsetTotal, N->PreLinkNextOffsetTotal,
N->DeletedEntryWasFirst ? "yes" : "no", N->DeletedEntryWasFinal ? "yes" : "no",
N->FormerIsFirst ? "yes" : "no", N->LatterIsFinal ? "yes" : "no");
}
void
SetNeighbour(db_entry *Dest, db_entry *Src)
{
Dest->Size = Src->Size;
Dest->LinkOffsets.PrevStart = Src->LinkOffsets.PrevStart;
Dest->LinkOffsets.PrevEnd = Src->LinkOffsets.PrevEnd;
Dest->LinkOffsets.NextStart = Src->LinkOffsets.NextStart;
Dest->LinkOffsets.NextEnd = Src->LinkOffsets.NextEnd;
ClearCopyString(Dest->HMMLBaseFilename, sizeof(Dest->HMMLBaseFilename), "%s", Src->HMMLBaseFilename);
ClearCopyString(Dest->OutputLocation, sizeof(Dest->OutputLocation), "%s", Src->HMMLBaseFilename);
ClearCopyString(Dest->Title, sizeof(Dest->Title), "%s", Src->Title);
}
void
GetNeighbourhoodForAddition(neighbourhood *N, edit_type_id EditType)
{
N->FormerIsFirst = TRUE;
int EntryIndex = N->ThisIndex - 1;
char *Ptr = (char *)N->Project;
Ptr += sizeof(*N->Project);
db_entry *FirstEntry = (db_entry *)Ptr;
for(; EntryIndex >= 0; --EntryIndex)
{
db_entry *Entry = FirstEntry + EntryIndex;
if(Entry->Size > 0)
{
if(!N->Prev)
{
N->PrevIndex = EntryIndex;
N->Prev = Entry;
}
else
{
N->FormerIsFirst = FALSE;
break;
}
}
}
switch(EditType)
{
case EDIT_REINSERTION: EntryIndex = N->ThisIndex + 1; break;
case EDIT_ADDITION: EntryIndex = N->ThisIndex; break;
default: break;
}
N->LatterIsFinal = TRUE;
for(; EntryIndex < N->Project->EntryCount;
++EntryIndex)
{
db_entry *Entry = FirstEntry + EntryIndex;
if(Entry->Size > 0)
{
if(!N->Next)
{
N->NextIndex = EntryIndex;
N->Next = Entry;
}
else
{
N->LatterIsFinal = FALSE;
break;
}
}
}
if(EditType == EDIT_ADDITION && N->Next)
{
++N->NextIndex;
}
}
void
GetNeighbourhoodForDeletion(neighbourhood *N)
{
char *Ptr = (char *)N->Project;
Ptr += sizeof(*N->Project);
db_entry *FirstEntry = (db_entry *)Ptr;
db_entry *Entry = FirstEntry + N->ThisIndex;
N->PreDeletionThisIndex = N->ThisIndex;
N->This = Entry;
int EntryIndex;
N->DeletedEntryWasFirst = TRUE;
N->FormerIsFirst = TRUE;
for(EntryIndex = N->PreDeletionThisIndex - 1; EntryIndex >= 0; --EntryIndex)
{
Entry = FirstEntry + EntryIndex;
if(Entry->Size > 0)
{
if(!N->Prev)
{
N->DeletedEntryWasFirst = FALSE;
N->PrevIndex = EntryIndex;
N->Prev = Entry;
}
else
{
N->FormerIsFirst = FALSE;
break;
}
}
}
N->DeletedEntryWasFinal = TRUE;
N->LatterIsFinal = TRUE;
for(EntryIndex = N->PreDeletionThisIndex + 1; EntryIndex < N->Project->EntryCount; ++EntryIndex)
{
Entry = FirstEntry + EntryIndex;
if(Entry->Size > 0)
{
if(!N->Next)
{
N->DeletedEntryWasFinal = FALSE;
N->NextIndex = EntryIndex - 1;
N->Next = Entry;
}
else
{
N->LatterIsFinal = FALSE;
break;
}
}
}
}
void
GetNeighbourhood(neighbourhood *N, edit_type_id EditType)
{
// TODO(matt): We could probably get rid of the N->ThisIndex and friends entirely, in favour of the pointers
if(EditType == EDIT_DELETION)
{
GetNeighbourhoodForDeletion(N);
}
else
{
GetNeighbourhoodForAddition(N, EditType);
}
DB.Metadata.Signposts.Prev.Ptr = N->Prev;
DB.Metadata.Signposts.This.Ptr = N->This;
DB.Metadata.Signposts.Next.Ptr = N->Next;
}
db_entry *
InsertIntoDB(neighbourhood *N, buffers *CollationBuffers, template *BespokeTemplate, string BaseFilename, bool RecheckingPrivacy, bool *Reinserting)
{
MEM_TEST_INITIAL();
//MEM_TEST_MID("InsertIntoDB1");
ResetNeighbourhood(N);
edit_type_id EditType = EDIT_APPEND;
int EntryInsertionStart = StringLength("---\n");
int EntryInsertionEnd = 0;
if(N->Project->EntryCount > 0)
{
db_entry *FirstEntry = LocateFirstEntry(N->Project);
N->ThisIndex = BinarySearchForMetadataEntry(N->Project, &N->This, BaseFilename);
if(N->This)
{
// Reinsert
*Reinserting = TRUE;
EntryInsertionStart = AccumulateDBEntryInsertionOffset(N->Project, N->ThisIndex);
EntryInsertionEnd = EntryInsertionStart + N->This->Size;
EditType = EDIT_REINSERTION;
N->WorkingThis = *N->This;
}
else
{
if(N->ThisIndex == -1) { ++N->ThisIndex; } // NOTE(matt): BinarySearchForMetadataEntry returns -1 if search term precedes the set
db_entry *Test = FirstEntry + N->ThisIndex;
//N->WorkingThis = *(FirstEntry + N->ThisIndex);
if(StringsDiffer(BaseFilename, Wrap0i(Test->HMMLBaseFilename, sizeof(Test->HMMLBaseFilename))) < 0)
{
// Insert
EditType = EDIT_INSERTION;
}
else
{
// Append
++N->ThisIndex;
EditType = EDIT_APPEND;
}
EntryInsertionStart = AccumulateDBEntryInsertionOffset(N->Project, N->ThisIndex);
}
GetNeighbourhood(N, *Reinserting ? EDIT_REINSERTION : EDIT_ADDITION);
}
bool VideoIsPrivate = FALSE;
//MEM_TEST_MID("InsertIntoDB2");
switch(HMMLToBuffers(CollationBuffers, BespokeTemplate, BaseFilename, N))
{
// TODO(matt): Actually sort out the fatality of these cases
case RC_ERROR_FILE:
case RC_ERROR_FATAL:
case RC_ERROR_HMML:
case RC_ERROR_MAX_REFS:
case RC_ERROR_QUOTE:
case RC_INVALID_REFERENCE:
return 0;
case RC_PRIVATE_VIDEO:
VideoIsPrivate = TRUE;
case RC_SUCCESS:
break;
}
//MEM_TEST_MID("InsertIntoDB3");
ClearCopyStringNoFormat(N->WorkingThis.HMMLBaseFilename, sizeof(N->WorkingThis.HMMLBaseFilename), BaseFilename);
if(!VideoIsPrivate) { ClearCopyStringNoFormat(N->WorkingThis.Title, sizeof(N->WorkingThis.Title), Wrap0i(CollationBuffers->Title, sizeof(CollationBuffers->Title))); }
if(!DB.File.Buffer.Location)
{
InitIndexFile(CurrentProject);
}
if(EditType == EDIT_REINSERTION)
{
// NOTE(matt): To save opening the DB.Metadata file, we defer sniping N->This in until InsertNeighbourLink()
if(!VideoIsPrivate)
{
*N->This = N->WorkingThis;
if(!(DB.File.Handle = fopen(DB.File.Path, "w"))) { return 0; }
fwrite(DB.File.Buffer.Location, EntryInsertionStart, 1, DB.File.Handle);
fwrite(CollationBuffers->SearchEntry.Location, N->This->Size, 1, DB.File.Handle);
fwrite(DB.File.Buffer.Location + EntryInsertionEnd, DB.File.Buffer.Size - EntryInsertionEnd, 1, DB.File.Handle);
fclose(DB.File.Handle);
if(N->This->Size == EntryInsertionEnd - EntryInsertionStart)
{
DB.File.Buffer.Ptr = DB.File.Buffer.Location + EntryInsertionStart;
CopyBufferSized(&DB.File.Buffer, &CollationBuffers->SearchEntry, N->This->Size);
}
else
{
FreeBuffer(&DB.File.Buffer);
ReadFileIntoBuffer(&DB.File);
}
}
}
else
{
++N->Project->EntryCount;
char *Ptr = (char*)N->Project;
Ptr += sizeof(*N->Project) + sizeof(db_entry) * N->ThisIndex;
uint64_t BytesIntoFile = Ptr - DB.Metadata.File.Buffer.Location;
if(!(DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"))) { return 0; }
fwrite(DB.Metadata.File.Buffer.Location, BytesIntoFile, 1, DB.Metadata.File.Handle);
SetFileEditPosition(&DB.Metadata);
fwrite(&N->WorkingThis, sizeof(N->WorkingThis), 1, DB.Metadata.File.Handle);
AccumulateFileEditSize(&DB.Metadata, sizeof(N->WorkingThis));
fwrite(DB.Metadata.File.Buffer.Location + BytesIntoFile, DB.Metadata.File.Buffer.Size - BytesIntoFile, 1, DB.Metadata.File.Handle);
CycleSignpostedFile(&DB.Metadata);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
char *EntryInDB = DB.Metadata.File.Buffer.Location + BytesIntoFile;
N->This = (db_entry *)EntryInDB;
if(!(DB.File.Handle = fopen(DB.File.Path, "w"))) { return 0; }
fwrite(DB.File.Buffer.Location, EntryInsertionStart, 1, DB.File.Handle);
fwrite(CollationBuffers->SearchEntry.Location, N->This->Size, 1, DB.File.Handle);
fwrite(DB.File.Buffer.Location + EntryInsertionStart, DB.File.Buffer.Size - EntryInsertionStart, 1, DB.File.Handle);
CycleFile(&DB.File);
}
if(!VideoIsPrivate)
{
#if 0
LogError(LOG_NOTICE, "%s %.*s/%.*s - %s", EditTypes[EditType].Name, (int)CurrentProject->Lineage.Length, CurrentProject->Lineage.Base, (int)BaseFilename.Length, BaseFilename.Base, CollationBuffers->Title);
fprintf(stderr, "%s%s%s %.*s/%.*s - %s\n",
ColourStrings[EditTypes[EditType].Colour], EditTypes[EditType].Name, ColourStrings[CS_END], (int)CurrentProject->Lineage.Length, CurrentProject->Lineage.Base, (int)BaseFilename.Length, BaseFilename.Base, CollationBuffers->Title);
#else
LogError(LOG_NOTICE, "%s %.*s/%.*s - %s", EditTypes[EditType].Name, (int)CurrentProject->Lineage.Length, CurrentProject->Lineage.Base, (int)BaseFilename.Length, BaseFilename.Base, CollationBuffers->Title);
fprintf(stderr, "%s%s%s ", ColourStrings[EditTypes[EditType].Colour], EditTypes[EditType].Name, ColourStrings[CS_END]);
PrintLineageAndEntry(CurrentProject->Lineage, BaseFilename, Wrap0(CollationBuffers->Title), TRUE);
#endif
}
else if(!RecheckingPrivacy)
{
LogError(LOG_NOTICE, "Privately %s %.*s/%.*s", EditTypes[EditType].Name, (int)CurrentProject->Lineage.Length, CurrentProject->Lineage.Base, (int)BaseFilename.Length, BaseFilename.Base);
fprintf(stderr, "%sPrivately %s%s ", ColourStrings[CS_PRIVATE], EditTypes[EditType].Name, ColourStrings[CS_END]);
PrintLineageAndEntry(CurrentProject->Lineage, BaseFilename, Wrap0(CollationBuffers->Title), TRUE);
}
// TODO(matt): Remove VideoIsPrivate in favour of generating a player page in a random location
MEM_TEST_AFTER("InsertIntoDB()");
return VideoIsPrivate ? 0 : N->This;
}
void
WritePastAssetsHeader(void)
{
DB.Metadata.Signposts.AssetsBlock.Ptr = LocateBlock(B_ASET);
DB.Metadata.File.Buffer.Ptr = DB.Metadata.Signposts.AssetsBlock.Ptr;
DB.Metadata.File.Buffer.Ptr += sizeof(db_block_assets);
DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w");
fwrite(DB.Metadata.File.Buffer.Location, DB.Metadata.File.Buffer.Ptr - DB.Metadata.File.Buffer.Location, 1, DB.Metadata.File.Handle);
}
void
ProcessPrevLandmarks(neighbourhood *N, db_asset *Asset, landmark_range ProjectRange, landmark_range CurrentTarget, int *RunningIndex)
{
db_landmark *FirstLandmark = LocateFirstLandmark(Asset);
if(N->Prev)
{
//landmark_range FormerTarget = BinarySearchForMetadataLandmark(Asset, CurrentProject->Index, N->PrevIndex, ExistingLandmarkCount);
landmark_range FormerTarget = {};
if(CurrentTarget.First > 0)
{
//db_landmark *Prior = FirstLandmark - 1;
FormerTarget = GetIndexRange(Asset, ProjectRange, CurrentTarget.First - 1);
}
fwrite(FirstLandmark, sizeof(db_landmark), FormerTarget.First, DB.Metadata.File.Handle);
*RunningIndex += FormerTarget.First;
for(int j = 0; j < FormerTarget.Length; ++j, ++*RunningIndex)
{
db_landmark *Landmark = FirstLandmark + *RunningIndex;
if(Landmark->EntryIndex >= 0 && Landmark->Position >= N->PreLinkPrevOffsetTotal)
{
Landmark->Position += N->PrevOffsetModifier;
}
fwrite(Landmark, sizeof(db_landmark), 1, DB.Metadata.File.Handle);
}
}
else
{
//fwrite(FirstLandmark + sizeof(db_landmark) * *RunningIndex, sizeof(db_landmark), CurrentTarget.First, DB.Metadata.File.Handle);
fwrite(FirstLandmark, sizeof(db_landmark), CurrentTarget.First, DB.Metadata.File.Handle);
*RunningIndex += CurrentTarget.First;
}
}
void
ProcessNextLandmarks(neighbourhood *N, db_asset *Asset, landmark_range ProjectRange, landmark_range CurrentTarget,
int ExistingLandmarkCount, int *RunningIndex, edit_type_id EditType)
{
db_landmark *FirstLandmark = LocateFirstLandmark(Asset);
if(N->Next && *RunningIndex < ProjectRange.First + ProjectRange.Length)
{
//db_landmark *Latter = FirstLandmark + *RunningIndex;
landmark_range LatterTarget = GetIndexRange(Asset, ProjectRange, *RunningIndex);
//BinarySearchForMetadataLandmark(Asset, CurrentProject->Index, Landmark->EntryIndex, ExistingLandmarkCount);
for(int j = 0; j < LatterTarget.Length; ++j)
{
db_landmark *Landmark = FirstLandmark + LatterTarget.First + j;
if(Landmark->Position >= N->PreLinkNextOffsetTotal)
{
Landmark->Position += N->NextOffsetModifier;
}
}
for(; *RunningIndex < ProjectRange.First + ProjectRange.Length; ++*RunningIndex)
{
db_landmark *Landmark = FirstLandmark + *RunningIndex;
switch(EditType)
{
case EDIT_DELETION: --Landmark->EntryIndex; break;
case EDIT_ADDITION: ++Landmark->EntryIndex; break;
default: break;
}
fwrite(Landmark, sizeof(db_landmark), 1, DB.Metadata.File.Handle);
}
}
if(*RunningIndex < ExistingLandmarkCount)
{
db_landmark *Landmark = FirstLandmark + *RunningIndex;
fwrite(Landmark, sizeof(db_landmark), ExistingLandmarkCount - *RunningIndex, DB.Metadata.File.Handle);
}
}
typedef struct
{
uint64_t Index;
uint64_t Location;
} asset_index_and_location;
void
OffsetAssociatedIndex(asset_index_and_location *AssetDeletionRecords, int AssetDeletionRecordCount, int32_t *Index)
{
fprintf(stderr, "Possibly offsetting associated index\n");
for(int i = AssetDeletionRecordCount - 1; i >= 0; --i)
{
if(*Index > AssetDeletionRecords[i].Index)
{
--*Index;
}
}
}
void *
OffsetAssociatedIndicesOfProject(db_header_project *Project, asset_index_and_location *AssetDeletionRecords, int AssetDeletionRecordCount)
{
if(Project->ArtIndex >= 0)
{
OffsetAssociatedIndex(AssetDeletionRecords, AssetDeletionRecordCount, &Project->ArtIndex);
}
if(Project->IconIndex >= 0)
{
OffsetAssociatedIndex(AssetDeletionRecords, AssetDeletionRecordCount, &Project->IconIndex);
}
db_entry *Entry = LocateFirstEntry(Project);
for(int j = 0; j < Project->EntryCount; ++j, ++Entry)
{
if(Entry->ArtIndex >= 0)
{
OffsetAssociatedIndex(AssetDeletionRecords, AssetDeletionRecordCount, &Entry->ArtIndex);
}
}
db_header_project *Child = LocateFirstChildProject(Project);
for(int ChildIndex = 0; ChildIndex < Project->ChildCount; ++ChildIndex)
{
Child = OffsetAssociatedIndicesOfProject(Child, AssetDeletionRecords, AssetDeletionRecordCount);
}
return SkipProjectAndChildren(Project);
}
void
OffsetAssociatedIndices(asset_index_and_location *AssetDeletionRecords, int AssetDeletionRecordCount)
{
db_block_projects *ProjectsBlock = LocateBlock(B_PROJ);
db_header_project *Project = LocateFirstChildProjectOfBlock(ProjectsBlock);
for(int i = 0; i < ProjectsBlock->Count; ++i)
{
Project = OffsetAssociatedIndicesOfProject(Project, AssetDeletionRecords, AssetDeletionRecordCount);
}
}
void
DeleteStaleAssets(void)
{
int AssetDeletionCount = 0;
DB.Metadata.Signposts.AssetsBlock.Ptr = LocateBlock(B_ASET);
db_block_assets *AssetsBlock = DB.Metadata.Signposts.AssetsBlock.Ptr;
int AssetsCount = AssetsBlock->Count;
// TODO(matt): Put this stuff on the heap. possibly once our memory situation is straightened out
asset_index_and_location AssetDeletionRecords[AssetsBlock->Count];
db_asset *Asset = LocateFirstAsset(AssetsBlock);
for(int AssetIndex = 0; AssetIndex < AssetsCount; ++AssetIndex)
{
if(Asset->LandmarkCount == 0 && !Asset->Associated)
{
AssetDeletionRecords[AssetDeletionCount].Location = (char *)Asset - DB.Metadata.File.Buffer.Location;
AssetDeletionRecords[AssetDeletionCount].Index = AssetIndex;
++AssetDeletionCount;
--AssetsBlock->Count;
}
Asset = SkipAsset(Asset);
}
if(AssetDeletionCount > 0)
{
OffsetAssociatedIndices(AssetDeletionRecords, AssetDeletionCount);
WritePastAssetsHeader();
SetFileEditPosition(&DB.Metadata);
uint64_t BytesIntoFile = DB.Metadata.File.Buffer.Ptr - DB.Metadata.File.Buffer.Location;
for(int DeletionIndex = 0; DeletionIndex < AssetDeletionCount; ++DeletionIndex)
{
DB.Metadata.File.Buffer.Ptr = DB.Metadata.File.Buffer.Location + AssetDeletionRecords[DeletionIndex].Location;
fwrite(DB.Metadata.File.Buffer.Location + BytesIntoFile,
(DB.Metadata.File.Buffer.Ptr - DB.Metadata.File.Buffer.Location) - BytesIntoFile,
1,
DB.Metadata.File.Handle);
BytesIntoFile += (DB.Metadata.File.Buffer.Ptr - DB.Metadata.File.Buffer.Location) - BytesIntoFile + sizeof(db_asset);
}
AccumulateFileEditSize(&DB.Metadata, -sizeof(db_asset) * AssetDeletionCount);
WriteFromByteToEnd(&DB.Metadata.File, BytesIntoFile);
CycleSignpostedFile(&DB.Metadata);
}
}
void
DeleteAllLandmarksAndAssets(void)
{
// TODO(matt): Test this!
uint64_t BytesThroughBuffer = 0;
db_block_assets *AssetsBlock = LocateBlock(B_ASET);
WriteFromByteToPointer(&DB.Metadata.File, &BytesThroughBuffer, AssetsBlock);
int AssetsCount = AssetsBlock->Count;
AssetsBlock->Count = 0;
fwrite(AssetsBlock, sizeof(db_block_assets), 1, DB.Metadata.File.Handle);
BytesThroughBuffer += sizeof(db_block_assets);
DB.Metadata.File.Buffer.Ptr = DB.Metadata.File.Buffer.Location + BytesThroughBuffer;
SetFileEditPosition(&DB.Metadata);
for(int AssetIndex = 0; AssetIndex < AssetsCount; ++AssetIndex)
{
db_asset *Asset = (db_asset *)(DB.Metadata.File.Buffer.Location + BytesThroughBuffer);
BytesThroughBuffer += sizeof(db_asset);
AccumulateFileEditSize(&DB.Metadata, -sizeof(db_asset));
BytesThroughBuffer += sizeof(db_landmark) * Asset->LandmarkCount;
AccumulateFileEditSize(&DB.Metadata, -sizeof(db_landmark) * Asset->LandmarkCount);
}
WriteFromByteToEnd(&DB.Metadata.File, BytesThroughBuffer);
CycleSignpostedFile(&DB.Metadata);
}
landmark_range
DetermineProjectLandmarksRange(db_asset *A, db_project_index I)
{
// TODO(matt): Do it in a Binary search fashion
landmark_range Result = {};
db_landmark *Landmark = LocateFirstLandmark(A);
for(; Result.First < A->LandmarkCount; ++Result.First, ++Landmark)
{
if(!(ProjectIndicesDiffer(I, Landmark->Project) > 0))
{
break;
}
}
for(; Result.First + Result.Length < A->LandmarkCount; ++Landmark, ++Result.Length)
{
if(ProjectIndicesDiffer(I, Landmark->Project))
{
break;
}
}
return Result;
}
void
DeleteLandmarks(neighbourhood *N)
{
SetFileEditPosition(&DB.Metadata);
db_block_assets *AssetsBlock = DB.Metadata.Signposts.AssetsBlock.Ptr;
for(int AssetIndex = 0; AssetIndex < AssetsBlock->Count; ++AssetIndex)
{
db_asset *Asset = (db_asset *)DB.Metadata.File.Buffer.Ptr;
DB.Metadata.File.Buffer.Ptr += sizeof(*Asset);
int ExistingLandmarkCount = Asset->LandmarkCount;
int RunningIndex = 0;
landmark_range ProjectRange = DetermineProjectLandmarksRange(Asset, CurrentProject->Index);
landmark_range DeletionTarget = BinarySearchForMetadataLandmark(Asset, ProjectRange, N->PreDeletionThisIndex);
Asset->LandmarkCount -= DeletionTarget.Length;
AccumulateFileEditSize(&DB.Metadata, -sizeof(DB.Landmark) * DeletionTarget.Length);
fwrite(Asset, sizeof(*Asset), 1, DB.Metadata.File.Handle);
ProcessPrevLandmarks(N, Asset, ProjectRange, DeletionTarget, &RunningIndex);
RunningIndex += DeletionTarget.Length;
ProcessNextLandmarks(N, Asset, ProjectRange, DeletionTarget, ExistingLandmarkCount, &RunningIndex, EDIT_DELETION);
DB.Metadata.File.Buffer.Ptr += sizeof(DB.Landmark) * ExistingLandmarkCount;
}
CycleSignpostedFile(&DB.Metadata);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
}
void UpdateLandmarksForNeighbourhood(neighbourhood *N, edit_type_id EditType);
void
AddLandmarks(neighbourhood *N, project *P, edit_type_id EditType)
{
for(int i = 0; i < Assets.Count; ++i)
{
asset *Asset = GetPlaceInBook(&Assets.Asset, i);
Asset->Known = FALSE;
}
SetFileEditPosition(&DB.Metadata);
db_block_assets *AssetsBlock = DB.Metadata.Signposts.AssetsBlock.Ptr;
for(int StoredAssetIndex = 0; StoredAssetIndex < AssetsBlock->Count; ++StoredAssetIndex)
{
db_asset *Asset = (db_asset *)DB.Metadata.File.Buffer.Ptr;
//PrintAssetAndLandmarks(Asset);
int ExistingLandmarkCount = Asset->LandmarkCount;
DB.Metadata.File.Buffer.Ptr += sizeof(*Asset);
int RunningIndex = 0;
for(int i = 0; i < Assets.Count; ++i)
{
asset *AssetInMemory = GetPlaceInBook(&Assets.Asset, i);
// TODO(matt): Exhaustively test BinarySearchForMetadataLandmark() and figure out why ProcessPrevLandmarks() /
// ProcessNextLandmarks() are apparently failing to write out all the existing landmarks
if(!StringsDiffer(Wrap0i(Asset->Filename, sizeof(Asset->Filename)), Wrap0i(AssetInMemory->Filename, sizeof(AssetInMemory->Filename))) && Asset->Type == AssetInMemory->Type)
{
AssetInMemory->Known = TRUE;
landmark_range ProjectRange = DetermineProjectLandmarksRange(Asset, P->Index);
/*
* Existing asset landmarks may all be -1, and only appropriate for a search page
* We have a N->Prev who does not contain this asset
* First batch of asset landmarks may be -1, with following ones >= 0 and appropriate for player pages
* Existing asset landmarks are all >= 0, and appropriate for player pages
*
*/
if(!AssetInMemory->OffsetLandmarks)// && Assets.Asset[i].PlayerLandmarkCount > 0)
{
Asset->LandmarkCount += AssetInMemory->PlayerLandmarkCount;
landmark_range ThisTarget = ProjectRange;
if(ProjectRange.Length > 0)
{
ThisTarget = BinarySearchForMetadataLandmark(Asset, ProjectRange, N->ThisIndex);
if(EditType == EDIT_REINSERTION) { Asset->LandmarkCount -= ThisTarget.Length; }
}
fwrite(Asset, sizeof(*Asset), 1, DB.Metadata.File.Handle);
if(ExistingLandmarkCount > 0)
{
ProcessPrevLandmarks(N, Asset, ProjectRange, ThisTarget, &RunningIndex);
}
for(int j = 0; j < AssetInMemory->PlayerLandmarkCount; ++j)
{
db_landmark Landmark = {};
// TODO(matt): Actually make sure that the correct project_index is set!
Landmark.Project = P->Index;
Landmark.EntryIndex = N->ThisIndex;
Landmark.Position = AssetInMemory->Player[j].Offset;
fwrite(&Landmark, sizeof(Landmark), 1, DB.Metadata.File.Handle);
}
if(ExistingLandmarkCount > 0)
{
if(EditType == EDIT_REINSERTION) { RunningIndex += ThisTarget.Length; }
ProcessNextLandmarks(N, Asset, ProjectRange, ThisTarget, ExistingLandmarkCount, &RunningIndex, EditType);
}
AssetInMemory->OffsetLandmarks = TRUE;
}
else
{
AssetInMemory->OffsetLandmarks = TRUE;
fwrite(Asset, sizeof(*Asset), 1, DB.Metadata.File.Handle);
fwrite(DB.Metadata.File.Buffer.Ptr, sizeof(db_landmark) * ExistingLandmarkCount, 1, DB.Metadata.File.Handle);
}
break;
}
}
// TODO(matt): Is this definitely okay to do here, or should we do it before the break?
AccumulateFileEditSize(&DB.Metadata, sizeof(db_landmark) * (Asset->LandmarkCount - ExistingLandmarkCount));
DB.Metadata.File.Buffer.Ptr += sizeof(db_landmark) * ExistingLandmarkCount;
}
CycleSignpostedFile(&DB.Metadata);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
bool NewAsset = FALSE;
for(int i = 0; i < Assets.Count; ++i)
{
asset *This = GetPlaceInBook(&Assets.Asset, i);
if(!This->Known && This->PlayerLandmarkCount > 0)
{
UpdateAssetInDB(This);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
NewAsset = TRUE;
}
}
if(NewAsset) {
UpdateLandmarksForNeighbourhood(N, EDIT_REINSERTION); // NOTE(matt): EDIT_REINSERTION this time because all existing
// assets will have been updated in the first pass
}
}
void
UpdateLandmarksForNeighbourhood(neighbourhood *N, edit_type_id EditType)
{
if(Config->QueryString.Length > 0)
{
WritePastAssetsHeader();
switch(EditType)
{
case EDIT_DELETION: DeleteLandmarks(N); break;
case EDIT_ADDITION: case EDIT_REINSERTION: { AddLandmarks(N, CurrentProject, EditType); break; }
default: break;
}
}
//PrintAssetsBlock();
}
void
DeleteLandmarksForSearch(db_project_index ProjectIndex)
{
if(Config->QueryString.Length > 0)
{
WritePastAssetsHeader();
SetFileEditPosition(&DB.Metadata);
db_block_assets *AssetsBlock = DB.Metadata.Signposts.AssetsBlock.Ptr;
for(int AssetIndex = 0; AssetIndex < AssetsBlock->Count; ++AssetIndex)
{
db_asset *Asset = (db_asset *)DB.Metadata.File.Buffer.Ptr;
int ExistingLandmarkCount = Asset->LandmarkCount;
landmark_range ProjectRange = DetermineProjectLandmarksRange(Asset, ProjectIndex);
landmark_range DeletionTarget = BinarySearchForMetadataLandmark(Asset, ProjectRange, SP_SEARCH);
Asset->LandmarkCount -= DeletionTarget.Length;
fwrite(Asset, sizeof(*Asset), 1, DB.Metadata.File.Handle);
DB.Metadata.File.Buffer.Ptr += sizeof(*Asset);
fwrite(DB.Metadata.File.Buffer.Ptr, sizeof(db_landmark), DeletionTarget.First, DB.Metadata.File.Handle);
DB.Metadata.File.Buffer.Ptr += sizeof(db_landmark) * DeletionTarget.First;
DB.Metadata.File.Buffer.Ptr += sizeof(db_landmark) * DeletionTarget.Length;
AccumulateFileEditSize(&DB.Metadata, -sizeof(db_landmark) * DeletionTarget.Length);
fwrite(DB.Metadata.File.Buffer.Ptr, sizeof(db_landmark), ExistingLandmarkCount - (DeletionTarget.First + DeletionTarget.Length), DB.Metadata.File.Handle);
DB.Metadata.File.Buffer.Ptr += sizeof(db_landmark) * (ExistingLandmarkCount - (DeletionTarget.First + DeletionTarget.Length));
}
CycleSignpostedFile(&DB.Metadata);
}
}
#define DEBUG_LANDMARKS 0
#if DEBUG_LANDMARKS
#define VerifyLandmarks(N) VerifyLandmarks_(N, __LINE__)
void
VerifyLandmarks_(neighbourhood *N, int LineNumber)
{
fprintf(stderr, "%sVerifyLandmarks(%u)%s\n", ColourStrings[CS_MAGENTA], LineNumber, ColourStrings[CS_END]);
db_block_assets *Block = LocateBlock(B_ASET);
db_asset *Asset = LocateFirstAsset(Block);
for(int i = 0; i < Block->Count; ++i)
{
db_landmark *Landmark = LocateFirstLandmark(Asset);
for(int j = 0; j < Asset->LandmarkCount; ++j, ++Landmark)
{
if(j + 1 < Asset->LandmarkCount)
{
db_landmark *Next = Landmark + 1;
if((ProjectIndicesDiffer(Next->Project, Landmark->Project) < 0) ||
(ProjectIndicesMatch(Next->Project, Landmark->Project) && Next->EntryIndex < Landmark->EntryIndex))
{
PrintC(CS_ERROR, "We fail, sadly\n");
PrintAssetsBlock(0);
PrintLandmark(Landmark);
fprintf(stderr, " vs ");
PrintLandmark(Next);
fprintf(stderr, "\n");
PrintNeighbourhood(N);
_exit(1);
}
}
}
Asset = SkipAsset(Asset);
}
#if 0
DB.Metadata.Buffer.Ptr = DB.Metadata.Buffer.Location;
DB.Header = *(db_header *)DB.Metadata.Buffer.Ptr;
DB.Metadata.Buffer.Ptr += sizeof(DB.Header);
DB.EntriesHeader = *(db_header_entries *)DB.Metadata.Buffer.Ptr;
DB.Metadata.Buffer.Ptr += sizeof(DB.EntriesHeader);
char *FirstEntry = DB.Metadata.Buffer.Ptr;
DB.Metadata.Buffer.Ptr += sizeof(DB.Entry) * DB.EntriesHeader.Count;
DB.AssetsHeader = *(db_header_assets *)DB.Metadata.Buffer.Ptr;
DB.Metadata.Buffer.Ptr += sizeof(DB.AssetsHeader);
char *FirstAsset = DB.Metadata.Buffer.Ptr;
buffer OutputDirectoryPath;
ClaimBuffer(&OutputDirectoryPath, "OutputDirectoryPath", MAX_BASE_DIR_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_BASE_FILENAME_LENGTH + 1 + 10);
bool UniversalSuccess = TRUE;
// Search
{
file_buffer HTML;
ReadSearchPageIntoBuffer(&HTML);
char *Cursor = FirstAsset;
for(int j = 0; j < DB.AssetsHeader.Count; ++j)
{
DB.Asset = *(db_asset *)Cursor;
Cursor += sizeof(DB.Asset);
landmark_range Target = BinarySearchForMetadataLandmark(Cursor, CurrentProject->Index, SP_SEARCH, DB.Asset.LandmarkCount);
for(int k = 0; k < Target.Length; ++k)
{
DB.Landmark = *(db_landmark *)(Cursor + sizeof(db_landmark) * (Target.First + k));
bool Found = FALSE;
for(int l = 0; l < Assets.Count; ++l)
{
// TODO(matt): String0ToInt (base16)
char HashString[9];
sprintf(HashString, "%08x", Assets.Asset[l].Hash);
if(HTML.Buffer.Size >= DB.Landmark.Position + 8 && !StringsDifferT(HashString, HTML.Buffer.Location + DB.Landmark.Position, 0))
{
Found = TRUE;
break;
}
}
if(!Found)
{
printf("Failure!!!\n");
printf(" %s\n", HTML.Path);
printf(" %.*s [at byte %d] is not a known asset hash\n", 8, HTML.Buffer.Location + DB.Landmark.Position, DB.Landmark.Position);
UniversalSuccess = FALSE;
}
}
Cursor += sizeof(DB.Landmark) * DB.Asset.LandmarkCount;
}
FreeBuffer(&HTML.Buffer);
}
for(int i = 0; i < DB.EntriesHeader.Count; ++i)
{
DB.Entry = *(db_entry *)(FirstEntry + sizeof(DB.Entry) * i);
ConstructDirectoryPath(&OutputDirectoryPath, PAGE_PLAYER, CurrentProject->PlayerLocation, DB.Entry.BaseFilename);
file_buffer HTML;
CopyString(HTML.Path, sizeof(HTML.Path), "%s/index.html", OutputDirectoryPath.Location);
if(ReadFileIntoBuffer_OLD(&HTML, 0) == RC_SUCCESS)
{
char *Cursor = FirstAsset;
for(int j = 0; j < DB.AssetsHeader.Count; ++j)
{
DB.Asset = *(db_asset *)Cursor;
Cursor += sizeof(DB.Asset);
landmark_range Target = BinarySearchForMetadataLandmark(Cursor, CurrentProject->Index, i, DB.Asset.LandmarkCount);
for(int k = 0; k < Target.Length; ++k)
{
DB.Landmark = *(db_landmark *)(Cursor + sizeof(db_landmark) * (Target.First + k));
bool Found = FALSE;
for(int l = 0; l < Assets.Count; ++l)
{
// TODO(matt): String0ToInt (base16)
char HashString[9];
sprintf(HashString, "%08x", Assets.Asset[l].Hash);
if((HTML.Buffer.Size >= DB.Landmark.Position + 8) && !StringsDifferT(HashString, HTML.Buffer.Location + DB.Landmark.Position, 0))
{
Found = TRUE;
break;
}
}
if(!Found)
{
printf("%sFailure ↓%s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END]);
printf(" %s\n", HTML.Path);
printf(" %.*s [at byte %d] is not a known asset hash\n", 8, HTML.Buffer.Location + DB.Landmark.Position, DB.Landmark.Position);
UniversalSuccess = FALSE;
}
}
Cursor += sizeof(DB.Landmark) * DB.Asset.LandmarkCount;
}
FreeBuffer(&HTML.Buffer);
}
else
{
printf("%sFailed to open%s %s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], HTML.Path);
}
}
Assert(UniversalSuccess);
printf("Success! All landmarks correspond to asset hashes\n");
DeclaimBuffer(&OutputDirectoryPath);
#endif
}
#else
#define VerifyLandmarks(N)
#endif
void
UpdateLandmarksForSearch(neighbourhood *N, db_project_index ProjectIndex)
{
if(Config->QueryString.Length > 0)
{
for(int i = 0; i < Assets.Count; ++i)
{
asset *This = GetPlaceInBook(&Assets.Asset, i);
This->Known = FALSE;
}
WritePastAssetsHeader();
SetFileEditPosition(&DB.Metadata);
DB.Metadata.Signposts.AssetsBlock.Ptr = LocateBlock(B_ASET);
db_block_assets *AssetsBlock = DB.Metadata.Signposts.AssetsBlock.Ptr;
db_asset *Asset = LocateFirstAsset(AssetsBlock);
for(int AssetIndex = 0; AssetIndex < AssetsBlock->Count; ++AssetIndex)
{
db_landmark *FirstLandmark = LocateFirstLandmark(Asset);
uint64_t ExistingLandmarkCount = Asset->LandmarkCount;
for(int i = 0; i < Assets.Count; ++i)
{
asset *This = GetPlaceInBook(&Assets.Asset, i);
if(!StringsDiffer0(Asset->Filename, This->Filename) && Asset->Type == This->Type)
{
This->Known = TRUE;
landmark_range ProjectRange = DetermineProjectLandmarksRange(Asset, ProjectIndex);
landmark_range Target = BinarySearchForMetadataLandmark(Asset, ProjectRange, SP_SEARCH);
db_asset NewAsset = *Asset;
NewAsset.LandmarkCount += This->SearchLandmarkCount - Target.Length;
fwrite(&NewAsset, sizeof(db_asset), 1, DB.Metadata.File.Handle);
//AccumulateFileEditSize(&DB.Metadata, sizeof(db_asset));
fwrite(FirstLandmark, sizeof(db_landmark), Target.First, DB.Metadata.File.Handle);
//AccumulateFileEditSize(&DB.Metadata, sizeof(db_landmark) * Target.First);
uint64_t ToWrite = ExistingLandmarkCount - Target.First;
for(int j = 0; j < This->SearchLandmarkCount; ++j)
{
db_landmark Landmark = {};
Landmark.Project = ProjectIndex;
Landmark.EntryIndex = SP_SEARCH;
Landmark.Position = This->Search[j].Offset;
fwrite(&Landmark, sizeof(Landmark), 1, DB.Metadata.File.Handle);
//AccumulateFileEditSize(&DB.Metadata, sizeof(db_landmark));
}
db_landmark *TrailingLandmark = FirstLandmark + Target.First + Target.Length;
AccumulateFileEditSize(&DB.Metadata, sizeof(db_landmark) * (This->SearchLandmarkCount - Target.Length));
ToWrite -= Target.Length;
fwrite(TrailingLandmark, sizeof(db_landmark), ToWrite, DB.Metadata.File.Handle);
break;
}
}
Asset = SkipAsset(Asset);
}
CycleSignpostedFile(&DB.Metadata);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
VerifyLandmarks(N);
bool NewAsset = FALSE;
for(int InternalAssetIndex = 0; InternalAssetIndex < Assets.Count; ++InternalAssetIndex)
{
asset *This = GetPlaceInBook(&Assets.Asset, InternalAssetIndex);
if(!This->Known && This->SearchLandmarkCount > 0)
{
NewAsset = TRUE;
UpdateAssetInDB(This);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
}
}
if(NewAsset) { UpdateLandmarksForSearch(N, ProjectIndex); }
}
}
enum
{
LINK_INCLUDE,
LINK_EXCLUDE
} link_types;
enum
{
LINK_FORWARDS,
LINK_BACKWARDS
} link_directions;
int
InsertNeighbourLink(db_header_project *P, db_entry *From, db_entry *To, enum8(link_directions) LinkDirection, bool FromHasOneNeighbour)
{
MEM_TEST_INITIAL();
file HTML = {};
ReadPlayerPageIntoBuffer(P, &HTML, From);
MEM_TEST_MID("InsertNeighbourLink1");
if(HTML.Buffer.Location)
{
if(!(HTML.Handle = fopen(HTML.Path, "w"))) { FreeFile(&HTML); return RC_ERROR_FILE; };
buffer Link = {};
ClaimBuffer(&Link, BID_LINK, Kilobytes(4));
buffer ToPlayerURL = {};
if(To)
{
ClaimBuffer(&ToPlayerURL, BID_TO_PLAYER_URL, MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_BASE_FILENAME_LENGTH);
ConstructPlayerURL(&ToPlayerURL, P, Wrap0i(To->OutputLocation, sizeof(To->OutputLocation)));
}
int NewPrevEnd = 0;
int NewNextEnd = 0;
switch(LinkDirection)
{
case LINK_BACKWARDS:
{
fwrite(HTML.Buffer.Location, From->LinkOffsets.PrevStart, 1, HTML.Handle);
if(To)
{
CopyStringToBuffer(&Link,
" <a class=\"episodeMarker prev\" href=\"%s\"><div>&#9195;</div><div>Previous: '%s'</div><div>&#9195;</div></a>\n",
ToPlayerURL.Location,
To->Title);
}
else
{
// TODO(matt): Be careful of this! Is this CurrentProject->Title definitely set to the right thing?
CopyStringToBuffer(&Link,
" <div class=\"episodeMarker first\"><div>&#8226;</div><div>Welcome to <cite>%.*s</cite></div><div>&#8226;</div></div>\n", (int)CurrentProject->Title.Length, CurrentProject->Title.Base);
}
NewPrevEnd = Link.Ptr - Link.Location;
fwrite(Link.Location, (Link.Ptr - Link.Location), 1, HTML.Handle);
if(FromHasOneNeighbour)
{
fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd, From->LinkOffsets.NextStart, 1, HTML.Handle);
RewindBuffer(&Link);
CopyStringToBuffer(&Link,
" <div class=\"episodeMarker last\"><div>&#8226;</div><div>You have arrived at the (current) end of <cite>%.*s</cite></div><div>&#8226;</div></div>\n", (int)CurrentProject->Title.Length, CurrentProject->Title.Base);
NewNextEnd = Link.Ptr - Link.Location;
fwrite(Link.Location, NewNextEnd, 1, HTML.Handle);
fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd,
HTML.Buffer.Size - (From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd),
1,
HTML.Handle);
}
else
{
fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd,
HTML.Buffer.Size - (From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd),
1,
HTML.Handle);
}
From->LinkOffsets.PrevEnd = NewPrevEnd;
if(FromHasOneNeighbour) { From->LinkOffsets.NextEnd = NewNextEnd; }
} break;
case LINK_FORWARDS:
{
if(FromHasOneNeighbour)
{
fwrite(HTML.Buffer.Location, From->LinkOffsets.PrevStart, 1, HTML.Handle);
CopyStringToBuffer(&Link,
" <div class=\"episodeMarker first\"><div>&#8226;</div><div>Welcome to <cite>%.*s</cite></div><div>&#8226;</div></div>\n", (int)CurrentProject->Title.Length, CurrentProject->Title.Base);
NewPrevEnd = Link.Ptr - Link.Location;
fwrite(Link.Location, NewPrevEnd, 1, HTML.Handle);
RewindBuffer(&Link);
fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd,
From->LinkOffsets.NextStart, 1, HTML.Handle);
}
else
{
fwrite(HTML.Buffer.Location, From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart, 1, HTML.Handle);
}
if(To)
{
CopyStringToBuffer(&Link,
" <a class=\"episodeMarker next\" href=\"%s\"><div>&#9196;</div><div>Next: '%s'</div><div>&#9196;</div></a>\n",
ToPlayerURL.Location,
To->Title);
}
else
{
CopyStringToBuffer(&Link,
" <div class=\"episodeMarker last\"><div>&#8226;</div><div>You have arrived at the (current) end of <cite>%.*s</cite></div><div>&#8226;</div></div>\n", (int)CurrentProject->Title.Length, CurrentProject->Title.Base);
}
NewNextEnd = Link.Ptr - Link.Location;
fwrite(Link.Location, (Link.Ptr - Link.Location), 1, HTML.Handle);
fwrite(HTML.Buffer.Location + From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd,
HTML.Buffer.Size - (From->LinkOffsets.PrevStart + From->LinkOffsets.PrevEnd + From->LinkOffsets.NextStart + From->LinkOffsets.NextEnd),
1,
HTML.Handle);
if(FromHasOneNeighbour) { From->LinkOffsets.PrevEnd = NewPrevEnd; }
From->LinkOffsets.NextEnd = NewNextEnd;
} break;
}
if(To) { DeclaimBuffer(&ToPlayerURL); }
DeclaimBuffer(&Link);
fclose(HTML.Handle);
HTML.Handle = 0;
FreeFile(&HTML);
MEM_TEST_AFTER("InsertNeighbourLink()");
return RC_SUCCESS;
}
else
{
FreeFile(&HTML);
MEM_TEST_AFTER("InsertNeighbourLink()");
return RC_ERROR_FILE;
}
}
int
DeleteNeighbourLinks(neighbourhood *N)
{
// TODO(matt): Compression Oriented Programming!
db_entry *Entry;
if(N->Prev)
{
Entry = N->Prev;
N->PreLinkPrevOffsetTotal = N->Prev->LinkOffsets.PrevEnd
+ N->Prev->LinkOffsets.NextStart
+ N->Prev->LinkOffsets.NextEnd;
}
else
{
Entry = N->Next;
N->PreLinkNextOffsetTotal = N->Next->LinkOffsets.PrevEnd
+ N->Next->LinkOffsets.NextStart
+ N->Next->LinkOffsets.NextEnd;
}
file HTML = {};
ReadPlayerPageIntoBuffer(N->Project, &HTML, Entry);
if(HTML.Buffer.Location)
{
if(!(HTML.Handle = fopen(HTML.Path, "w"))) { FreeFile(&HTML); return RC_ERROR_FILE; };
fwrite(HTML.Buffer.Location, Entry->LinkOffsets.PrevStart, 1, HTML.Handle);
fwrite(HTML.Buffer.Location + Entry->LinkOffsets.PrevStart + Entry->LinkOffsets.PrevEnd, Entry->LinkOffsets.NextStart, 1, HTML.Handle);
fwrite(HTML.Buffer.Location + Entry->LinkOffsets.PrevStart + Entry->LinkOffsets.PrevEnd + Entry->LinkOffsets.NextStart + Entry->LinkOffsets.NextEnd,
HTML.Buffer.Size - (Entry->LinkOffsets.PrevStart + Entry->LinkOffsets.PrevEnd + Entry->LinkOffsets.NextStart + Entry->LinkOffsets.NextEnd),
1,
HTML.Handle);
fclose(HTML.Handle);
Entry->LinkOffsets.PrevEnd = 0;
Entry->LinkOffsets.NextEnd = 0;
if(N->Prev && N->PrevIndex >= 0)
{
N->PrevOffsetModifier = N->Prev->LinkOffsets.PrevEnd
+ N->Prev->LinkOffsets.NextStart
+ N->Prev->LinkOffsets.NextEnd
- N->PreLinkPrevOffsetTotal;
}
else
{
N->NextOffsetModifier = N->Next->LinkOffsets.PrevEnd
+ N->Next->LinkOffsets.NextStart
+ N->Next->LinkOffsets.NextEnd
- N->PreLinkNextOffsetTotal;
}
}
FreeFile(&HTML);
return RC_SUCCESS;
}
void
LinkToNewEntry(neighbourhood *N)
{
MEM_TEST_INITIAL();
N->PreLinkThisOffsetTotal = N->This->LinkOffsets.PrevEnd
+ N->This->LinkOffsets.NextStart
+ N->This->LinkOffsets.NextEnd;
if(N->Prev)
{
N->PreLinkPrevOffsetTotal = N->Prev->LinkOffsets.PrevEnd
+ N->Prev->LinkOffsets.NextStart
+ N->Prev->LinkOffsets.NextEnd;
MEM_TEST_MID("LinkToNewEntry1");
InsertNeighbourLink(N->Project, N->Prev, N->This, LINK_FORWARDS, N->FormerIsFirst);
MEM_TEST_MID("LinkToNewEntry2");
N->PrevOffsetModifier = N->Prev->LinkOffsets.PrevEnd
+ N->Prev->LinkOffsets.NextStart
+ N->Prev->LinkOffsets.NextEnd
- N->PreLinkPrevOffsetTotal;
}
if(N->Next)
{
N->PreLinkNextOffsetTotal = N->Next->LinkOffsets.PrevEnd
+ N->Next->LinkOffsets.NextStart
+ N->Next->LinkOffsets.NextEnd;
MEM_TEST_MID("LinkToNewEntry3");
InsertNeighbourLink(N->Project, N->Next, N->This, LINK_BACKWARDS, N->LatterIsFinal);
MEM_TEST_MID("LinkToNewEntry4");
N->NextOffsetModifier = N->Next->LinkOffsets.PrevEnd
+ N->Next->LinkOffsets.NextStart
+ N->Next->LinkOffsets.NextEnd
- N->PreLinkNextOffsetTotal;
}
MEM_TEST_AFTER("LinkToNewEntry()");
}
void
MarkNextAsFirst(neighbourhood *N)
{
// TODO(matt): We added some untested logic in here. If things related to the prev / next links fail, we screwed up here
file HTML = {};
ReadPlayerPageIntoBuffer(N->Project, &HTML, N->Next);
if(HTML.Buffer.Location)
{
buffer Link;
ClaimBuffer(&Link, BID_LINK, Kilobytes(4));
HTML.Handle = fopen(HTML.Path, "w");
fwrite(HTML.Buffer.Location, N->Next->LinkOffsets.PrevStart, 1, HTML.Handle);
CopyStringToBuffer(&Link,
" <div class=\"episodeMarker first\"><div>&#8226;</div><div>Welcome to <cite>%.*s</cite></div><div>&#8226;</div></div>\n", (int)CurrentProject->Title.Length, CurrentProject->Title.Base);
fwrite(Link.Location, (Link.Ptr - Link.Location), 1, HTML.Handle);
fwrite(HTML.Buffer.Location + N->Next->LinkOffsets.PrevStart + N->Next->LinkOffsets.PrevEnd,
HTML.Buffer.Size - (N->Next->LinkOffsets.PrevStart + N->Next->LinkOffsets.PrevEnd),
1,
HTML.Handle);
N->Next->LinkOffsets.PrevEnd = Link.Ptr - Link.Location;
DeclaimBuffer(&Link);
fclose(HTML.Handle);
}
else
{
link_insertion_offsets Blank = {};
N->Next->LinkOffsets = Blank;
}
FreeFile(&HTML);
}
void
MarkPrevAsFinal(neighbourhood *N)
{
file File = {};
ReadPlayerPageIntoBuffer(N->Project, &File, N->Prev);
if(File.Buffer.Location)
{
File.Handle = fopen(File.Path, "w");
buffer Link;
ClaimBuffer(&Link, BID_LINK, Kilobytes(4));
fwrite(File.Buffer.Location, N->Prev->LinkOffsets.PrevStart + N->Prev->LinkOffsets.PrevEnd + N->Prev->LinkOffsets.NextStart, 1, File.Handle);
CopyStringToBuffer(&Link,
" <div class=\"episodeMarker last\"><div>&#8226;</div><div>You have arrived at the (current) end of <cite>%.*s</cite></div><div>&#8226;</div></div>\n", (int)CurrentProject->Title.Length, CurrentProject->Title.Base);
fwrite(Link.Location, (Link.Ptr - Link.Location), 1, File.Handle);
fwrite(File.Buffer.Location + N->Prev->LinkOffsets.PrevStart + N->Prev->LinkOffsets.PrevEnd + N->Prev->LinkOffsets.NextStart + N->Prev->LinkOffsets.NextEnd,
File.Buffer.Size - (N->Prev->LinkOffsets.PrevStart + N->Prev->LinkOffsets.PrevEnd + N->Prev->LinkOffsets.NextStart + N->Prev->LinkOffsets.NextEnd),
1,
File.Handle);
N->Prev->LinkOffsets.NextEnd = Link.Ptr - Link.Location;
DeclaimBuffer(&Link);
fclose(File.Handle);
}
else
{
link_insertion_offsets Blank = {};
N->Prev->LinkOffsets = Blank;
}
FreeFile(&File);
}
void
LinkOverDeletedEntry(neighbourhood *N)
{
if(N->Project->EntryCount > 0)
{
if(N->Project->EntryCount == 1)
{
DeleteNeighbourLinks(N);
}
else
{
if(N->DeletedEntryWasFirst)
{
N->PreLinkNextOffsetTotal = N->Next->LinkOffsets.PrevEnd
+ N->Next->LinkOffsets.NextStart
+ N->Next->LinkOffsets.NextEnd;
MarkNextAsFirst(N);
N->NextOffsetModifier = N->Next->LinkOffsets.PrevEnd
+ N->Next->LinkOffsets.NextStart
+ N->Next->LinkOffsets.NextEnd
- N->PreLinkNextOffsetTotal;
return;
}
else if(N->DeletedEntryWasFinal)
{
N->PreLinkPrevOffsetTotal = N->Prev->LinkOffsets.PrevEnd
+ N->Prev->LinkOffsets.NextStart
+ N->Prev->LinkOffsets.NextEnd;
MarkPrevAsFinal(N);
N->PrevOffsetModifier = N->Prev->LinkOffsets.PrevEnd
+ N->Prev->LinkOffsets.NextStart
+ N->Prev->LinkOffsets.NextEnd
- N->PreLinkPrevOffsetTotal;
return;
}
else
{
// Assert(N->PrevIndex >= 0 && N->NextIndex >= 0)
N->PreLinkPrevOffsetTotal = N->Prev->LinkOffsets.PrevEnd
+ N->Prev->LinkOffsets.NextStart
+ N->Prev->LinkOffsets.NextEnd;
N->PreLinkNextOffsetTotal = N->Next->LinkOffsets.PrevEnd
+ N->Next->LinkOffsets.NextStart
+ N->Next->LinkOffsets.NextEnd;
}
InsertNeighbourLink(N->Project, N->Prev, N->Next, LINK_FORWARDS, N->FormerIsFirst);
InsertNeighbourLink(N->Project, N->Next, N->Prev, LINK_BACKWARDS, N->LatterIsFinal);
N->PrevOffsetModifier = N->Prev->LinkOffsets.PrevEnd
+ N->Prev->LinkOffsets.NextStart
+ N->Prev->LinkOffsets.NextEnd
- N->PreLinkPrevOffsetTotal;
N->NextOffsetModifier = N->Next->LinkOffsets.PrevEnd
+ N->Next->LinkOffsets.NextStart
+ N->Next->LinkOffsets.NextEnd
- N->PreLinkNextOffsetTotal;
}
}
}
void
LinkNeighbours(neighbourhood *N, enum8(link_types) LinkType)
{
if(LinkType == LINK_INCLUDE)
{
LinkToNewEntry(N);
}
else
{
LinkOverDeletedEntry(N);
}
}
rc
DeleteFromDB(neighbourhood *N, string BaseFilename)
{
// TODO(matt): LogError()
db_entry *Entry = 0;
int EntryIndex = BinarySearchForMetadataEntry(N->Project, &Entry, BaseFilename);
if(!DB.File.Buffer.Location)
{
InitIndexFile(CurrentProject);
}
if(Entry)
{
int DeleteFileFrom = AccumulateDBEntryInsertionOffset(N->Project, EntryIndex);
int DeleteFileTo = DeleteFileFrom + Entry->Size;
N->This = Entry;
N->ThisIndex = EntryIndex;
GetNeighbourhood(N, EDIT_DELETION);
int NewEntryCount = N->Project->EntryCount;
--NewEntryCount;
if(NewEntryCount == 0)
{
DeleteSearchPageFromFilesystem(N->Project);
DeleteLandmarksForSearch(CurrentProject->Index);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
}
N->Project->EntryCount = NewEntryCount;
if(!(DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"))) { FreeBuffer(&DB.Metadata.File.Buffer); return RC_ERROR_FILE; }
char *Ptr = (char *)N->This;
uint64_t BytesIntoFile = Ptr - DB.Metadata.File.Buffer.Location;
fwrite(DB.Metadata.File.Buffer.Location, BytesIntoFile, 1, DB.Metadata.File.Handle);
SetFileEditPosition(&DB.Metadata);
BytesIntoFile += sizeof(*N->This);
AccumulateFileEditSize(&DB.Metadata, -sizeof(*N->This));
fwrite(DB.Metadata.File.Buffer.Location + BytesIntoFile, DB.Metadata.File.Buffer.Size - BytesIntoFile, 1, DB.Metadata.File.Handle);
// TODO(matt): At this point, the N->This and possibly N->Next pointers are rendered invalid, so we must reacquire
// the Neighbourhood
// AFD
N->This = 0;
DB.Metadata.Signposts.This.Ptr = 0;
CycleSignpostedFile(&DB.Metadata);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
if(N->Project->EntryCount > 0)
{
if(!(DB.File.Handle = fopen(DB.File.Path, "w"))) { FreeBuffer(&DB.File.Buffer); return RC_ERROR_FILE; }
fwrite(DB.File.Buffer.Location, DeleteFileFrom, 1, DB.File.Handle);
fwrite(DB.File.Buffer.Location + DeleteFileTo, DB.File.Buffer.Size - DeleteFileTo, 1, DB.File.Handle);
CycleFile(&DB.File);
}
}
return Entry ? RC_SUCCESS : RC_NOOP;
}
typedef struct
{
char *String;
bool SelfClosing;
} html_element;
html_element HTMLElements[] =
{
{ "a", FALSE },
{ "div", FALSE },
{ "img", TRUE },
{ "input", TRUE },
{ "label", FALSE },
{ "li", FALSE },
{ "nav", FALSE },
{ "script", FALSE },
{ "span", FALSE },
{ "ul", FALSE },
};
typedef enum
{
NODE_A,
NODE_DIV,
NODE_IMG,
NODE_INPUT,
NODE_LABEL,
NODE_LI,
NODE_NAV,
NODE_SCRIPT,
NODE_SPAN,
NODE_UL,
} html_element_id;
void
OpenNode(buffer *B, uint32_t *IndentationLevel, html_element_id Element, char *ID)
{
AppendStringToBuffer(B, Wrap0("<"));
AppendStringToBuffer(B, Wrap0(HTMLElements[Element].String));
if(ID)
{
AppendStringToBuffer(B, Wrap0(" "));
AppendStringToBuffer(B, Wrap0("id=\""));
AppendStringToBuffer(B, Wrap0(ID));
AppendStringToBuffer(B, Wrap0("\""));
}
if(!HTMLElements[Element].SelfClosing)
{
++*IndentationLevel;
}
}
void
OpenNodeC(buffer *B, uint32_t *IndentationLevel, html_element_id Element, char *ID)
{
AppendStringToBuffer(B, Wrap0("<"));
AppendStringToBuffer(B, Wrap0(HTMLElements[Element].String));
if(ID)
{
AppendStringToBuffer(B, Wrap0(" "));
AppendStringToBuffer(B, Wrap0("id=\""));
AppendStringToBuffer(B, Wrap0(ID));
AppendStringToBuffer(B, Wrap0("\""));
}
AppendStringToBuffer(B, Wrap0(">"));
if(!HTMLElements[Element].SelfClosing)
{
++*IndentationLevel;
}
}
void
OpenNodeNewLine(buffer *B, uint32_t *IndentationLevel, html_element_id Element, char *ID)
{
AppendStringToBuffer(B, Wrap0("\n"));
IndentBuffer(B, *IndentationLevel);
OpenNode(B, IndentationLevel, Element, ID);
}
void
OpenNodeCNewLine(buffer *B, uint32_t *IndentationLevel, html_element_id Element, char *ID)
{
AppendStringToBuffer(B, Wrap0("\n"));
IndentBuffer(B, *IndentationLevel);
OpenNodeC(B, IndentationLevel, Element, ID);
}
void
CloseNode(buffer *B, uint32_t *IndentationLevel, html_element_id Element)
{
--*IndentationLevel;
AppendStringToBuffer(B, Wrap0("</"));
AppendStringToBuffer(B, Wrap0(HTMLElements[Element].String));
AppendStringToBuffer(B, Wrap0(">"));
}
void
CloseNodeNewLine(buffer *B, uint32_t *IndentationLevel, html_element_id Element)
{
AppendStringToBuffer(B, Wrap0("\n"));
--*IndentationLevel;
IndentBuffer(B, *IndentationLevel);
AppendStringToBuffer(B, Wrap0("</"));
AppendStringToBuffer(B, Wrap0(HTMLElements[Element].String));
AppendStringToBuffer(B, Wrap0(">"));
}
string
TrimString(string S, uint32_t CharsFromStart, uint32_t CharsFromEnd)
{
string Result = S;
Result.Length -= (CharsFromStart + CharsFromEnd);
Result.Base += CharsFromStart;
return Result;
}
void
GenerateFilterOfProjectAndChildren(buffer *Filter, db_header_project *StoredP, project *P, bool *SearchRequired, bool TopLevel, bool *RequiresCineraJS)
{
if(!TopLevel || StoredP->EntryCount > 0)
{
OpenNodeNewLine(Filter, &Filter->IndentLevel, NODE_DIV, 0);
AppendStringToBuffer(Filter, Wrap0(" class=\"cineraFilterProject\" data-baseURL=\""));
AppendStringToBuffer(Filter, P->BaseURL);
AppendStringToBuffer(Filter, Wrap0("\">"));
OpenNodeNewLine(Filter, &Filter->IndentLevel, NODE_SPAN, 0);
AppendStringToBuffer(Filter, Wrap0(" class=\"cineraText\">"));
PushIcon(Filter, TRUE, P->IconType, P->Icon, P->IconAsset, P->IconVariants, PAGE_SEARCH, RequiresCineraJS);
AppendStringToBuffer(Filter, P->HTMLTitle.Length > 0 ? P->HTMLTitle : P->Title);
CloseNode(Filter, &Filter->IndentLevel, NODE_SPAN);
}
db_header_project *StoredChild = LocateFirstChildProject(StoredP);
for(int ChildIndex = 0; ChildIndex < StoredP->ChildCount; ++ChildIndex)
{
project *Child = P->Child + ChildIndex;
GenerateFilterOfProjectAndChildren(Filter, StoredChild, Child, SearchRequired, FALSE, RequiresCineraJS);
StoredChild = SkipProjectAndChildren(StoredChild);
}
if(!TopLevel || StoredP->EntryCount > 0)
{
CloseNodeNewLine(Filter, &Filter->IndentLevel, NODE_DIV);
}
}
void
GenerateIndexOfProjectAndChildren(buffer *Index, db_header_project *StoredP, project *P, bool *SearchRequired, bool TopLevel, bool *RequiresCineraJS)
{
if(!TopLevel || StoredP->EntryCount > 0)
{
OpenNodeNewLine(Index, &Index->IndentLevel, NODE_DIV, 0);
AppendStringToBuffer(Index, Wrap0(" class=\"cineraIndexProject "));
AppendStringToBuffer(Index, P->Theme);
AppendStringToBuffer(Index, Wrap0("\" data-project=\""));
AppendStringToBuffer(Index, P->ID);
AppendStringToBuffer(Index, Wrap0("\" data-baseURL=\""));
AppendStringToBuffer(Index, P->BaseURL);
AppendStringToBuffer(Index, Wrap0("\" data-playerLocation=\""));
AppendStringToBuffer(Index, P->PlayerLocation);
AppendStringToBuffer(Index, Wrap0("\">"));
OpenNodeNewLine(Index, &Index->IndentLevel, NODE_DIV, 0);
AppendStringToBuffer(Index, Wrap0(" class=\"cineraProjectTitle\">"));
PushIcon(Index, TRUE, P->IconType, P->Icon, P->IconAsset, P->IconVariants, PAGE_SEARCH, RequiresCineraJS);
AppendStringToBuffer(Index, P->HTMLTitle.Length > 0 ? P->HTMLTitle : P->Title);
CloseNode(Index, &Index->IndentLevel, NODE_DIV);
}
if(StoredP->EntryCount > 0)
{
OpenNodeNewLine(Index, &Index->IndentLevel, NODE_DIV, 0);
AppendStringToBuffer(Index, Wrap0(" class=\"cineraIndexEntries\">"));
db_entry *Entry = LocateFirstEntry(StoredP);
buffer PlayerURL;
ClaimBuffer(&PlayerURL, BID_URL_PLAYER, MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1 + MAX_BASE_FILENAME_LENGTH);
for(int i = 0; i < StoredP->EntryCount; ++i, ++Entry)
{
if(Entry->Size > 0)
{
*SearchRequired = TRUE;
OpenNodeCNewLine(Index, &Index->IndentLevel, NODE_DIV, 0);
OpenNode(Index, &Index->IndentLevel, NODE_A, 0);
AppendStringToBuffer(Index, Wrap0(" href=\""));
ConstructPlayerURL(&PlayerURL, StoredP, Wrap0i(Entry->OutputLocation, sizeof(Entry->OutputLocation)));
AppendBuffer(Index, &PlayerURL);
AppendStringToBuffer(Index, Wrap0("\">"));
if(*StoredP->Unit)
{
// TODO(matt): That rigorous notion of numbering, goddammit?!
string NumberL = TrimString(Wrap0i(Entry->HMMLBaseFilename, sizeof(Entry->HMMLBaseFilename)),
P->ID.Length, 0);
char Number[NumberL.Length + 1];
ClearCopyStringNoFormat(Number, sizeof(Number), NumberL);
if(CurrentProject->NumberingScheme == NS_LINEAR)
{
for(int i = 0; Number[i]; ++i)
{
if(Number[i] == '_')
{
Number[i] = '.';
}
}
}
AppendStringToBuffer(Index, P->Unit);
AppendStringToBuffer(Index, Wrap0(" "));
AppendStringToBuffer(Index, Wrap0i(Number, sizeof(Number)));
AppendStringToBuffer(Index, Wrap0(": "));
}
// HERE
AppendStringToBufferHTMLSafe(Index, Wrap0i(Entry->Title, sizeof(Entry->Title)));
CloseNode(Index, &Index->IndentLevel, NODE_A);
CloseNode(Index, &Index->IndentLevel, NODE_DIV);
}
}
DeclaimBuffer(&PlayerURL);
CloseNodeNewLine(Index, &Index->IndentLevel, NODE_DIV);
}
db_header_project *StoredChild = LocateFirstChildProject(StoredP);
for(int ChildIndex = 0; ChildIndex < StoredP->ChildCount; ++ChildIndex)
{
project *Child = &P->Child[ChildIndex];
GenerateIndexOfProjectAndChildren(Index, StoredChild, Child, SearchRequired, FALSE, RequiresCineraJS);
StoredChild = SkipProjectAndChildren(StoredChild);
}
if(!TopLevel || StoredP->EntryCount > 0)
{
CloseNodeNewLine(Index, &Index->IndentLevel, NODE_DIV);
}
}
void
CountOfProjectOrDescendantsWithPublicEntries(db_header_project *P, uint8_t *Count)
{
db_entry *Entry = LocateFirstEntry(P);
for(int EntryIndex = 0; EntryIndex < P->EntryCount; ++EntryIndex)
{
if(Entry->Size > 0)
{
++*Count;
break;
}
++Entry;
}
db_header_project *Child = LocateFirstChildProject(P);
for(int ChildIndex = 0; ChildIndex < P->ChildCount; ++ChildIndex)
{
CountOfProjectOrDescendantsWithPublicEntries(Child, Count);
Child = SkipProjectAndChildren(Child);
}
}
bool
ProjectsHavePublicEntries()
{
db_block_projects *ProjectsBlock = DB.Metadata.Signposts.ProjectsBlock.Ptr ? DB.Metadata.Signposts.ProjectsBlock.Ptr : LocateBlock(B_PROJ);
db_header_project *Child = LocateFirstChildProjectOfBlock(ProjectsBlock);
uint8_t Count = 0;
for(int i = 0; i < ProjectsBlock->Count; ++i)
{
CountOfProjectOrDescendantsWithPublicEntries(Child, &Count);
if(Count)
{
return TRUE;
}
Child = SkipProjectAndChildren(Child);
}
return Count;
}
bool
AtLeastNProjectsHavePublicEntries(uint8_t N)
{
uint8_t FoundProjectsWithPublicEntries = 0;
db_block_projects *ProjectsBlock = DB.Metadata.Signposts.ProjectsBlock.Ptr ? DB.Metadata.Signposts.ProjectsBlock.Ptr : LocateBlock(B_PROJ);
db_header_project *Child = LocateFirstChildProjectOfBlock(ProjectsBlock);
for(int i = 0; i < ProjectsBlock->Count; ++i)
{
CountOfProjectOrDescendantsWithPublicEntries(Child, &FoundProjectsWithPublicEntries);
if(FoundProjectsWithPublicEntries >= N)
{
return TRUE;
}
Child = SkipProjectAndChildren(Child);
}
return FALSE;
}
typedef struct
{
string *S;
uint64_t Count;
} strings;
void
PushUniqueStringsRecursively(strings *UniqueThemes, db_header_project *P)
{
bool FoundMatch = FALSE;
string Theme = Wrap0i(P->Theme, sizeof(P->Theme));
for(int i = 0; i < UniqueThemes->Count; ++i)
{
if(StringsMatch(UniqueThemes->S[i], Theme))
{
FoundMatch = TRUE;
}
}
if(!FoundMatch && Theme.Length > 0)
{
UniqueThemes->S = Fit(UniqueThemes->S, sizeof(*UniqueThemes->S), UniqueThemes->Count, 4, FALSE);
UniqueThemes->S[UniqueThemes->Count] = Theme;
++UniqueThemes->Count;
}
db_header_project *Child = LocateFirstChildProject(P);
for(int i = 0; i < P->ChildCount; ++i)
{
PushUniqueStringsRecursively(UniqueThemes, Child);
Child = SkipProject(Child);
}
}
void
PushUniqueString(strings *UniqueThemes, string GlobalTheme)
{
bool FoundMatch = FALSE;
for(int i = 0; i < UniqueThemes->Count; ++i)
{
if(StringsMatch(UniqueThemes->S[i], GlobalTheme))
{
FoundMatch = TRUE;
}
}
if(!FoundMatch && GlobalTheme.Length > 0)
{
UniqueThemes->S = Fit(UniqueThemes->S, sizeof(*UniqueThemes->S), UniqueThemes->Count, 4, FALSE);
UniqueThemes->S[UniqueThemes->Count] = GlobalTheme;
++UniqueThemes->Count;
}
}
void
GenerateThemeLinks(buffer *IncludesSearch, db_header_project *P)
{
strings UniqueThemes = {};
if(P)
{
PushUniqueStringsRecursively(&UniqueThemes, P);
}
else
{
PushUniqueString(&UniqueThemes, Config->GlobalTheme);
db_block_projects *ProjectsBlock = DB.Metadata.Signposts.ProjectsBlock.Ptr ? DB.Metadata.Signposts.ProjectsBlock.Ptr : LocateBlock(B_PROJ);
P = LocateFirstChildProjectOfBlock(ProjectsBlock);
for(int i = 0; i < ProjectsBlock->Count; ++i)
{
PushUniqueStringsRecursively(&UniqueThemes, P);
P = SkipProjectAndChildren(P);
}
}
for(int i = 0; i < UniqueThemes.Count; ++i)
{
string ThemeID = UniqueThemes.S[i];
string ThemeFilename = MakeString("sls", "cinera__", &ThemeID, ".css");
asset *CSSTheme = GetAsset(ThemeFilename, ASSET_CSS);
FreeString(&ThemeFilename);
buffer URL;
ConstructResolvedAssetURL(&URL, CSSTheme, PAGE_SEARCH);
CopyStringToBuffer(IncludesSearch,
"\n <link rel=\"stylesheet\" type=\"text/css\" href=\"%.*s",
(int)(URL.Ptr - URL.Location),
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(IncludesSearch, CSSTheme, PAGE_SEARCH, 0);
CopyStringToBuffer(IncludesSearch,
"\">");
}
FreeAndResetCount(UniqueThemes.S, UniqueThemes.Count);
}
int
SearchToBuffer(buffers *CollationBuffers, db_header_project *StoredP, project *P, string Theme, bool RequiresCineraJS) // NOTE(matt): This guy malloc's CollationBuffers->Search
{
bool DoWork = FALSE;
if(DB.Metadata.File.Buffer.Location)
{
if(StoredP)
{
uint8_t FoundProjectsWithPublicEntries = 0;
CountOfProjectOrDescendantsWithPublicEntries(StoredP, &FoundProjectsWithPublicEntries);
if(FoundProjectsWithPublicEntries > 0)
{
DoWork = TRUE;
}
}
else
{
if(ProjectsHavePublicEntries())
{
DoWork = TRUE;
}
}
}
if(DoWork)
{
DB.Header = *(db_header *)DB.Metadata.File.Buffer.Location;
RewindBuffer(&CollationBuffers->IncludesSearch);
buffer URL;
asset *CSSCinera = GetAsset(Wrap0(BuiltinAssets[ASSET_CSS_CINERA].Filename), ASSET_CSS);
ConstructResolvedAssetURL(&URL, CSSCinera, PAGE_SEARCH);
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
"<meta charset=\"UTF-8\">\n"
" <meta name=\"generator\" content=\"Cinera %d.%d.%d\">\n"
"\n"
" <link rel=\"stylesheet\" type=\"text/css\" href=\"%.*s",
CINERA_APP_VERSION.Major,
CINERA_APP_VERSION.Minor,
CINERA_APP_VERSION.Patch,
(int)(URL.Ptr - URL.Location),
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesSearch, CSSCinera, PAGE_SEARCH, 0);
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
"\">");
// TODO(matt): Switch the CollationBuffers->IncludesSearch to be allocated rather than claimed
GenerateThemeLinks(&CollationBuffers->IncludesSearch, StoredP);
asset *CSSTopics = GetAsset(Wrap0(BuiltinAssets[ASSET_CSS_TOPICS].Filename), ASSET_CSS);
ConstructResolvedAssetURL(&URL, CSSTopics, PAGE_SEARCH);
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
"\n <link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesSearch, CSSTopics, PAGE_SEARCH, 0);
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
"\">");
asset *JSSearch = GetAsset(Wrap0(BuiltinAssets[ASSET_JS_SEARCH].Filename), ASSET_JS);
ConstructResolvedAssetURL(&URL, JSSearch, PAGE_SEARCH);
uint32_t IndentationLevel = 1;
++IndentationLevel;
buffer Script = {};
Script.ID = BID_SCRIPT;
OpenNodeNewLine(&Script, &IndentationLevel, NODE_SCRIPT, 0);
AppendStringToBuffer(&Script, Wrap0(" type=\"text/javascript\" src=\""));
AppendStringToBuffer(&Script, Wrap0i(URL.Location, URL.Ptr - URL.Location));
DeclaimBuffer(&URL);
PushAssetLandmark(&Script, JSSearch, PAGE_SEARCH, TRUE);
AppendStringToBuffer(&Script, Wrap0("\">"));
CloseNode(&Script, &IndentationLevel, NODE_SCRIPT);
bool SearchRequired = FALSE;
//IndentBuffer(&CollationBuffers->Search, ++IndentationLevel);
buffer *B = &CollationBuffers->Search;
OpenNode(B, &IndentationLevel, NODE_DIV, "cineraIndexControl");
AppendStringToBuffer(B, Wrap0(" class=\""));
AppendStringToBuffer(B, Theme);
AppendStringToBuffer(B, Wrap0("\">"));
OpenNodeCNewLine(B, &IndentationLevel, NODE_DIV, "cineraIndexSort");
AppendStringToBuffer(B, Wrap0("Sort: Old to New &#9206;"));
CloseNode(B, &IndentationLevel, NODE_DIV);
bool FilterRequired = FALSE;
if(StoredP)
{
if(StoredP->ChildCount > 0)
{
uint8_t FoundProjectsWithPublicEntries = 0;
CountOfProjectOrDescendantsWithPublicEntries(StoredP, &FoundProjectsWithPublicEntries);
if(FoundProjectsWithPublicEntries > 0)
{
FilterRequired = TRUE;
}
}
}
else
{
if(AtLeastNProjectsHavePublicEntries(2))
{
FilterRequired = TRUE;
}
}
buffer Filter = { .ID = BID_FILTER, .IndentLevel = IndentationLevel };
buffer Index = { .ID = BID_INDEX, .IndentLevel = IndentationLevel };
if(FilterRequired)
{
Index.IndentLevel -= 2;
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraIndexFilter\">"));
OpenNodeCNewLine(B, &IndentationLevel, NODE_SPAN, 0);
OpenNode(B, &IndentationLevel, NODE_IMG, 0);
AppendStringToBuffer(B, Wrap0(" src=\""));
asset *FilterImage = GetAsset(Wrap0(BuiltinAssets[ASSET_IMG_FILTER].Filename), ASSET_IMG);
ConstructResolvedAssetURL(&URL, FilterImage, PAGE_SEARCH);
AppendStringToBuffer(B, Wrap0i(URL.Location, URL.Ptr - URL.Location));
DeclaimBuffer(&URL);
PushAssetLandmark(B, FilterImage, PAGE_SEARCH, TRUE);
AppendStringToBuffer(B, Wrap0("\">"));
CloseNode(B, &IndentationLevel, NODE_SPAN);
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"filter_container\">"));
}
// NOTE(matt): What I think the rules should be are:
// 1: if(Multiple projects have entries) { GenerateFilter at all }
// 2: if(ProjectsBlock->Count > 1) { Include the top-level "holding projects" in the filter }
if(StoredP)
{
if(FilterRequired)
{
GenerateFilterOfProjectAndChildren(&Filter, StoredP, P, &SearchRequired, TRUE, &RequiresCineraJS);
}
GenerateIndexOfProjectAndChildren(&Index, StoredP, P, &SearchRequired, TRUE, &RequiresCineraJS);
}
else
{
db_block_projects *ProjectsBlock = DB.Metadata.Signposts.ProjectsBlock.Ptr ? DB.Metadata.Signposts.ProjectsBlock.Ptr : LocateBlock(B_PROJ);
StoredP = LocateFirstChildProjectOfBlock(ProjectsBlock);
for(int i = 0; i < ProjectsBlock->Count; ++i)
{
project *Child = &Config->Project[i];
if(FilterRequired)
{
GenerateFilterOfProjectAndChildren(&Filter, StoredP, Child, &SearchRequired, FALSE, &RequiresCineraJS);
}
GenerateIndexOfProjectAndChildren(&Index, StoredP, Child, &SearchRequired, FALSE, &RequiresCineraJS);
StoredP = SkipProjectAndChildren(StoredP);
}
}
if(FilterRequired)
{
AppendLandmarkedBuffer(B, &Filter, PAGE_SEARCH);
FreeBuffer(&Filter);
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV);
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV);
}
asset *JSCineraPre = GetAsset(Wrap0(BuiltinAssets[ASSET_JS_CINERA_PRE].Filename), ASSET_JS);
ConstructResolvedAssetURL(&URL, JSCineraPre, PAGE_SEARCH);
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
"\n <script type=\"text/javascript\" src=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesSearch, JSCineraPre, PAGE_SEARCH, 0);
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
"\"></script>");
if(RequiresCineraJS)
{
asset *JSCineraPost = GetAsset(Wrap0(BuiltinAssets[ASSET_JS_CINERA_POST].Filename), ASSET_JS);
ConstructResolvedAssetURL(&URL, JSCineraPost, PAGE_SEARCH);
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
"\n <script type=\"text/javascript\" src=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesSearch, JSCineraPost, PAGE_SEARCH, 0);
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
"\" defer></script>");
}
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraQueryContainer\">"));
OpenNodeNewLine(B, &IndentationLevel, NODE_LABEL, 0);
AppendStringToBuffer(B, Wrap0(" for=\"query\">"));
AppendStringToBuffer(B, Wrap0("Query:"));
CloseNode(B, &IndentationLevel, NODE_LABEL);
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"inputContainer\">"));
OpenNodeNewLine(B, &IndentationLevel, NODE_INPUT, 0);
AppendStringToBuffer(B, Wrap0(" type=\"text\" autocomplete=\"off\" id=\"query\">"));
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"spinner\">"));
AppendStringToBuffer(B, Wrap0("\n"));
IndentBuffer(B, IndentationLevel);
AppendStringToBuffer(B, Wrap0("Downloading data..."));
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV);
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV);
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV);
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV);
OpenNodeCNewLine(B, &IndentationLevel, NODE_DIV, "cineraResultsSummary");
AppendStringToBuffer(B, Wrap0("Found: 0 episodes, 0 markers, 0h 0m 0s total."));
CloseNode(B, &IndentationLevel, NODE_DIV);
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, "cineraResults");
AppendStringToBuffer(B, Wrap0(" data-single=\""));
AppendStringToBuffer(B, Wrap0(CurrentProject->SingleBrowserTab ? "1" : "0"));
AppendStringToBuffer(B, Wrap0("\">"));
CloseNode(B, &IndentationLevel, NODE_DIV);
AppendStringToBuffer(B, Wrap0("\n"));
OpenNodeCNewLine(B, &IndentationLevel, NODE_DIV, "cineraIndex");
AppendLandmarkedBuffer(B, &Index, PAGE_SEARCH);
FreeBuffer(&Index);
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV);
AppendLandmarkedBuffer(B, &Script, PAGE_SEARCH);
FreeBuffer(&Script);
if(!SearchRequired) { return RC_NOOP; }
else { return RC_SUCCESS; }
}
return RC_NOOP;
}
int
GeneratePlayerPage(neighbourhood *N, buffers *CollationBuffers, template *PlayerTemplate, string OutputLocation, bool Reinserting)
{
MEM_TEST_INITIAL();
string PlayerLocationL = Wrap0i(N->Project->PlayerLocation, sizeof(N->Project->PlayerLocation));
MEM_TEST_MID("GeneratePlayerPage1");
char *PlayerPath = ConstructDirectoryPath(N->Project, &PlayerLocationL, &OutputLocation);
MEM_TEST_MID("GeneratePlayerPage2");
DIR *OutputDirectoryHandle;
if(!(OutputDirectoryHandle = opendir(PlayerPath))) // TODO(matt): open()
{
if(!MakeDir(Wrap0(PlayerPath)))
{
LogError(LOG_ERROR, "Unable to create directory %s: %s", PlayerPath, strerror(errno));
fprintf(stderr, "Unable to create directory %s: %s\n", PlayerPath, strerror(errno));
return RC_ERROR_DIRECTORY;
};
}
closedir(OutputDirectoryHandle);
ExtendString0(&PlayerPath, Wrap0("/index.html"));
MEM_TEST_MID("GeneratePlayerPage3");
bool SearchInTemplate = FALSE;
for(int TagIndex = 0; TagIndex < PlayerTemplate->Metadata.TagCount; ++TagIndex)
{
if(PlayerTemplate->Metadata.Tags[TagIndex].TagCode == TAG_SEARCH)
{
SearchInTemplate = TRUE;
// TODO(matt): Fully determine whether UpdateNeighbourhoodPointers() must happen here
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
SearchToBuffer(CollationBuffers, N->Project, CurrentProject, Wrap0i(N->Project->Theme, sizeof(N->Project->Theme)), PlayerTemplate->Metadata.RequiresCineraJS);
break;
}
}
MEM_TEST_MID("GeneratePlayerPage4");
BuffersToHTML(CollationBuffers, PlayerTemplate, PlayerPath, PAGE_PLAYER, &N->This->LinkOffsets.PrevStart);
MEM_TEST_MID("GeneratePlayerPage5");
Free(PlayerPath);
// NOTE(matt): A previous InsertNeighbourLink() call will have done SnipeEntryIntoMetadataBuffer(), but we must do it here now
// that PrevStart has been adjusted by BuffersToHTML()
UpdateLandmarksForNeighbourhood(N, Reinserting ? EDIT_REINSERTION : EDIT_ADDITION);
MEM_TEST_MID("GeneratePlayerPage6");
MEM_TEST_MID("GeneratePlayerPage7");
if(SearchInTemplate)
{
FreeBuffer(&CollationBuffers->Search);
}
MEM_TEST_MID("GeneratePlayerPage8");
ResetAssetLandmarks();
MEM_TEST_AFTER("GeneratePlayerPage()");
return RC_SUCCESS;
}
rc
GenerateSearchPage(neighbourhood *N, buffers *CollationBuffers, db_header_project *StoredP, project *P)
{
string SearchLocationL = Wrap0i(StoredP->SearchLocation, sizeof(StoredP->SearchLocation));
char *SearchPath = ConstructDirectoryPath(StoredP, &SearchLocationL, 0);
DIR *OutputDirectoryHandle;
if(!(OutputDirectoryHandle = opendir(SearchPath))) // TODO(matt): open()
{
if(!MakeDir(Wrap0(SearchPath)))
{
LogError(LOG_ERROR, "Unable to create directory %s: %s", SearchPath, strerror(errno));
fprintf(stderr, "Unable to create directory %s: %s\n", SearchPath, strerror(errno));
return RC_ERROR_DIRECTORY;
};
}
closedir(OutputDirectoryHandle);
ExtendString0(&SearchPath, Wrap0("/index.html"));
// TODO(matt): We used to do UpdateNeighbourhoodPointers() here. If something goes awry near here, reinstate that call
switch(SearchToBuffer(CollationBuffers, StoredP, P,
Wrap0i(StoredP->Theme, sizeof(StoredP->Theme)), P->SearchTemplate.Metadata.RequiresCineraJS))
{
case RC_SUCCESS:
{
BuffersToHTML(CollationBuffers, &P->SearchTemplate, SearchPath, PAGE_SEARCH, 0);
UpdateLandmarksForSearch(N, P->Index);
break;
}
case RC_NOOP:
{
DeleteSearchPageFromFilesystem(StoredP);
DeleteLandmarksForSearch(P->Index);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
break;
}
}
Free(SearchPath);
FreeBuffer(&CollationBuffers->Search);
ResetAssetLandmarks();
return RC_SUCCESS;
}
rc
GenerateGlobalSearchPage(neighbourhood *N, buffers *CollationBuffers)
{
db_block_projects *ProjectsBlock = DB.Metadata.Signposts.ProjectsBlock.Ptr;
string SearchLocationL = Wrap0i(ProjectsBlock->GlobalSearchDir, sizeof(ProjectsBlock->GlobalSearchDir));
char *SearchPath = MakeString0("l", &SearchLocationL);
DIR *OutputDirectoryHandle;
if(!(OutputDirectoryHandle = opendir(SearchPath))) // TODO(matt): open()
{
if(!MakeDir(Wrap0(SearchPath)))
{
LogError(LOG_ERROR, "Unable to create directory %s: %s", SearchPath, strerror(errno));
fprintf(stderr, "Unable to create directory %s: %s\n", SearchPath, strerror(errno));
return RC_ERROR_DIRECTORY;
};
}
closedir(OutputDirectoryHandle);
ExtendString0(&SearchPath, Wrap0("/index.html"));
switch(SearchToBuffer(CollationBuffers, 0, 0, Config->GlobalTheme, Config->SearchTemplate.Metadata.RequiresCineraJS))
{
case RC_SUCCESS:
{
BuffersToHTML(CollationBuffers, &Config->SearchTemplate, SearchPath, PAGE_SEARCH, 0);
UpdateLandmarksForSearch(N, GLOBAL_SEARCH_PAGE_INDEX);
break;
}
case RC_NOOP:
{
char *GlobalSearchPageLocation = MakeString0("l", &Config->GlobalSearchDir);
DeleteGlobalSearchPageFromFilesystem(GlobalSearchPageLocation);
Free(GlobalSearchPageLocation);
DeleteLandmarksForSearch(GLOBAL_SEARCH_PAGE_INDEX);
DeleteStaleAssets();
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
break;
}
}
Free(SearchPath);
FreeBuffer(&CollationBuffers->Search);
ResetAssetLandmarks();
return RC_SUCCESS;
}
int
GenerateSearchPages(neighbourhood *N, buffers *CollationBuffers)
{
// TODO(matt): Ascend through all the ancestry, generating search pages
PrintFunctionName("GenerateSearchPage()");
project *This = CurrentProject;
//PrintLineage(This->Lineage, TRUE);
ResetNeighbourhood(N);
N->Project = LocateProject(This->Index);
GenerateSearchPage(N, CollationBuffers, N->Project, This);
while(This->Parent)
{
//PrintLineage(This->Parent->Lineage, TRUE);
This = This->Parent;
ResetNeighbourhood(N);
N->Project = LocateProject(This->Index);
GenerateSearchPage(N, CollationBuffers, N->Project, This);
}
if(Config->GlobalSearchDir.Length > 0 && Config->GlobalSearchURL.Length > 0)
{
// TODO(matt): Fully determine whether UpdateNeighbourhoodPointers() must happen here
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
GenerateGlobalSearchPage(N, CollationBuffers);
}
//sleep(1);
return RC_SUCCESS;
}
int
DeleteEntry(neighbourhood *N, string BaseFilename)
{
// TODO(matt): DeleteFromDB() may reasonably use this BaseFilename to locate the Entry. But, we'll need to return the
// Entry->Output to pass to DeletePlayerPageFromFilesystem()
// db_entry5
// TODO(matt): Fix deletion of the final entry in a project
if(DeleteFromDB(N, BaseFilename) == RC_SUCCESS)
{
LinkNeighbours(N, LINK_EXCLUDE);
DeletePlayerPageFromFilesystem(N->Project, BaseFilename, FALSE, TRUE);
UpdateLandmarksForNeighbourhood(N, EDIT_DELETION);
return RC_SUCCESS;
}
return RC_NOOP;
}
int
InsertEntry(neighbourhood *N, buffers *CollationBuffers, template *BespokeTemplate, string BaseFilename, bool RecheckingPrivacy)
{
MEM_TEST_INITIAL();
bool Reinserting = FALSE;
MEM_TEST_MID("InsertEntry0");
db_entry *Entry = InsertIntoDB(N, CollationBuffers, BespokeTemplate, BaseFilename, RecheckingPrivacy, &Reinserting);
MEM_TEST_MID("InsertEntry1");
if(Entry)
{
MEM_TEST_MID("InsertEntry2");
LinkNeighbours(N, LINK_INCLUDE);
MEM_TEST_MID("InsertEntry3");
if(BespokeTemplate->File.Buffer.Location)
{
MEM_TEST_MID("InsertEntry4");
GeneratePlayerPage(N, CollationBuffers, BespokeTemplate, Wrap0i(Entry->OutputLocation, sizeof(Entry->OutputLocation)), Reinserting);
MEM_TEST_MID("InsertEntry5");
FreeTemplate(BespokeTemplate);
MEM_TEST_MID("InsertEntry6");
}
else
{
MEM_TEST_MID("InsertEntry7");
GeneratePlayerPage(N, CollationBuffers, &CurrentProject->PlayerTemplate, Wrap0i(Entry->OutputLocation, sizeof(Entry->OutputLocation)), Reinserting);
MEM_TEST_MID("InsertEntry8");
}
MEM_TEST_AFTER("InsertEntry()");
return RC_SUCCESS;
}
MEM_TEST_AFTER("InsertEntry()");
return RC_NOOP;
}
void
SetCurrentProject(project *P, neighbourhood *N)
{
if(CurrentProject != P)
{
FreeFile(&DB.File);
CurrentProject = P;
InitNeighbourhood(N);
}
}
void
RecheckPrivacyRecursively(project *P, neighbourhood *N, buffers *CollationBuffers, template *BespokeTemplate)
{
SetCurrentProject(P, N);
if(DB.Metadata.File.Buffer.Size > 0)
{
ResetNeighbourhood(N);
char *Ptr = (char *)N->Project;
Ptr += sizeof(*N->Project);
db_entry *FirstEntry = (db_entry *)Ptr;
int PrivateEntryIndex = 0;
db_entry PrivateEntries[N->Project->EntryCount];
bool Inserted = FALSE;
for(int IndexEntry = 0; IndexEntry < N->Project->EntryCount; ++IndexEntry)
{
db_entry *Entry = FirstEntry + IndexEntry;
if(Entry->Size == 0)
{
PrivateEntries[PrivateEntryIndex] = *Entry;
++PrivateEntryIndex;
}
}
for(int i = 0; i < PrivateEntryIndex; ++i)
{
Inserted = (InsertEntry(N, CollationBuffers, BespokeTemplate, Wrap0i(PrivateEntries[i].HMMLBaseFilename, sizeof(PrivateEntries[i].HMMLBaseFilename)), TRUE) == RC_SUCCESS);
}
if(Inserted)
{
GenerateSearchPages(N, CollationBuffers);
DeleteStaleAssets();
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
}
}
for(int i = 0; i < P->ChildCount; ++i)
{
RecheckPrivacyRecursively(&P->Child[i], N, CollationBuffers, BespokeTemplate);
}
}
void
RecheckPrivacy(neighbourhood *N, buffers *CollationBuffers, template *BespokeTemplate)
{
for(int i = 0; i < Config->ProjectCount; ++i)
{
RecheckPrivacyRecursively(&Config->Project[i], N, CollationBuffers, BespokeTemplate);
}
LastPrivacyCheck = time(0);
}
void
UpdateDeferredAssetChecksums(void)
{
for(int i = 0; i < Assets.Count; ++i)
{
asset *This = GetPlaceInBook(&Assets.Asset, i);
if(This->DeferredUpdate)
{
UpdateAssetInDB(This);
}
}
}
rc
RemoveDirectory(string D)
{
// TODO(matt): Change this to operate on a string
// AFD
int Result;
char *Path = 0;
ExtendString0(&Path, D);
if((remove(Path) == -1))
{
LogError(LOG_NOTICE, "Unable to remove directory %s: %s", Path, strerror(errno));
fprintf(stderr, "%sUnable to remove directory%s %s: %s\n", ColourStrings[CS_WARNING], ColourStrings[CS_END], Path, strerror(errno));
Result = RC_ERROR_DIRECTORY;
}
else
{
LogError(LOG_INFORMATIONAL, "Deleted %s", Path);
fprintf(stderr, "%sDeleted%s %s\n", ColourStrings[CS_DELETION], ColourStrings[CS_END], Path);
Result = RC_SUCCESS;
}
Free(Path);
return Result;
}
void
RemoveChildDirectories(string FullPath, string ParentDirectory)
{
RemoveDirectory(FullPath);
while(FullPath.Length > ParentDirectory.Length)
{
if(FullPath.Base[FullPath.Length] == '/')
{
RemoveDirectory(FullPath);
}
--FullPath.Length;
}
}
void
RemoveDirectoryAndChildren(string FullPath, string ParentDirectory)
{
RemoveChildDirectories(FullPath, ParentDirectory);
RemoveDirectory(ParentDirectory);
}
int
UpgradeDB(int OriginalDBVersion)
{
// NOTE(matt): For each new DB version, we must declare and initialise one instance of each preceding version, only cast the
// incoming DB to the type of the OriginalDBVersion, and move into the final case all operations on that incoming DB
database4 DB4 = { };
switch(OriginalDBVersion)
{
case 4:
{
DB4.Header = *(db_header4 *)DB.Metadata.File.Buffer.Location;
DB.Header.HexSignature = DB4.Header.HexSignature;
DB.Header.CurrentDBVersion = CINERA_DB_VERSION;
DB.Header.CurrentAppVersion = CINERA_APP_VERSION;
DB.Header.CurrentHMMLVersion.Major = hmml_version.Major;
DB.Header.CurrentHMMLVersion.Minor = hmml_version.Minor;
DB.Header.CurrentHMMLVersion.Patch = hmml_version.Patch;
DB.Header.InitialDBVersion = DB4.Header.InitialDBVersion;
DB.Header.InitialAppVersion = DB4.Header.InitialAppVersion;
DB.Header.InitialHMMLVersion = DB4.Header.InitialHMMLVersion;
DB.Header.BlockCount = 0;
DB.ProjectsBlock.BlockID = FOURCC("PROJ");
DB.ProjectsBlock.Count = 0;
++DB.Header.BlockCount;
DB.AssetsBlock.BlockID = FOURCC("ASET");
DB.AssetsBlock.Count = 0;
ClearCopyStringNoFormat(DB.AssetsBlock.RootDir, sizeof(DB.AssetsBlock.RootDir), Config->AssetsRootDir);
ClearCopyStringNoFormat(DB.AssetsBlock.RootURL, sizeof(DB.AssetsBlock.RootURL), Config->AssetsRootURL);
ClearCopyStringNoFormat(DB.AssetsBlock.CSSDir, sizeof(DB.AssetsBlock.CSSDir), Config->CSSDir);
ClearCopyStringNoFormat(DB.AssetsBlock.ImagesDir, sizeof(DB.AssetsBlock.ImagesDir), Config->ImagesDir);
ClearCopyStringNoFormat(DB.AssetsBlock.JSDir, sizeof(DB.AssetsBlock.JSDir), Config->JSDir);
++DB.Header.BlockCount;
DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w");
fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.File.Handle);
DB.Metadata.Signposts.ProjectsBlock.Byte = ftell(DB.Metadata.File.Handle);
fwrite(&DB.ProjectsBlock, sizeof(DB.ProjectsBlock), 1, DB.Metadata.File.Handle);
DB.Metadata.Signposts.AssetsBlock.Byte = ftell(DB.Metadata.File.Handle);
fwrite(&DB.AssetsBlock, sizeof(DB.AssetsBlock), 1, DB.Metadata.File.Handle);
CycleSignpostedFile(&DB.Metadata);
DB.Metadata.Signposts.ProjectsBlock.Ptr = DB.Metadata.File.Buffer.Location + DB.Metadata.Signposts.ProjectsBlock.Byte;
DB.Metadata.Signposts.AssetsBlock.Ptr = DB.Metadata.File.Buffer.Location + DB.Metadata.Signposts.AssetsBlock.Byte;
} // Falls through
// TODO(matt); DBVersion 4 is the first that uses HexSignatures
// We should try and deprecate earlier versions and just enforce a clean rebuild for invalid DB files
// Perhaps do this either the next time we have to bump the version, or when we scrap Single Edition
}
fprintf(stderr, "\n%sUpgraded Cinera DB from %d to %d!%s\n\n", ColourStrings[CS_SUCCESS], OriginalDBVersion, DB.Header.CurrentDBVersion, ColourStrings[CS_END]);
return RC_SUCCESS;
}
typedef struct
{
bool Present;
char ID[MAX_BASE_FILENAME_LENGTH];
} entry_presence_id; // Metadata, unless we actually want to bolster this?
rc
DeleteDeadDBEntries(neighbourhood *N, bool *Modified)
{
// TODO(matt): Additionally remove the BaseDir if it changed
bool NewPlayerLocation = FALSE;
bool NewSearchLocation = FALSE;
if(StringsDiffer(CurrentProject->BaseDir, Wrap0i(N->Project->BaseDir, sizeof(N->Project->BaseDir))) ||
StringsDiffer(CurrentProject->PlayerLocation, Wrap0i(N->Project->PlayerLocation, sizeof(N->Project->PlayerLocation))))
{
string PlayerLocationL = Wrap0i(N->Project->PlayerLocation, sizeof(N->Project->PlayerLocation));
char *OldPlayerDirectory = ConstructDirectoryPath(N->Project, &PlayerLocationL, 0);
db_header_project NewProjectHeader = *N->Project;
ClearCopyStringNoFormat(NewProjectHeader.BaseDir, sizeof(NewProjectHeader.BaseDir), CurrentProject->BaseDir);
char *NewPlayerDirectory = ConstructDirectoryPath(&NewProjectHeader, &CurrentProject->PlayerLocation, 0);
printf("%sRelocating Player Page%s from %s to %s%s\n",
ColourStrings[CS_REINSERTION], N->Project->EntryCount > 1 ? "s" : "",
OldPlayerDirectory, NewPlayerDirectory, ColourStrings[CS_END]);
Free(NewPlayerDirectory);
db_entry *FirstEntry = LocateFirstEntry(N->Project);
for(int EntryIndex = 0; EntryIndex < N->Project->EntryCount; ++EntryIndex)
{
db_entry *This = FirstEntry + EntryIndex;
DeletePlayerPageFromFilesystem(N->Project, Wrap0i(This->OutputLocation, sizeof(This->OutputLocation)), TRUE, TRUE);
}
if(PlayerLocationL.Length > 0)
{
RemoveChildDirectories(Wrap0(OldPlayerDirectory), Wrap0i(N->Project->BaseDir, sizeof(N->Project->BaseDir)));
}
Free(OldPlayerDirectory);
ClearCopyStringNoFormat(N->Project->PlayerLocation, sizeof(N->Project->PlayerLocation), CurrentProject->PlayerLocation);
NewPlayerLocation = TRUE;
}
if(StringsDiffer(CurrentProject->BaseDir, Wrap0i(N->Project->BaseDir, sizeof(N->Project->BaseDir))) ||
StringsDiffer(CurrentProject->SearchLocation, Wrap0i(N->Project->SearchLocation, sizeof(N->Project->SearchLocation))))
{
string SearchLocationL = Wrap0i(N->Project->SearchLocation, sizeof(N->Project->SearchLocation));
char *OldSearchDirectory = ConstructDirectoryPath(N->Project, &SearchLocationL, 0);
db_header_project NewProjectHeader = *N->Project;
ClearCopyStringNoFormat(NewProjectHeader.BaseDir, sizeof(NewProjectHeader.BaseDir), CurrentProject->BaseDir);
ClearCopyStringNoFormat(NewProjectHeader.SearchLocation, sizeof(NewProjectHeader.SearchLocation), CurrentProject->SearchLocation);
char *NewSearchDirectory = ConstructDirectoryPath(&NewProjectHeader, &CurrentProject->SearchLocation, 0);
MakeDir(Wrap0(NewSearchDirectory));
printf("%sRelocating Search Page from %s to %s%s\n",
ColourStrings[CS_REINSERTION], OldSearchDirectory, NewSearchDirectory, ColourStrings[CS_END]);
char *OldSearchPagePath = MakeString0("ss", OldSearchDirectory, "/index.html");
char *NewSearchPagePath = MakeString0("ss", NewSearchDirectory, "/index.html");
rename(OldSearchPagePath, NewSearchPagePath);
Free(OldSearchPagePath);
Free(NewSearchPagePath);
char *OldSearchIndexPath = ConstructIndexFilePath(N->Project);
char *NewSearchIndexPath = ConstructIndexFilePath(&NewProjectHeader);
rename(OldSearchIndexPath, NewSearchIndexPath);
Free(OldSearchIndexPath);
Free(NewSearchIndexPath);
Free(NewSearchDirectory);
FreeFile(&DB.File);
DB.File = InitFile(&CurrentProject->BaseDir, &CurrentProject->ID, EXT_INDEX);
ReadFileIntoBuffer(&DB.File); // NOTE(matt): Could we actually catch errors (permissions?) here and bail?
if(SearchLocationL.Length > 0)
{
RemoveChildDirectories(Wrap0(OldSearchDirectory), Wrap0i(N->Project->BaseDir, sizeof(N->Project->BaseDir)));
}
remove(OldSearchDirectory);
Free(OldSearchDirectory);
ClearCopyStringNoFormat(N->Project->SearchLocation, sizeof(N->Project->SearchLocation), CurrentProject->SearchLocation);
NewSearchLocation = TRUE;
}
if(StringsDiffer(CurrentProject->BaseDir, Wrap0i(N->Project->BaseDir, sizeof(N->Project->BaseDir))))
{
ClearCopyStringNoFormat(N->Project->BaseDir, sizeof(N->Project->BaseDir), CurrentProject->BaseDir);
DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w");
WriteFromByteToEnd(&DB.Metadata.File, 0);
CycleSignpostedFile(&DB.Metadata);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
*Modified = TRUE;
}
if(StringsDiffer(CurrentProject->BaseURL, Wrap0i(N->Project->BaseURL, sizeof(N->Project->BaseURL))))
{
ClearCopyStringNoFormat(N->Project->BaseURL, sizeof(N->Project->BaseURL), CurrentProject->BaseURL);
DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w");
WriteFromByteToEnd(&DB.Metadata.File, 0);
CycleSignpostedFile(&DB.Metadata);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
*Modified = TRUE;
}
if(NewPlayerLocation || NewSearchLocation)
{
if(!(DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w"))) { FreeBuffer(&DB.Metadata.File.Buffer); return RC_ERROR_FILE; }
ClearCopyStringNoFormat(N->Project->BaseDir, sizeof(N->Project->BaseDir), CurrentProject->BaseDir);
ClearCopyStringNoFormat(N->Project->BaseURL, sizeof(N->Project->BaseURL), CurrentProject->BaseURL);
fwrite(DB.Metadata.File.Buffer.Location, DB.Metadata.File.Buffer.Size, 1, DB.Metadata.File.Handle);
CycleSignpostedFile(&DB.Metadata);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
}
entry_presence_id Entries[N->Project->EntryCount];
db_entry *FirstEntry = LocateFirstEntry(N->Project);
for(int EntryIndex = 0; EntryIndex < N->Project->EntryCount; ++EntryIndex)
{
db_entry *This = FirstEntry + EntryIndex;
CopyStringNoFormat(Entries[EntryIndex].ID, sizeof(Entries[EntryIndex].ID), Wrap0i(This->HMMLBaseFilename, sizeof(This->HMMLBaseFilename)));
Entries[EntryIndex].Present = FALSE;
}
char *HMMLDir0 = MakeString0("l", &CurrentProject->HMMLDir);
DIR *HMMLDirHandle = opendir(HMMLDir0);
Free(HMMLDir0);
if(!HMMLDirHandle)
{
LogError(LOG_ERROR, "Unable to scan project directory %.*s: %s", (int)CurrentProject->HMMLDir.Length, CurrentProject->HMMLDir.Base, strerror(errno));
fprintf(stderr, "Unable to scan project directory %.*s: %s\n", (int)CurrentProject->HMMLDir.Length, CurrentProject->HMMLDir.Base, strerror(errno));
return RC_ERROR_DIRECTORY;
}
struct dirent *ProjectFiles;
while((ProjectFiles = readdir(HMMLDirHandle)))
{
string Filename = Wrap0(ProjectFiles->d_name);
if(ExtensionMatches(Filename, EXT_HMML))
{
for(int i = 0; i < N->Project->EntryCount; ++i)
{
if(StringsMatch(Wrap0(Entries[i].ID), GetBaseFilename(Filename, EXT_HMML)))
{
Entries[i].Present = TRUE;
break;
}
}
}
}
closedir(HMMLDirHandle);
bool Deleted = FALSE;
for(int i = 0; i < N->Project->EntryCount; ++i)
{
if(Entries[i].Present == FALSE)
{
Deleted = TRUE;
ResetNeighbourhood(N);
DeleteEntry(N, Wrap0(Entries[i].ID));
}
}
return Deleted ? RC_SUCCESS : RC_NOOP;
}
int
SyncDBWithInput(neighbourhood *N, buffers *CollationBuffers, template *BespokeTemplate)
{
MEM_TEST_INITIAL();
if(DB.Metadata.File.Buffer.Size > 0 && Config->QueryString.Length == 0)
{
DeleteAllLandmarksAndAssets();
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
}
bool Modified = FALSE;
if(StringsDiffer(CurrentProject->Title, Wrap0i(N->Project->Title, sizeof(N->Project->Title))))
{
ClearCopyStringNoFormat(N->Project->Title, sizeof(N->Project->Title), CurrentProject->Title);
DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w");
WriteFromByteToEnd(&DB.Metadata.File, 0);
CycleSignpostedFile(&DB.Metadata);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
Modified = TRUE;
}
if(StringsDiffer(CurrentProject->Theme, Wrap0i(N->Project->Theme, sizeof(N->Project->Theme))))
{
ClearCopyStringNoFormat(N->Project->Theme, sizeof(N->Project->Theme), CurrentProject->Theme);
DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w");
WriteFromByteToEnd(&DB.Metadata.File, 0);
CycleSignpostedFile(&DB.Metadata);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
Modified = TRUE;
}
if(StringsDiffer(CurrentProject->Unit, Wrap0i(N->Project->Unit, sizeof(N->Project->Unit))))
{
ClearCopyStringNoFormat(N->Project->Unit, sizeof(N->Project->Unit), CurrentProject->Unit);
DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w");
WriteFromByteToEnd(&DB.Metadata.File, 0);
CycleSignpostedFile(&DB.Metadata);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
Modified = TRUE;
}
bool Deleted = FALSE;
Deleted = (DB.Metadata.File.Buffer.Size > 0 && DeleteDeadDBEntries(N, &Modified) == RC_SUCCESS);
// NOTE(matt): Stack-string
char HMMLDir0[CurrentProject->HMMLDir.Length + 1];
CopyStringNoFormat(HMMLDir0, sizeof(HMMLDir0), CurrentProject->HMMLDir);
DIR *HMMLDirHandle = opendir(HMMLDir0);
if(!HMMLDirHandle)
{
LogError(LOG_ERROR, "Unable to scan project directory %.*s: %s", (int)CurrentProject->HMMLDir.Length, CurrentProject->HMMLDir.Base, strerror(errno));
fprintf(stderr, "Unable to scan project directory %.*s: %s\n", (int)CurrentProject->HMMLDir.Length, CurrentProject->HMMLDir.Base, strerror(errno));
return RC_ERROR_DIRECTORY;
}
MEM_TEST_MID("SyncDBWithInput1");
struct dirent *ProjectFiles;
bool Inserted = FALSE;
while((ProjectFiles = readdir(HMMLDirHandle)))
{
string Filename = Wrap0(ProjectFiles->d_name);
if(ExtensionMatches(Filename, EXT_HMML))
{
ResetNeighbourhood(N);
MEM_TEST_MID("SyncDBWithInput1a");
Inserted |= (InsertEntry(N, CollationBuffers, BespokeTemplate, GetBaseFilename(Filename, EXT_HMML), 0) == RC_SUCCESS);
MEM_TEST_MID("SyncDBWithInput1b");
VerifyLandmarks(N);
}
}
closedir(HMMLDirHandle);
MEM_TEST_MID("SyncDBWithInput2");
UpdateDeferredAssetChecksums();
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
if(Deleted || Inserted || Modified)
{
// TODO(matt): The DB may not be set up correctly at this point. Figure out why!
//
// To reproduce:
// 1. Generate it with a blank global_search_dir and global_search_url
// 2. Generate it with a filled global_search_dir and global_search_url
//
// Of note: We seem to be producing the asset landmark for Generation -1 just fine. The problem is that
// we're not getting the index.html file
GenerateSearchPages(N, CollationBuffers);
DeleteStaleAssets();
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
VerifyLandmarks(N);
}
MEM_TEST_AFTER("SyncDBWithInput()");
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);
}
void
AccumulateProjectIndices(project_generations *A, db_header_project *P)
{
AddEntryToGeneration(A, 0);
db_header_project *Child = LocateFirstChildProject(P);
IncrementCurrentGeneration(A);
for(int i = 0; i < P->ChildCount; ++i)
{
AccumulateProjectIndices(A, Child);
Child = SkipProjectAndChildren(Child);
}
DecrementCurrentGeneration(A);
}
project_generations
CopyAccumulator(project_generations *G)
{
project_generations Result = {};
for(int i = 0; i < G->Count; ++i)
{
PushGeneration(&Result);
Result.EntriesInGeneration[i] = G->EntriesInGeneration[i];
}
return Result;
}
project_generations
InitAccumulator(project_generations *G)
{
project_generations Result = {};
for(int i = 0; i < G->CurrentGeneration; ++i)
{
PushGeneration(&Result);
++Result.CurrentGeneration;
}
return Result;
}
void
WriteEntireDatabase()
{
// NOTE(matt): This may suffice when the only changes are to existing fixed-size variables,
// e.g. ProjectsBlock->GlobalSearchDir
DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w");
fwrite(DB.Metadata.File.Buffer.Location, DB.Metadata.File.Buffer.Size, 1, DB.Metadata.File.Handle);
fclose(DB.Metadata.File.Handle);
}
int
InitDB(void)
{
// TODO(matt): InitDB() is called once on startup. This is correct for the .metadata file, because we only want one of
// those to house the info for all projects. However, we will want to create a .index file for each project, so
// need a separate InitIndex() function that we can call when looping over the projects after ParseConfig()
DB.Metadata.File.Buffer.ID = BID_DATABASE;
DB.Metadata.File = InitFile(0, &Config->DatabaseLocation, EXT_NULL);
ReadFileIntoBuffer(&DB.Metadata.File); // NOTE(matt): Could we actually catch errors (permissions?) here and bail?
if(DB.Metadata.File.Buffer.Location)
{
// TODO(matt): Handle this gracefully (it'll be an invalid file)
Assert(DB.Metadata.File.Buffer.Size >= sizeof(DB.Header));
uint32_t OriginalDBVersion = 0;
uint32_t FirstInt = *(uint32_t *)DB.Metadata.File.Buffer.Location;
if(FirstInt != FOURCC("CNRA"))
{
// TODO(matt): More checking, somehow? Ideally this should be able to report "Invalid .metadata file" rather than
// trying to upgrade a file that isn't even valid
// TODO(matt): We should try and deprecate < CINERA_DB_VERSION 4 and enforce a clean rebuild for invalid DB files
// Perhaps do this either the next time we have to bump the version, or when we scrap Single Edition
OriginalDBVersion = FirstInt;
}
else
{
OriginalDBVersion = *(uint32_t *)(DB.Metadata.File.Buffer.Location + sizeof(DB.Header.HexSignature));
}
if(OriginalDBVersion < CINERA_DB_VERSION)
{
if(CINERA_DB_VERSION == 6)
{
fprintf(stderr, "\n%sHandle conversion from CINERA_DB_VERSION %d to %d!%s\n\n", ColourStrings[CS_ERROR], OriginalDBVersion, CINERA_DB_VERSION, ColourStrings[CS_END]);
FreeConfig(Config);
_exit(RC_ERROR_FATAL);
}
if(UpgradeDB(OriginalDBVersion) == RC_ERROR_FILE) { FreeConfig(Config); return RC_NOOP; }
}
else if(OriginalDBVersion > CINERA_DB_VERSION)
{
fprintf(stderr, "%sUnsupported DB Version (%d). Please upgrade Cinera%s\n", ColourStrings[CS_ERROR], OriginalDBVersion, ColourStrings[CS_END]);
FreeConfig(Config);
_exit(RC_ERROR_FATAL);
}
db_header *Header = (db_header *)DB.Metadata.File.Buffer.Location;
Header->CurrentAppVersion = CINERA_APP_VERSION;
Header->CurrentHMMLVersion.Major = hmml_version.Major;
Header->CurrentHMMLVersion.Minor = hmml_version.Minor;
Header->CurrentHMMLVersion.Patch = hmml_version.Patch;
buffer *B = &DB.Metadata.File.Buffer;
B->Ptr = B->Location + sizeof(*Header);
for(int BlockIndex = 0; BlockIndex < Header->BlockCount; ++BlockIndex)
{
uint32_t BlockID = *(uint32_t *)B->Ptr;
if(BlockID == FOURCC("PROJ"))
{
DB.Metadata.Signposts.ProjectsBlock.Ptr = B->Ptr;
B->Ptr = SkipBlock(B->Ptr);
}
else if(BlockID == FOURCC("ASET"))
{
DB.Metadata.Signposts.AssetsBlock.Ptr = B->Ptr;
DB.Metadata.File.Buffer.Ptr = SkipBlock(B->Ptr);
}
else
{
fprintf(stderr, "%sMalformed metadata file%s: %s\n",
ColourStrings[CS_ERROR], ColourStrings[CS_END], DB.Metadata.File.Path);
}
}
DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w");
if(DB.Metadata.File.Handle)
{
fwrite(B->Location, B->Size, 1, DB.Metadata.File.Handle);
SetFileEditPosition(&DB.Metadata);
CycleSignpostedFile(&DB.Metadata);
}
else
{
// TODO(matt): Handle unopenable database files
PrintC(CS_RED, "Could not open database file: ");
PrintC(CS_MAGENTA, DB.Metadata.File.Path);
fprintf(stderr, "\n");
_exit(0);
}
}
else
{
// NOTE(matt): Initialising new db_header
DB.Header.HexSignature = FOURCC("CNRA");
DB.Header.InitialDBVersion = DB.Header.CurrentDBVersion = CINERA_DB_VERSION;
DB.Header.InitialAppVersion = DB.Header.CurrentAppVersion = CINERA_APP_VERSION;
DB.Header.InitialHMMLVersion.Major = DB.Header.CurrentHMMLVersion.Major = hmml_version.Major;
DB.Header.InitialHMMLVersion.Minor = DB.Header.CurrentHMMLVersion.Minor = hmml_version.Minor;
DB.Header.InitialHMMLVersion.Patch = DB.Header.CurrentHMMLVersion.Patch = hmml_version.Patch;
DB.Header.BlockCount = 0;
DB.ProjectsBlock.BlockID = FOURCC("PROJ");
ClearCopyStringNoFormat(DB.ProjectsBlock.GlobalSearchDir, sizeof(DB.ProjectsBlock.GlobalSearchDir), Config->GlobalSearchDir);
ClearCopyStringNoFormat(DB.ProjectsBlock.GlobalSearchURL, sizeof(DB.ProjectsBlock.GlobalSearchURL), Config->GlobalSearchURL);
DB.ProjectsBlock.Count = 0;
++DB.Header.BlockCount;
DB.AssetsBlock.BlockID = FOURCC("ASET");
DB.AssetsBlock.Count = 0;
ClearCopyStringNoFormat(DB.AssetsBlock.RootDir, sizeof(DB.AssetsBlock.RootDir), Config->AssetsRootDir);
ClearCopyStringNoFormat(DB.AssetsBlock.RootURL, sizeof(DB.AssetsBlock.RootURL), Config->AssetsRootURL);
ClearCopyStringNoFormat(DB.AssetsBlock.CSSDir, sizeof(DB.AssetsBlock.CSSDir), Config->CSSDir);
ClearCopyStringNoFormat(DB.AssetsBlock.ImagesDir, sizeof(DB.AssetsBlock.ImagesDir), Config->ImagesDir);
ClearCopyStringNoFormat(DB.AssetsBlock.JSDir, sizeof(DB.AssetsBlock.JSDir), Config->JSDir);
++DB.Header.BlockCount;
char *DatabaseLocation0 = MakeString0("l", &Config->DatabaseLocation);
StripComponentFromPath0(DatabaseLocation0);
DIR *OutputDirectoryHandle = opendir(DatabaseLocation0);
if(!OutputDirectoryHandle)
{
if(!MakeDir(Wrap0(DatabaseLocation0)))
{
LogError(LOG_ERROR, "Unable to create directory %.*s: %s", (int)Config->DatabaseLocation.Length, Config->DatabaseLocation.Base, strerror(errno));
fprintf(stderr, "Unable to create directory %.*s: %s\n", (int)Config->DatabaseLocation.Length, Config->DatabaseLocation.Base, strerror(errno));
Free(DatabaseLocation0);
return RC_ERROR_DIRECTORY;
};
}
else
{
closedir(OutputDirectoryHandle);
OutputDirectoryHandle = 0;
}
Free(DatabaseLocation0);
DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w");
if(DB.Metadata.File.Handle)
{
fwrite(&DB.Header, sizeof(DB.Header), 1, DB.Metadata.File.Handle);
DB.Metadata.Signposts.ProjectsBlock.Byte = ftell(DB.Metadata.File.Handle);
fwrite(&DB.ProjectsBlock, sizeof(DB.ProjectsBlock), 1, DB.Metadata.File.Handle);
DB.Metadata.Signposts.AssetsBlock.Byte = ftell(DB.Metadata.File.Handle);
fwrite(&DB.AssetsBlock, sizeof(DB.AssetsBlock), 1, DB.Metadata.File.Handle);
fclose(DB.Metadata.File.Handle);
ReadFileIntoBuffer(&DB.Metadata.File);
DB.Metadata.Signposts.ProjectsBlock.Ptr = DB.Metadata.File.Buffer.Location + DB.Metadata.Signposts.ProjectsBlock.Byte;
DB.Metadata.Signposts.AssetsBlock.Ptr = DB.Metadata.File.Buffer.Location + DB.Metadata.Signposts.AssetsBlock.Byte;
}
else
{
// TODO(matt): Handle unopenable database files
PrintC(CS_RED, "Could not open database file: ");
PrintC(CS_MAGENTA_BOLD, DB.Metadata.File.Path);
fprintf(stderr, "\n");
_exit(0);
}
#if 0
DB.File.Handle = fopen(DB.File.Path, "w");
fprintf(DB.File.Handle, "---\n");
fclose(DB.File.Handle);
ReadFileIntoBuffer(&DB.File);
#endif
}
return RC_SUCCESS;
}
void
InitProjectInDBRecursively(project_generations *G, project *P)
{
// TODO(matt): Remove this section once we know it has been run at least once!
char *BaseDir0 = MakeString0("l", &P->BaseDir);
DIR *BaseDirHandle = opendir(BaseDir0);
Free(BaseDir0);
if(BaseDirHandle)
{
struct dirent *BaseDirFiles;
bool RemovedOldMetadataFile = FALSE;
while((BaseDirFiles = readdir(BaseDirHandle)) && !RemovedOldMetadataFile)
{
char *Ptr = BaseDirFiles->d_name;
Ptr += (StringLength(BaseDirFiles->d_name) - StringLength(".metadata"));
if(!(StringsDiffer0(Ptr, ".metadata")))
{
char *MetadataPath = MakeString0("lss", &P->BaseDir, "/", BaseDirFiles->d_name);
remove(MetadataPath);
Free(MetadataPath);
rewinddir(BaseDirHandle);
while((BaseDirFiles = readdir(BaseDirHandle)))
{
string Filename = Wrap0(BaseDirFiles->d_name);
if(ExtensionMatches(Filename, EXT_INDEX))
{
// NOTE(matt): Stack-string
int NullTerminationBytes = 1;
char IndexPath[P->BaseDir.Length + StringLength("/") + Filename.Length + NullTerminationBytes];
char *Ptr = IndexPath;
Ptr += CopyStringToBarePtr(Ptr, P->BaseDir);
Ptr += CopyStringToBarePtr(Ptr, Wrap0("/"));
Ptr += CopyStringToBarePtr(Ptr, Filename);
*Ptr = '\0';
remove(IndexPath);
break;
}
}
RemovedOldMetadataFile = TRUE;
}
}
closedir(BaseDirHandle);
}
//
////
AddEntryToGeneration(G, P);
db_header_project Project = {};
CopyStringNoFormat(Project.ID, sizeof(Project.ID), P->ID);
CopyStringNoFormat(Project.Title, sizeof(Project.Title), P->Title);
// TODO(matt): Store the Theme as an index into the Assets block?
CopyStringNoFormat(Project.Theme, sizeof(Project.Theme), P->Theme);
CopyStringNoFormat(Project.BaseDir, sizeof(Project.BaseDir), P->BaseDir);
CopyStringNoFormat(Project.BaseURL, sizeof(Project.BaseURL), P->BaseURL);
CopyStringNoFormat(Project.SearchLocation, sizeof(Project.SearchLocation), P->SearchLocation);
CopyStringNoFormat(Project.PlayerLocation, sizeof(Project.PlayerLocation), P->PlayerLocation);
CopyStringNoFormat(Project.Unit, sizeof(Project.Unit), P->Unit);
Project.ArtIndex = SAI_UNSET;
Project.IconIndex = SAI_UNSET;
Project.EntryCount = 0;
Project.ChildCount = P->ChildCount;
fwrite(&Project, sizeof(Project), 1, DB.Metadata.File.Handle);
AccumulateFileEditSize(&DB.Metadata, sizeof(Project));
IncrementCurrentGeneration(G);
for(int i = 0; i < P->ChildCount; ++i)
{
InitProjectInDBRecursively(G, &P->Child[i]);
}
DecrementCurrentGeneration(G);
}
void
OffsetAssetLandmarks(db_asset *A, uint64_t *BytesThroughFile, project_generations *OldAcc, project_generations *NewAcc)
{
db_landmark *Landmark = LocateFirstLandmark(A);
WriteFromByteToPointer(&DB.Metadata.File, BytesThroughFile, Landmark);
int i = 0;
while(i < A->LandmarkCount && Landmark->Project.Generation == -1)
{
fwrite(Landmark, sizeof(db_landmark), 1, DB.Metadata.File.Handle);
*BytesThroughFile += sizeof(db_landmark);
++Landmark;
++i;
}
for(; i < A->LandmarkCount; ++i, ++Landmark)
{
db_landmark NewLandmark = *Landmark;
uint32_t OldEntriesInGeneration = OldAcc->EntriesInGeneration ? OldAcc->EntriesInGeneration[NewLandmark.Project.Generation] : 0;
if(NewLandmark.Project.Index >= OldEntriesInGeneration)
{
NewLandmark.Project.Index +=
(NewLandmark.Project.Generation < NewAcc->Count ? NewAcc->EntriesInGeneration[NewLandmark.Project.Generation] : 0)
-
(NewLandmark.Project.Generation < OldAcc->Count ? OldEntriesInGeneration : 0);
}
fwrite(&NewLandmark, sizeof(db_landmark), 1, DB.Metadata.File.Handle);
*BytesThroughFile += sizeof(db_landmark);
}
}
db_header_project *
InitProjectInDBPostamble(project_generations *OldAcc, project_generations *NewAcc)
{
uint64_t ProjectHeaderPos = DB.Metadata.Signposts.Edit.BytePosition;
uint64_t Byte = ProjectHeaderPos;
db_block_assets *AssetsBlock = LocateBlock(B_ASET);
db_asset *Asset = LocateFirstAsset(AssetsBlock);
WriteFromByteToPointer(&DB.Metadata.File, &Byte, Asset);
for(int i = 0; i < AssetsBlock->Count; ++i)
{
OffsetAssetLandmarks(Asset, &Byte, OldAcc, NewAcc);
Asset = SkipAsset(Asset);
}
WriteFromByteToEnd(&DB.Metadata.File, Byte);
CycleSignpostedFile(&DB.Metadata);
DB.Metadata.Signposts.ProjectHeader.Ptr = DB.Metadata.File.Buffer.Location + ProjectHeaderPos;
return DB.Metadata.Signposts.ProjectHeader.Ptr;
}
void
InsertProjectIntoDB_TopLevel(project_generations *G, db_block_projects **Block, db_header_project **Child, project *P)
{
uint64_t Byte = 0;
DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w");
WriteFromByteToPointer(&DB.Metadata.File, &Byte, *Block);
db_block_projects NewBlock = **Block;
++NewBlock.Count;
fwrite(&NewBlock, sizeof(db_block_projects), 1, DB.Metadata.File.Handle);
Byte += sizeof(db_block_projects);
WriteFromByteToPointer(&DB.Metadata.File, &Byte, *Child);
SetFileEditPosition(&DB.Metadata);
project_generations OldG = CopyAccumulator(G);
InitProjectInDBRecursively(G, P);
uint64_t BlockPos = (char *)*Block - DB.Metadata.File.Buffer.Location;
*Child = InitProjectInDBPostamble(&OldG, G);
*Block = (db_block_projects *)(DB.Metadata.File.Buffer.Location + BlockPos);
}
void
InsertProjectIntoDB(project_generations *G, db_header_project **Parent, db_header_project **Child, project *P)
{
uint64_t Byte = 0;
DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w");
WriteFromByteToPointer(&DB.Metadata.File, &Byte, *Parent);
db_header_project NewParent = **Parent;
++NewParent.ChildCount;
fwrite(&NewParent, sizeof(db_header_project), 1, DB.Metadata.File.Handle);
Byte += sizeof(db_header_project);
WriteFromByteToPointer(&DB.Metadata.File, &Byte, *Child);
SetFileEditPosition(&DB.Metadata);
project_generations OldG = CopyAccumulator(G);
InitProjectInDBRecursively(G, P);
uint64_t PPos = (char *)*Parent - DB.Metadata.File.Buffer.Location;
*Child = InitProjectInDBPostamble(&OldG, G);
*Parent = (db_header_project *)(DB.Metadata.File.Buffer.Location + PPos);
}
bool
ConfiguredAndStoredProjectIDsMatch(project *ConfiguredP, db_header_project *StoredP)
{
return StringsMatch(ConfiguredP->ID, Wrap0i(StoredP->ID, sizeof(StoredP->ID)));
}
void
DeleteLandmarksOfProjectAndChildren(db_asset *A, uint64_t *BytesThroughBuffer, project_generations *MainAcc, project_generations *LocalAcc)
{
//db_project_index Index = GetCurrentProjectIndex(MainAcc);
//landmark_range LandmarkRange = DetermineProjectLandmarksRange(A, Index);
//LandmarkDeletionCount += LandmarkRange.Length;
uint64_t LandmarkDeletionCount = 0;
for(int i = LocalAcc->CurrentGeneration; i < LocalAcc->Count; ++i)
{
for(int j = 0; j < LocalAcc->EntriesInGeneration[i]; ++j)
{
db_project_index Acc = {};
Acc.Generation = i;
Acc.Index = j;
if(i < MainAcc->Count)
{
Acc.Index += MainAcc->EntriesInGeneration[i];
}
landmark_range LandmarkRange = DetermineProjectLandmarksRange(A, Acc);
LandmarkDeletionCount += LandmarkRange.Length;
}
}
AccumulateFileEditSize(&DB.Metadata, -(sizeof(db_landmark) * LandmarkDeletionCount));
db_asset NewAsset = *A;
NewAsset.LandmarkCount -= LandmarkDeletionCount;
WriteFromByteToPointer(&DB.Metadata.File, BytesThroughBuffer, A);
fwrite(&NewAsset, sizeof(db_asset), 1, DB.Metadata.File.Handle);
*BytesThroughBuffer += sizeof(db_asset);
// TODO(matt): For each generation in our LocalAcc
// Write out the landmarks to the first landmark in our range
// Omit the landmarks within our range
// Decrement the project index of the remaining landmarks in this generation
//
db_landmark *FirstLandmark = LocateFirstLandmark(A);
uint64_t LandmarkIndex = 0;
for(int GenIndex = LocalAcc->CurrentGeneration; GenIndex < LocalAcc->Count; ++GenIndex)
{
uint64_t GenLandmarkDeletionCount = 0;
db_landmark *GenLandmark = 0;
//uint64_t LandmarkIndex = 0;
for(int j = 0; j < LocalAcc->EntriesInGeneration[GenIndex]; ++j)
{
db_project_index Acc = {};
Acc.Generation = GenIndex;
Acc.Index = j;
if(GenIndex < MainAcc->Count)
{
Acc.Index += MainAcc->EntriesInGeneration[GenIndex];
}
landmark_range LandmarkRange = DetermineProjectLandmarksRange(A, Acc);
GenLandmarkDeletionCount += LandmarkRange.Length;
if(!GenLandmark && LandmarkRange.Length > 0)
{
GenLandmark = FirstLandmark + LandmarkRange.First;
LandmarkIndex = LandmarkRange.First;
}
LandmarkIndex += LandmarkRange.Length;
}
//db_landmark *Landmark = LocateFirstLandmark(A) + LandmarkRange.First;
if(GenLandmark)
{
WriteFromByteToPointer(&DB.Metadata.File, BytesThroughBuffer, GenLandmark);
*BytesThroughBuffer += sizeof(db_landmark) * GenLandmarkDeletionCount;
GenLandmark += GenLandmarkDeletionCount;
}
db_landmark *TailLandmark = FirstLandmark + LandmarkIndex;
for(;
LandmarkIndex < A->LandmarkCount && TailLandmark->Project.Generation == GenIndex;
++LandmarkIndex, ++TailLandmark)
{
db_landmark NewLandmark = *TailLandmark;
// NOTE(matt): These are all unsigned values. If the subtraction would yield a negative value, it actually ends up
// being positive because the high bit is set. Casting them to a larger type removes the possibility
// for under / overflow, and treating them as signed permits signed comparison
if((int64_t)NewLandmark.Project.Index - (int64_t)LocalAcc->EntriesInGeneration[GenIndex] >= (int64_t)MainAcc->EntriesInGeneration[GenIndex])
{
NewLandmark.Project.Index -= LocalAcc->EntriesInGeneration[GenIndex];
}
fwrite(&NewLandmark, sizeof(db_landmark), 1, DB.Metadata.File.Handle);
*BytesThroughBuffer += sizeof(db_landmark);
}
}
db_landmark *Landmark = FirstLandmark + LandmarkIndex;
fwrite(Landmark, sizeof(db_landmark), A->LandmarkCount - LandmarkIndex, DB.Metadata.File.Handle);
*BytesThroughBuffer += sizeof(db_landmark) * (A->LandmarkCount - LandmarkIndex);
}
void *
DeleteHTMLFilesOfProject(db_header_project *Project)
{
char *Ptr = (char *)Project;
Ptr += sizeof(db_header_project) + sizeof(db_entry) * Project->EntryCount;
db_entry *Entry = LocateFirstEntry(Project);
for(int i = 0; i < Project->EntryCount; ++i, ++Entry)
{
DeletePlayerPageFromFilesystem(Project, Wrap0i(Entry->OutputLocation, sizeof(Entry->OutputLocation)), FALSE, FALSE);
}
DeleteSearchPageFromFilesystem(Project);
return Ptr;
}
void *
DeleteHTMLFilesOfProjectAndChildren(db_header_project *Project)
{
db_header_project *Ptr = DeleteHTMLFilesOfProject(Project);
for(int i = 0; i < Project->ChildCount; ++i)
{
Ptr = DeleteHTMLFilesOfProjectAndChildren(Ptr);
}
DeleteSearchPageFromFilesystem(Project);
return Ptr;
}
void
DeleteProjectInterior(db_header_project **Child, project_generations *G, uint64_t *BytesThroughBuffer)
{
WriteFromByteToPointer(&DB.Metadata.File, BytesThroughBuffer, *Child);
SetFileEditPosition(&DB.Metadata);
char *AfterChild = DeleteHTMLFilesOfProjectAndChildren(*Child);
AccumulateFileEditSize(&DB.Metadata, -(AfterChild - (char *)*Child));
*BytesThroughBuffer += AfterChild - (char *)*Child;
DB.Metadata.Signposts.AssetsBlock.Ptr = LocateBlock(B_ASET);
db_block_assets *AssetsBlock = DB.Metadata.Signposts.AssetsBlock.Ptr;
db_asset *Asset = LocateFirstAsset(AssetsBlock);
WriteFromByteToPointer(&DB.Metadata.File, BytesThroughBuffer, Asset);
project_generations ChildAccumulator = InitAccumulator(G);
AccumulateProjectIndices(&ChildAccumulator, *Child);
for(int AssetIndex = 0; AssetIndex < AssetsBlock->Count; ++AssetIndex)
{
DeleteLandmarksOfProjectAndChildren(Asset, BytesThroughBuffer, G, &ChildAccumulator);
Asset = SkipAsset(Asset);
}
*BytesThroughBuffer += WriteFromByteToEnd(&DB.Metadata.File, *BytesThroughBuffer);
CycleSignpostedFile(&DB.Metadata);
}
void
DeleteProject_TopLevel(db_block_projects **Parent, db_header_project **Child, project_generations *G)
{
PrintFunctionName("DeleteProject_TopLevel()");
// TODO(matt); Print out something sensible to inform real life users
// TODO(matt):
//
// 0:1
// 1:4
uint64_t PPos = (char *)*Parent - DB.Metadata.File.Buffer.Location;
uint64_t CPos = (char *)*Child - DB.Metadata.File.Buffer.Location;
DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w");
uint64_t Byte = 0;
WriteFromByteToPointer(&DB.Metadata.File, &Byte, *Parent);
db_block_projects NewParent = **Parent;
--NewParent.Count;
fwrite(&NewParent, sizeof(db_block_projects), 1, DB.Metadata.File.Handle);
Byte += sizeof(db_block_projects);
DeleteProjectInterior(Child, G, &Byte);
*Parent = (db_block_projects *)(DB.Metadata.File.Buffer.Location + PPos);
*Child = (db_header_project *)(DB.Metadata.File.Buffer.Location + CPos);
}
void
DeleteProject(db_header_project **Parent, db_header_project **Child, project_generations *G)
{
PrintFunctionName("DeleteProject()");
// TODO(matt); Print out something sensible to inform real life users
// TODO(matt):
//
// 0:1
// 1:4
uint64_t PPos = (char *)*Parent - DB.Metadata.File.Buffer.Location;
uint64_t CPos = (char *)*Child - DB.Metadata.File.Buffer.Location;
//db_project_index Index = GetCurrentProjectIndex(G);
DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w");
uint64_t Byte = 0;
WriteFromByteToPointer(&DB.Metadata.File, &Byte, *Parent);
db_header_project NewParent = **Parent;
--NewParent.ChildCount;
fwrite(&NewParent, sizeof(db_header_project), 1, DB.Metadata.File.Handle);
Byte += sizeof(db_header_project);
DeleteProjectInterior(Child, G, &Byte);
*Parent = (db_header_project *)(DB.Metadata.File.Buffer.Location + PPos);
*Child = (db_header_project *)(DB.Metadata.File.Buffer.Location + CPos);
}
void SyncProjects(project_generations *G, project *C, db_header_project **SParent, db_header_project **SChild);
void SyncProjects_TopLevel(project_generations *G, project *C, db_block_projects **SParent, db_header_project **SChild);
db_landmark *
DetermineFirstLandmarkAndRangeOfProjectHierarchy(db_asset *A,
project_generations *MainAcc, project_generations *PrevAcc, project_generations *ThisAcc,
uint32_t GenIndex, landmark_range *Dest)
{
db_landmark *Result = 0;
for(int j = 0; j < ThisAcc->EntriesInGeneration[GenIndex]; ++j)
{
db_project_index ThisIndex = {};
ThisIndex.Generation = GenIndex;
ThisIndex.Index = j;
if(MainAcc && GenIndex < MainAcc->Count) { ThisIndex.Index += MainAcc->EntriesInGeneration[GenIndex]; }
if(PrevAcc && GenIndex < PrevAcc->Count) { ThisIndex.Index += PrevAcc->EntriesInGeneration[GenIndex]; }
landmark_range LandmarkRange = DetermineProjectLandmarksRange(A, ThisIndex);
if(!Result && LandmarkRange.Length > 0)
{
Dest->First = LandmarkRange.First;
Result = LocateFirstLandmark(A) + LandmarkRange.First;
}
Dest->Length += LandmarkRange.Length;
}
return Result;
}
bool
ReorganiseProjectsInterior(project_generations *G, project *CChild, uint64_t ChildIndex, uint64_t ChildCount, db_header_project *SChild)
{
bool Result = FALSE;
project_generations LocalAcc = InitAccumulator(G);
AccumulateProjectIndices(&LocalAcc, SChild);
db_header_project *This = SkipProjectAndChildren(SChild);
for(++ChildIndex; ChildIndex < ChildCount; ++ChildIndex)
{
if(ConfiguredAndStoredProjectIDsMatch(CChild, This))
{
project_generations ThisAcc = InitAccumulator(G);
AccumulateProjectIndices(&ThisAcc, This);
DB.Metadata.File.Handle = fopen(DB.Metadata.File.Path, "w");
uint64_t Byte = 0;
WriteFromByteToPointer(&DB.Metadata.File, &Byte, SChild);
char *Next = SkipProjectAndChildren(This);
db_block_assets *AssetsBlock = LocateBlock(B_ASET);
db_asset *Asset = LocateFirstAsset(AssetsBlock);
WriteFromPointerToPointer(&DB.Metadata.File, This, Next, &Byte);
WriteFromPointerToPointer(&DB.Metadata.File, SChild, This, &Byte);
WriteFromPointerToPointer(&DB.Metadata.File, Next, Asset, &Byte);
// TODO(matt): Don't do this. Instead, do the actual assets loop, fixing up the landmarks
//WriteFromByteToEnd(&DB.Metadata.File, &Byte);
for(int AssetIndex = 0; AssetIndex < AssetsBlock->Count; ++AssetIndex)
{
uint64_t RunningLandmarkIndex = 0;
db_landmark *FirstLandmark = LocateFirstLandmark(Asset);
WriteFromByteToPointer(&DB.Metadata.File, &Byte, FirstLandmark);
for(uint64_t GenIndex = 0; GenIndex < ThisAcc.Count; ++GenIndex)
{
landmark_range ThisRange = {};
db_landmark *ThisLandmark = DetermineFirstLandmarkAndRangeOfProjectHierarchy(Asset, G, &LocalAcc, &ThisAcc, GenIndex, &ThisRange);
landmark_range LocalAccRange = {};
db_landmark *LocalAccLandmark = DetermineFirstLandmarkAndRangeOfProjectHierarchy(Asset, G, 0, &LocalAcc, GenIndex, &LocalAccRange);
if(LocalAccLandmark)
{
WriteFromByteToPointer(&DB.Metadata.File, &Byte, LocalAccLandmark);
RunningLandmarkIndex = LocalAccRange.First;
}
for(int ThisLandmarkIndex = 0; ThisLandmarkIndex < ThisRange.Length;
++ThisLandmarkIndex, ++RunningLandmarkIndex, ++ThisLandmark)
{
db_landmark NewLandmark = *ThisLandmark;
if(GenIndex < LocalAcc.Count)
{
NewLandmark.Project.Index -= LocalAcc.EntriesInGeneration[GenIndex];
}
fwrite(&NewLandmark, sizeof(db_landmark), 1, DB.Metadata.File.Handle);
Byte += sizeof(db_landmark);
}
for(int LocalAccLandmarkIndex = 0; LocalAccLandmarkIndex < LocalAccRange.Length;
++LocalAccLandmarkIndex, ++RunningLandmarkIndex, ++LocalAccLandmark)
{
db_landmark NewLandmark = *LocalAccLandmark;
NewLandmark.Project.Index += ThisAcc.EntriesInGeneration[GenIndex];
fwrite(&NewLandmark, sizeof(db_landmark), 1, DB.Metadata.File.Handle);
Byte += sizeof(db_landmark);
}
db_landmark *TailLandmark = FirstLandmark + RunningLandmarkIndex;
for(; RunningLandmarkIndex < Asset->LandmarkCount && TailLandmark->Project.Generation == GenIndex;
++RunningLandmarkIndex, ++TailLandmark)
{
fwrite(TailLandmark, sizeof(db_landmark), 1, DB.Metadata.File.Handle);
Byte += sizeof(db_landmark);
}
}
db_landmark *Landmark = FirstLandmark + RunningLandmarkIndex;
fwrite(Landmark, sizeof(db_landmark), Asset->LandmarkCount - RunningLandmarkIndex, DB.Metadata.File.Handle);
Byte += sizeof(db_landmark) * (Asset->LandmarkCount - RunningLandmarkIndex);
// For each generation
// Write up to the start of the LocalAcc block
// Write out the landmarks for This, --ing their Project.Index by the LocalAcc[Gen].EntriesInGeneration
// Write landmarks for projects in LocalAcc, ++ing their Project.Index by ThisAcc[Gen].EntriesInGeneration
// Write out the remaining landmarks for this generation
Asset = SkipAsset(Asset);
}
CycleSignpostedFile(&DB.Metadata);
Result = TRUE;
break;
}
else
{
AccumulateProjectIndices(&LocalAcc, This);
}
This = SkipProjectAndChildren(This);
}
return Result;
}
bool
ReorganiseProjects(project_generations *G, project *CChild, db_header_project **SParent, uint64_t ChildIndex, db_header_project **SChild)
{
uint64_t SParentPos = (char *)*SParent - DB.Metadata.File.Buffer.Location;
uint64_t SChildPos = (char *)*SChild - DB.Metadata.File.Buffer.Location;
bool Result = ReorganiseProjectsInterior(G, CChild, ChildIndex, (*SParent)->ChildCount, *SChild);
if(Result == TRUE)
{
*SParent = (db_header_project *)(DB.Metadata.File.Buffer.Location + SParentPos);
*SChild = (db_header_project *)(DB.Metadata.File.Buffer.Location + SChildPos);
AddEntryToGeneration(G, CChild);
SyncProjects(G, CChild, SParent, SChild);
}
return Result;
}
bool
ReorganiseProjects_TopLevel(project_generations *G, project *CChild, db_block_projects **SParent, uint64_t ChildIndex, db_header_project **SChild)
{
uint64_t SParentPos = (char *)*SParent - DB.Metadata.File.Buffer.Location;
uint64_t SChildPos = (char *)*SChild - DB.Metadata.File.Buffer.Location;
bool Result = ReorganiseProjectsInterior(G, CChild, ChildIndex, (*SParent)->Count, *SChild);
if(Result == TRUE)
{
*SParent = (db_block_projects *)(DB.Metadata.File.Buffer.Location + SParentPos);
*SChild = (db_header_project *)(DB.Metadata.File.Buffer.Location + SChildPos);
AddEntryToGeneration(G, CChild);
SyncProjects_TopLevel(G, CChild, SParent, SChild);
}
return Result;
}
void
SyncProjects_TopLevel(project_generations *G, project *C, db_block_projects **SParent, db_header_project **SChild)
{
uint64_t SChildPos = (char *)*SChild - DB.Metadata.File.Buffer.Location;
uint64_t SParentPos = (char *)*SParent - DB.Metadata.File.Buffer.Location;
IncrementCurrentGeneration(G);
db_header_project *SGrandChild = LocateFirstChildProject(*SChild);
for(int ci = 0; ci < C->ChildCount; ++ci)
{
bool Located = FALSE;
project *CChild = &C->Child[ci];
if(ci < (*SChild)->ChildCount)
{
if(ConfiguredAndStoredProjectIDsMatch(CChild, SGrandChild))
{
Located = TRUE;
AddEntryToGeneration(G, CChild);
SyncProjects(G, CChild, SChild, &SGrandChild);
}
else
{
Located = ReorganiseProjects(G, CChild, SChild, ci, &SGrandChild);
}
}
if(!Located)
{
InsertProjectIntoDB(G, SChild, &SGrandChild, CChild);
}
SGrandChild = SkipProjectAndChildren(SGrandChild);
}
uint64_t DeletionCount = (*SChild)->ChildCount - C->ChildCount;
for(int DeletionIndex = 0; DeletionIndex < DeletionCount; ++DeletionIndex)
{
DeleteProject(SChild, &SGrandChild, G);
}
if((*SChild)->ChildCount == 0 && (*SChild)->EntryCount == 0)
{
DeleteSearchPageFromFilesystem(*SChild);
DeleteLandmarksForSearch(C->Index);
DeleteStaleAssets();
}
DecrementCurrentGeneration(G);
*SParent = (db_block_projects *)(DB.Metadata.File.Buffer.Location + SParentPos);
*SChild = (db_header_project *)(DB.Metadata.File.Buffer.Location + SChildPos);
}
void
SyncProjects(project_generations *G, project *C, db_header_project **SParent, db_header_project **SChild)
{
uint64_t SChildPos = (char *)*SChild - DB.Metadata.File.Buffer.Location;
uint64_t SParentPos = (char *)*SParent - DB.Metadata.File.Buffer.Location;
IncrementCurrentGeneration(G);
db_header_project *SGrandChild = LocateFirstChildProject(*SChild);
for(int ci = 0; ci < C->ChildCount; ++ci)
{
bool Located = FALSE;
project *CChild = &C->Child[ci];
if(ci < (*SChild)->ChildCount)
{
if(ConfiguredAndStoredProjectIDsMatch(CChild, SGrandChild))
{
Located = TRUE;
AddEntryToGeneration(G, CChild);
SyncProjects(G, CChild, SChild, &SGrandChild);
}
else
{
Located = ReorganiseProjects(G, CChild, SChild, ci, &SGrandChild);
}
}
if(!Located)
{
InsertProjectIntoDB(G, SChild, &SGrandChild, CChild);
}
SGrandChild = SkipProjectAndChildren(SGrandChild);
}
uint64_t DeletionCount = (*SChild)->ChildCount - C->ChildCount;
for(int DeletionIndex = 0; DeletionIndex < DeletionCount; ++DeletionIndex)
{
DeleteProject(SChild, &SGrandChild, G);
}
if((*SChild)->ChildCount == 0 && (*SChild)->EntryCount == 0)
{
DeleteSearchPageFromFilesystem(*SChild);
DeleteLandmarksForSearch(C->Index);
}
DecrementCurrentGeneration(G);
*SParent = (db_header_project *)(DB.Metadata.File.Buffer.Location + SParentPos);
*SChild = (db_header_project *)(DB.Metadata.File.Buffer.Location + SChildPos);
}
void
SyncDB(config *C)
{
// TODO(matt): Ensure that the project hierarchy in the db and config are in alignment
// Update the Project.Index in db_landmark if projects have been reorganised
// Do the stuff that SyncDBWithInput() does
//
// After having done all this, I think we should just be monitoring the filesystem for changed files and in a
// position to figure out how to set the CurrentProject
//
DB.Metadata.Signposts.ProjectsBlock.Ptr = LocateBlock(B_PROJ);
project_generations Accumulator = {};
db_block_projects *SParent = DB.Metadata.Signposts.ProjectsBlock.Ptr;
db_header_project *SChild = LocateFirstChildProjectOfBlock(SParent);
for(uint64_t ci = 0; ci < C->ProjectCount; ++ci)
{
bool Located = FALSE;
project *CChild = &C->Project[ci];
if(ci < SParent->Count)
{
if(ConfiguredAndStoredProjectIDsMatch(CChild, SChild))
{
Located = TRUE;
AddEntryToGeneration(&Accumulator, CChild);
SyncProjects_TopLevel(&Accumulator, CChild, &SParent, &SChild);
}
else
{
Located = ReorganiseProjects_TopLevel(&Accumulator, CChild, &SParent, ci, &SChild);
}
}
if(!Located)
{
InsertProjectIntoDB_TopLevel(&Accumulator, &SParent, &SChild, CChild);
}
SChild = SkipProjectAndChildren(SChild);
}
uint64_t DeletionCount = SParent->Count - C->ProjectCount;
for(int DeletionIndex = 0; DeletionIndex < DeletionCount; ++DeletionIndex)
{
DeleteProject_TopLevel(&SParent, &SChild, &Accumulator);
}
DeleteStaleAssets();
//PrintAssetsBlock();
//PrintConfig(C);
PrintGenerations(&Accumulator, FALSE);
FreeGenerations(&Accumulator);
//_exit(0);
}
void
SyncGlobalPagesWithInput(neighbourhood *N, buffers *CollationBuffers)
{
db_block_projects *ProjectsBlock = DB.Metadata.Signposts.ProjectsBlock.Ptr;
string StoredGlobalSearchDir = Wrap0i(ProjectsBlock->GlobalSearchDir, sizeof(ProjectsBlock->GlobalSearchDir));
string StoredGlobalSearchURL = Wrap0i(ProjectsBlock->GlobalSearchURL, sizeof(ProjectsBlock->GlobalSearchURL));
char *StoredGlobalSearchDir0 = MakeString0("l", &StoredGlobalSearchDir);
if(StringsDiffer(StoredGlobalSearchDir, Config->GlobalSearchDir))
{
ClearCopyStringNoFormat(ProjectsBlock->GlobalSearchDir, sizeof(ProjectsBlock->GlobalSearchDir), Config->GlobalSearchDir);
WriteEntireDatabase();
}
if(StringsDiffer(StoredGlobalSearchURL, Config->GlobalSearchURL))
{
ClearCopyStringNoFormat(ProjectsBlock->GlobalSearchURL, sizeof(ProjectsBlock->GlobalSearchURL), Config->GlobalSearchURL);
WriteEntireDatabase();
}
if(!ProjectsBlock->GlobalSearchDir[0])
{
DeleteGlobalSearchPageFromFilesystem(StoredGlobalSearchDir0);
DeleteLandmarksForSearch(GLOBAL_SEARCH_PAGE_INDEX);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
}
else if(StringLength(StoredGlobalSearchDir0) > 0 && StringsDifferLv0(Config->GlobalSearchDir, StoredGlobalSearchDir0))
{
// TODO(matt): Properly stress test this relocation code!
MakeDir(Config->GlobalSearchDir);
string StoredGlobalSearchDirL = Wrap0(StoredGlobalSearchDir0);
char *OldPath = ConstructHTMLIndexFilePath(0, &StoredGlobalSearchDirL, 0);
char *NewPath = ConstructHTMLIndexFilePath(0, &Config->GlobalSearchDir, 0);
rename(OldPath, NewPath);
Free(OldPath);
Free(NewPath);
}
if(StringLength(StoredGlobalSearchDir0))
{
GenerateGlobalSearchPage(N, CollationBuffers);
}
// TODO(matt): Come back to this!
Free(StoredGlobalSearchDir0);
}
void
SetCacheDirectory(config *DefaultConfig)
{
// TODO(matt): This might not have to exist because we should have a default cache directory already set from the config
int Flags = WRDE_NOCMD | WRDE_UNDEF | WRDE_APPEND;
wordexp_t Expansions = {};
wordexp("$XDG_CACHE_HOME/cinera", &Expansions, Flags);
wordexp("$HOME/.cache/cinera", &Expansions, Flags);
if(Expansions.we_wordc > 0 )
{
#if AFE
CopyString(DefaultConfig->CacheDir, sizeof(DefaultConfig->CacheDir), Expansions.we_wordv[0]);
#endif
}
wordfree(&Expansions);
}
void
InitMemoryArena(arena *Arena, int Size)
{
Arena->Size = Size;
if(!(Arena->Location = calloc(1, Arena->Size)))
{
_exit(RC_ERROR_MEMORY);
}
Arena->Ptr = Arena->Location;
}
bool
ProjectIndexIs(db_project_index I, uint64_t Generation, uint64_t Index)
{
return(I.Generation == Generation && I.Index == Index);
}
void
GenerateGlobalNavigationRecursively(config *C, project *P, uint32_t *IndentationLevel)
{
OpenNodeCNewLine(&C->NavGeneric, IndentationLevel, NODE_LI, 0);
OpenNode(&C->NavGeneric, IndentationLevel, NODE_A, 0);
AppendStringToBuffer(&C->NavGeneric, Wrap0(" href=\""));
buffer URL;
ClaimBuffer(&URL, BID_URL_SEARCH, MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1);
ConstructSearchURL(&URL, P);
AppendBuffer(&C->NavGeneric, &URL);
DeclaimBuffer(&URL);
AppendStringToBuffer(&C->NavGeneric, Wrap0("\">"));
AppendStringToBuffer(&C->NavGeneric, P->HTMLTitle.Length ? P->HTMLTitle: P->Title);
CloseNode(&C->NavGeneric, IndentationLevel, NODE_A);
if(P->ChildCount > 0)
{
OpenNodeCNewLine(&C->NavGeneric, IndentationLevel, NODE_UL, 0);
}
for(int i = 0; i < P->ChildCount; ++i)
{
project *Child = &P->Child[i];
GenerateGlobalNavigationRecursively(C, Child, IndentationLevel);
}
if(P->ChildCount > 0)
{
CloseNodeNewLine(&C->NavGeneric, IndentationLevel, NODE_UL);
CloseNodeNewLine(&C->NavGeneric, IndentationLevel, NODE_LI);
}
else
{
CloseNode(&C->NavGeneric, IndentationLevel, NODE_LI);
}
}
void
GenerateGlobalNavigationBars(config *C)
{
// NavGeneric;
// NavDropdownPre;
// NavDropdownPost;
// NavHorizontalPre;
// NavPlainPre;
uint32_t IndentationLevel = 0;
OpenNode(&C->NavDropdownPre, &IndentationLevel, NODE_NAV, 0);
AppendStringToBuffer(&C->NavDropdownPre, Wrap0(" class=\"cineraNavDropdown "));
AppendStringToBuffer(&C->NavDropdownPre, C->GlobalTheme);
AppendStringToBuffer(&C->NavDropdownPre, Wrap0("\">"));
OpenNodeNewLine(&C->NavDropdownPre, &IndentationLevel, NODE_NAV, 0);
AppendStringToBuffer(&C->NavDropdownPre, Wrap0(" class=\"cineraNavTitle\">"));
AppendStringToBuffer(&C->NavDropdownPre, Wrap0("Indexed Episode Guides"));
CloseNode(&C->NavDropdownPre, &IndentationLevel, NODE_NAV);
OpenNodeNewLine(&C->NavDropdownPre, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(&C->NavDropdownPre, Wrap0(" class=\"cineraPositioner\">"));
CloseNodeNewLine(&C->NavDropdownPost, &IndentationLevel, NODE_DIV);
CloseNodeNewLine(&C->NavDropdownPost, &IndentationLevel, NODE_NAV);
IndentationLevel = 0;
OpenNode(&C->NavHorizontalPre, &IndentationLevel, NODE_UL, 0);
AppendStringToBuffer(&C->NavHorizontalPre, Wrap0(" class=\"cineraNavHorizontal "));
AppendStringToBuffer(&C->NavHorizontalPre, C->GlobalTheme);
AppendStringToBuffer(&C->NavHorizontalPre, Wrap0("\">"));
IndentationLevel = 0;
OpenNode(&C->NavPlainPre, &IndentationLevel, NODE_UL, 0);
AppendStringToBuffer(&C->NavPlainPre, Wrap0(" class=\"cineraNavPlain "));
AppendStringToBuffer(&C->NavPlainPre, C->GlobalTheme);
AppendStringToBuffer(&C->NavPlainPre, Wrap0("\">"));
IndentationLevel = 1;
for(int i = 0; i < C->ProjectCount; ++i)
{
project *P = &C->Project[i];
GenerateGlobalNavigationRecursively(C, P, &IndentationLevel);
}
CloseNodeNewLine(&C->NavGeneric, &IndentationLevel, NODE_UL);
}
void
GenerateNavigationRecursively(project *TargetP, project *P, uint32_t *IndentationLevel)
{
if(TargetP == P)
{
OpenNodeNewLine(&TargetP->NavGeneric, IndentationLevel, NODE_LI, 0);
AppendStringToBuffer(&TargetP->NavGeneric, Wrap0(" class=\"current\">"));
}
else
{
OpenNodeCNewLine(&TargetP->NavGeneric, IndentationLevel, NODE_LI, 0);
}
OpenNode(&TargetP->NavGeneric, IndentationLevel, NODE_A, 0);
AppendStringToBuffer(&TargetP->NavGeneric, Wrap0(" href=\""));
buffer URL;
ClaimBuffer(&URL, BID_URL_SEARCH, MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1);
ConstructSearchURL(&URL, P);
AppendBuffer(&TargetP->NavGeneric, &URL);
DeclaimBuffer(&URL);
AppendStringToBuffer(&TargetP->NavGeneric, Wrap0("\">"));
AppendStringToBuffer(&TargetP->NavGeneric, P->HTMLTitle.Length ? P->HTMLTitle : P->Title);
CloseNode(&TargetP->NavGeneric, IndentationLevel, NODE_A);
if(P->ChildCount > 0)
{
OpenNodeCNewLine(&TargetP->NavGeneric, IndentationLevel, NODE_UL, 0);
}
for(int i = 0; i < P->ChildCount; ++i)
{
project *Child = &P->Child[i];
GenerateNavigationRecursively(TargetP, Child, IndentationLevel);
}
if(P->ChildCount > 0)
{
CloseNodeNewLine(&TargetP->NavGeneric, IndentationLevel, NODE_UL);
CloseNodeNewLine(&TargetP->NavGeneric, IndentationLevel, NODE_LI);
}
else
{
CloseNode(&TargetP->NavGeneric, IndentationLevel, NODE_LI);
}
}
void
GenerateNavigationBars(project *P)
{
// NavGeneric;
// NavDropdownPre;
// NavDropdownPost;
// NavHorizontalPre;
// NavPlainPre;
uint32_t IndentationLevel = 0;
OpenNode(&P->NavDropdownPre, &IndentationLevel, NODE_NAV, 0);
AppendStringToBuffer(&P->NavDropdownPre, Wrap0(" class=\"cineraNavDropdown "));
AppendStringToBuffer(&P->NavDropdownPre, P->Theme);
AppendStringToBuffer(&P->NavDropdownPre, Wrap0("\">"));
OpenNodeNewLine(&P->NavDropdownPre, &IndentationLevel, NODE_NAV, 0);
AppendStringToBuffer(&P->NavDropdownPre, Wrap0(" class=\"cineraNavTitle\">"));
AppendStringToBuffer(&P->NavDropdownPre, P->HTMLTitle.Length ? P->HTMLTitle : P->Title);
CloseNode(&P->NavDropdownPre, &IndentationLevel, NODE_NAV);
OpenNodeNewLine(&P->NavDropdownPre, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(&P->NavDropdownPre, Wrap0(" class=\"cineraPositioner\">"));
CloseNodeNewLine(&P->NavDropdownPost, &IndentationLevel, NODE_DIV);
CloseNodeNewLine(&P->NavDropdownPost, &IndentationLevel, NODE_NAV);
IndentationLevel = 0;
OpenNode(&P->NavHorizontalPre, &IndentationLevel, NODE_UL, 0);
AppendStringToBuffer(&P->NavHorizontalPre, Wrap0(" class=\"cineraNavHorizontal "));
AppendStringToBuffer(&P->NavHorizontalPre, P->Theme);
AppendStringToBuffer(&P->NavHorizontalPre, Wrap0("\">"));
IndentationLevel = 0;
OpenNode(&P->NavPlainPre, &IndentationLevel, NODE_UL, 0);
AppendStringToBuffer(&P->NavPlainPre, Wrap0(" class=\"cineraNavPlain "));
AppendStringToBuffer(&P->NavPlainPre, P->Theme);
AppendStringToBuffer(&P->NavPlainPre, Wrap0("\">"));
IndentationLevel = 1;
project *Parent = P;
while(Parent->Parent)
{
Parent = Parent->Parent;
}
// TODO(matt): Should this be calling GenerateNavigationRecursively() on the Parent?
for(int i = 0; i < Parent->ChildCount; ++i)
{
project *Child = &Parent->Child[i];
GenerateNavigationRecursively(P, Child, &IndentationLevel);
}
CloseNodeNewLine(&P->NavGeneric, &IndentationLevel, NODE_UL);
}
void
SyncProject(project *P, neighbourhood *N, buffers *CollationBuffers, template *BespokeTemplate)
{
for(int i = 0; i < P->ChildCount; ++i)
{
SyncProject(&P->Child[i], N, CollationBuffers, BespokeTemplate);
}
SetCurrentProject(P, N);
fprintf(stderr, "\n"
"┌─ ");
PrintLineage(P->Lineage, FALSE);
fprintf(stderr, " ─╼");
bool Titled = FALSE;
bool TitledTemplates = FALSE;
// TODO(matt): We need to figure out a way to stay awake beyond the detection of an invalid template
if(CurrentProject->PlayerTemplatePath.Length > 0)
{
if(!TitledTemplates)
{
fprintf(stderr, "\n"
"%s─── Packing templates ───╼\n", Titled ? "" : "");
TitledTemplates = TRUE;
Titled = TRUE;
}
switch(PackTemplate(&CurrentProject->PlayerTemplate, CurrentProject->PlayerTemplatePath, TEMPLATE_PLAYER))
{
case RC_ERROR_FILE: // Could not load template
case RC_ERROR_MEMORY: // Could not allocate memory for template
// TODO(matt): Urgently decide how we handle the case in which we've been given an invalid template
free(MemoryArena.Location);
_exit(0);
case RC_INVALID_TEMPLATE: // Invalid template
case RC_SUCCESS:
break;
}
}
// TODO(matt): We need to figure out a way to stay awake beyond the detection of an invalid template
if(CurrentProject->SearchTemplatePath.Length > 0)
{
if(!TitledTemplates)
{
fprintf(stderr, "\n"
"%s─── Packing templates ───╼\n", Titled ? "" : "");
TitledTemplates = TRUE;
Titled = TRUE;
}
switch(PackTemplate(&CurrentProject->SearchTemplate, CurrentProject->SearchTemplatePath, TEMPLATE_SEARCH))
{
case RC_ERROR_MEMORY: // Could not allocate memory for template
case RC_ERROR_FILE: // Could not load template
// TODO(matt): Urgently decide how we handle the case in which we've been given an invalid template
Free(MemoryArena.Location);
_exit(0);
case RC_INVALID_TEMPLATE: // Invalid template
case RC_SUCCESS:
break;
}
}
GenerateNavigationBars(P);
fprintf(stderr, "\n"
"%s─── Synchronising with Input Directories ───╼\n", Titled ? "" : "");
SyncDBWithInput(N, CollationBuffers, BespokeTemplate);
PushWatchHandle(P->HMMLDir, EXT_HMML, WT_HMML, P, 0);
}
#define DEBUG_EVENTS 0
#if DEBUG_EVENTS
void
PrintEvent(struct inotify_event *Event, int EventIndex)
{
printf("\nEvent[%d]\n"
" wd: %d\n"
" mask: %d\n",
EventIndex,
Event->wd,
Event->mask);
if(Event->mask & IN_ACCESS) { printf(" IN_ACCESS\n"); }
if(Event->mask & IN_ATTRIB) { printf(" IN_ATTRIB\n"); }
if(Event->mask & IN_CLOSE_WRITE) { printf(" IN_CLOSE_WRITE\n"); }
if(Event->mask & IN_CLOSE_NOWRITE) { printf(" IN_CLOSE_NOWRITE\n"); }
if(Event->mask & IN_CREATE) { printf(" IN_CREATE\n"); }
if(Event->mask & IN_DELETE) { printf(" IN_DELETE\n"); }
if(Event->mask & IN_DELETE_SELF) { printf(" IN_DELETE_SELF\n"); }
if(Event->mask & IN_MODIFY) { printf(" IN_MODIFY\n"); }
if(Event->mask & IN_MOVE_SELF) { printf(" IN_MOVE_SELF\n"); }
if(Event->mask & IN_MOVED_FROM) { printf(" IN_MOVED_FROM\n"); }
if(Event->mask & IN_MOVED_TO) { printf(" IN_MOVED_TO\n"); }
if(Event->mask & IN_OPEN) { printf(" IN_OPEN\n"); }
if(Event->mask & IN_MOVE) { printf(" IN_MOVE\n"); }
if(Event->mask & IN_CLOSE) { printf(" IN_CLOSE\n"); }
if(Event->mask & IN_DONT_FOLLOW) { printf(" IN_DONT_FOLLOW\n"); }
if(Event->mask & IN_EXCL_UNLINK) { printf(" IN_EXCL_UNLINK\n"); }
if(Event->mask & IN_MASK_ADD) { printf(" IN_MASK_ADD\n"); }
if(Event->mask & IN_ONESHOT) { printf(" IN_ONESHOT\n"); }
if(Event->mask & IN_ONLYDIR) { printf(" IN_ONLYDIR\n"); }
if(Event->mask & IN_IGNORED) { printf(" IN_IGNORED\n"); }
if(Event->mask & IN_ISDIR) { printf(" IN_ISDIR\n"); }
if(Event->mask & IN_Q_OVERFLOW) { printf(" IN_Q_OVERFLOW\n"); }
if(Event->mask & IN_UNMOUNT) { printf(" IN_UNMOUNT\n"); }
printf( " cookie: %d\n"
" len: %d\n"
" name: %s\n",
Event->cookie,
Event->len,
Event->name);
}
#endif
void
InitAll(neighbourhood *Neighbourhood, buffers *CollationBuffers, template *BespokeTemplate)
{
RewindCollationBuffers(CollationBuffers);
InitDB();
// TODO(matt): Straight up remove these PrintAssetsBlock() calls
//PrintAssetsBlock(0);
SyncDB(Config);
//PrintAssetsBlock(0);
printf("\n╾─ Hashing assets ─╼\n");
// NOTE(matt): This had to happen before PackTemplate() because those guys may need to do PushAsset() and we must
// ensure that the builtin assets get placed correctly
InitAssets();
PushConfiguredAssets();
//
////
if(Config->GlobalSearchTemplatePath.Length > 0)
{
fprintf(stderr, "\n");
switch(PackTemplate(&Config->SearchTemplate, Config->GlobalSearchTemplatePath, TEMPLATE_GLOBAL_SEARCH))
{
case RC_ERROR_MEMORY: // Could not allocate memory for template
case RC_ERROR_FILE: // Could not load template
// TODO(matt): Urgently decide how we handle the case in which we've been given an invalid template
free(MemoryArena.Location);
_exit(0);
case RC_INVALID_TEMPLATE: // Invalid template
case RC_SUCCESS:
break;
}
}
GenerateGlobalNavigationBars(Config);
for(int i = 0; i < Config->ProjectCount; ++i)
{
SyncProject(&Config->Project[i], Neighbourhood, CollationBuffers, BespokeTemplate);
}
SyncGlobalPagesWithInput(Neighbourhood, CollationBuffers);
for(int i = 0; i < Assets.Count; ++i)
{
asset *This = GetPlaceInBook(&Assets.Asset, i);
UpdateAssetInDB(This);
}
DeleteStaleAssets();
//PrintAssetsBlock(0);
printf("\n╾─ Monitoring file system for %snew%s, %sedited%s and %sdeleted%s .hmml and asset files ─╼\n",
ColourStrings[EditTypes[EDIT_ADDITION].Colour], ColourStrings[CS_END],
ColourStrings[EditTypes[EDIT_REINSERTION].Colour], ColourStrings[CS_END],
ColourStrings[EditTypes[EDIT_DELETION].Colour], ColourStrings[CS_END]);
}
void
RemoveAndFreeWatchHandles(watch_handles *W)
{
for(int i = 0; i < W->Count; ++i)
{
watch_handle *This = &W->Handles[i];
inotify_rm_watch(inotifyInstance, This->Descriptor);
FreeAndResetCount(This->Files, This->FileCount);
}
FreeAndResetCountAndCapacity(W->Handles, W->Count, W->Capacity);
FreeAndReinitialiseBook(&W->Paths);
}
void
DiscardAllAndFreeConfig(void)
{
FreeSignpostedFile(&DB.Metadata); // NOTE(matt): This seems fine
FreeFile(&DB.File); // NOTE(matt): This seems fine
FreeAssets(&Assets); // NOTE(matt): This seems fine
RemoveAndFreeWatchHandles(&WatchHandles);
CurrentProject = 0; // NOTE(matt): This is fine
FreeConfig(Config); // NOTE(matt): This is fine
Config = 0;
}
watch_file *
GetWatchFileForEvent(struct inotify_event *Event)
{
watch_file *Result = 0;
bool Update = FALSE;
for(int HandleIndex = 0; HandleIndex < WatchHandles.Count; ++HandleIndex)
{
watch_handle *ThisWatch = WatchHandles.Handles + HandleIndex;
if(Event->wd == ThisWatch->Descriptor)
{
if(Event->mask & IN_DELETE_SELF || Event->mask == (IN_CREATE | IN_ISDIR))
{
Update = TRUE;
}
if(StringsMatch(ThisWatch->TargetPath, ThisWatch->WatchedPath))
{
for(int FileIndex = 0; FileIndex < ThisWatch->FileCount; ++FileIndex)
{
watch_file *ThisFile = ThisWatch->Files + FileIndex;
if(ThisFile->Extension != EXT_NULL)
{
if(ExtensionMatches(Wrap0(Event->name), ThisFile->Extension))
{
Result = ThisFile;
break;
}
}
else if(StringsMatch(Wrap0(Event->name), ThisFile->Path))
{
Result = ThisFile;
break;
}
}
if(Result)
{
break;
}
}
}
}
if(Update)
{
UpdateWatchHandles(Event->wd);
}
return Result;
}
void
ParseAndEitherPrintConfigOrInitAll(string ConfigPath, tokens_list *TokensList, neighbourhood *N, buffers *CollationBuffers, template *BespokeTemplate)
{
Config = ParseConfig(ConfigPath, TokensList);
if(Config)
{
if(Mode & MODE_DRYRUN)
{
PrintConfig(Config, TRUE);
}
else
{
InitAll(N, CollationBuffers, BespokeTemplate);
}
}
}
int
MonitorFilesystem(neighbourhood *N, buffers *CollationBuffers, template *BespokeTemplate, string ConfigPath, tokens_list *TokensList)
{
buffer Events = {};
if(ClaimBuffer(&Events, BID_INOTIFY_EVENTS, Kilobytes(4)) == RC_ARENA_FULL) { return RC_ARENA_FULL; };
Clear(Events.Location, Events.Size);
struct inotify_event *Event;
int BytesRead = read(inotifyInstance, Events.Location, Events.Size); // TODO(matt): Handle error EINVAL
if(inotifyInstance < 0) { perror("MonitorFilesystem()"); }
// TODO(matt): Test this with longer update intervals, and combinations of events...
#if DEBUG_EVENTS
if(BytesRead > 0)
{
PrintWatchHandles();
}
int DebugEventIndex = 0;
#endif
bool Deleted = FALSE;
bool Inserted = FALSE;
#if DEBUG_LANDMARKS
bool UpdatedAsset = FALSE;
#endif
for(Events.Ptr = Events.Location;
Events.Ptr - Events.Location < BytesRead;
Events.Ptr += sizeof(struct inotify_event) + Event->len
#if DEBUG_EVENTS
, ++DebugEventIndex
#endif
)
{
Event = (struct inotify_event *)Events.Ptr;
#if DEBUG_EVENTS
PrintEvent(Event, DebugEventIndex);
//PrintWatchHandles();
#endif
watch_file *WatchFile = GetWatchFileForEvent(Event);
if(WatchFile)
{
char *PeekPtr = Events.Ptr + sizeof(struct inotify_event) + Event->len;
struct inotify_event *Peek = (struct inotify_event *)PeekPtr;
while(PeekPtr - Events.Location < BytesRead && Event->wd == Peek->wd && StringsMatch(Wrap0i(Event->name, Event->len), Wrap0i(Peek->name, Peek->len)))
{
#if DEBUG_EVENTS
fprintf(stderr, "Squashing events:\n"
" ");
PrintEvent(Event, DebugEventIndex++);
fprintf(stderr, "\n"
" ");
PrintEvent(Peek, DebugEventIndex);
fprintf(stderr, "\n");
#endif
Event = Peek;
Events.Ptr = PeekPtr;
PeekPtr = Events.Ptr + sizeof(struct inotify_event) + Event->len;
Peek = (struct inotify_event *)PeekPtr;
}
switch(WatchFile->Type)
{
// TODO(matt): We're probably watching for too many events when the target directory exists
case WT_HMML:
{
SetCurrentProject(WatchFile->Project, N);
//PrintLineage(CurrentProject->Lineage, TRUE);
string BaseFilename = GetBaseFilename(Wrap0(Event->name), WatchFile->Extension);
if(Event->mask & (IN_DELETE | IN_MOVED_FROM))
{
// TODO(matt): Why are we getting here after editing a .hmml file? We should just reinsert
Deleted |= (DeleteEntry(N, BaseFilename) == RC_SUCCESS);
}
else if(Event->mask & (IN_CLOSE_WRITE | IN_MOVED_TO))
{
Inserted |= (InsertEntry(N, CollationBuffers, BespokeTemplate, BaseFilename, 0) == RC_SUCCESS);
}
} break;
case WT_ASSET:
{
// TODO(matt): Why are we getting here after editing a .hmml file?
//PrintAssetsBlock(0);
UpdateAsset(WatchFile->Asset, FALSE);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
#if DEBUG_LANDMARKS
UpdatedAsset = TRUE;
#endif
} break;
case WT_CONFIG:
{
if(Config)
{
DiscardAllAndFreeConfig();
PushWatchHandle(ConfigPath, EXT_NULL, WT_CONFIG, 0, 0);
}
ParseAndEitherPrintConfigOrInitAll(ConfigPath, TokensList, N, CollationBuffers, BespokeTemplate);
} break;
}
}
}
if(Deleted || Inserted)
{
UpdateDeferredAssetChecksums();
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
GenerateSearchPages(N, CollationBuffers);
DeleteStaleAssets();
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
VerifyLandmarks(N);
}
#if DEBUG_LANDMARKS
if(UpdatedAsset)
{
VerifyLandmarks(N);
PrintAssetsBlock(0);
}
#endif
DeclaimBuffer(&Events);
return RC_SUCCESS;
}
void
Exit(void)
{
Free(MemoryArena.Location);
_exit(0);
}
void
InitWatchHandles(uint32_t DefaultEventsMask, uint64_t DesiredPageSize)
{
WatchHandles.DefaultEventsMask = DefaultEventsMask;
InitBook(&WatchHandles.Paths, 1, DesiredPageSize, MBT_STRING);
}
int
main(int ArgC, char **Args)
{
Assert(ArrayCount(BufferIDStrings) == BID_COUNT);
Assert(ArrayCount(TemplateTags) == TEMPLATE_TAG_COUNT);
char CommandLineArg;
char *ConfigPath = "$XDG_CONFIG_HOME/cinera/cinera.conf";
while((CommandLineArg = getopt(ArgC, Args, "0c:ehv")) != -1)
{
switch(CommandLineArg)
{
case '0':
Mode |= MODE_DRYRUN; break;
case 'c':
ConfigPath = optarg; break;
case 'e':
Mode |= MODE_EXAMINE; break;
case 'v':
PrintVersions();
_exit(RC_SUCCESS);
break;
case 'h':
default:
PrintHelp(Args[0], ConfigPath);
return RC_SUCCESS;
}
}
// NOTE(matt): Init MemoryArenas (they are global)
InitMemoryArena(&MemoryArena, Megabytes(4));
ConfigPath = ExpandPath(Wrap0(ConfigPath), 0);
PrintVersions();
#if DEBUG_MEM
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
fprintf(MemLog, " Allocated MemoryArena (%d)\n", MemoryArena.Size);
fclose(MemLog);
printf(" Allocated MemoryArena (%d)\n", MemoryArena.Size);
#endif
#if DEBUG
printf("Allocated MemoryArena: %d\n\n", MemoryArena.Size);
#endif
// NOTE(matt): Tree structure of buffer dependencies
// IncludesPlayer
// Menus
// Player
// ScriptPlayer
//
// IncludesSearch
// Search
buffers CollationBuffers = {};
if(ClaimBuffer(&CollationBuffers.IncludesPlayer, BID_COLLATION_BUFFERS_INCLUDES_PLAYER, Kilobytes(2)) == RC_ARENA_FULL) { Exit(); };
if(ClaimBuffer(&CollationBuffers.Player, BID_COLLATION_BUFFERS_PLAYER, Kilobytes(552)) == RC_ARENA_FULL) { Exit(); };
if(ClaimBuffer(&CollationBuffers.IncludesSearch, BID_COLLATION_BUFFERS_INCLUDES_SEARCH, Kilobytes(2)) == RC_ARENA_FULL) { Exit(); };
if(ClaimBuffer(&CollationBuffers.SearchEntry, BID_COLLATION_BUFFERS_SEARCH_ENTRY, Kilobytes(32)) == RC_ARENA_FULL) { Exit(); };
CollationBuffers.Search.ID = BID_COLLATION_BUFFERS_SEARCH; // NOTE(matt): Allocated by SearchToBuffer()
tokens_list TokensList = {};
template BespokeTemplate = {};
neighbourhood Neighbourhood = {};
InitWatchHandles((IN_CREATE | IN_MOVED_FROM | IN_MOVED_TO | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF), Kilobytes(4));
inotifyInstance = inotify_init1(IN_NONBLOCK);
string ConfigPathL = Wrap0(ConfigPath);
PushWatchHandle(ConfigPathL, EXT_NULL, WT_CONFIG, 0, 0);
Config = ParseConfig(ConfigPathL, &TokensList);
if(Config)
{
if(Mode & MODE_EXAMINE)
{
// TODO(matt): Allow optionally passing a .db file as an argument?
ExamineDB();
_exit(RC_SUCCESS);
}
if(Mode & MODE_DRYRUN)
{
PrintConfig(Config, FALSE);
}
else
{
InitAll(&Neighbourhood, &CollationBuffers, &BespokeTemplate);
}
}
else
{
fprintf(stderr, "\n"
"Print config help? (Y/n)\n");
if(getchar() != 'n')
{
if(ArgC < 2)
{
PrintHelp(Args[0], ConfigPath);
}
else
{
PrintHelpConfig();
}
}
}
//PrintWatchHandles();
while(MonitorFilesystem(&Neighbourhood, &CollationBuffers, &BespokeTemplate, ConfigPathL, &TokensList) != RC_ARENA_FULL)
{
// TODO(matt): Refetch the quotes and rebuild player pages if needed
//
// Every sixty mins, redownload the quotes and, I suppose, SyncDBWithInput(). But here we still don't even know
// who the speaker is. To know, we'll probably have to store all quoted speakers in the project's .metadata. Maybe
// postpone this for now, but we will certainly need this to happen
//
// The most ideal solution is possibly that we store quote numbers in the Metadata->Entry, listen for and handle a
// REST PUT request from insobot when a quote changes (unless we're supposed to poll insobot for them?), and rebuild
// the player page(s) accordingly.
//
if(!(Mode & MODE_DRYRUN) && Config && Config->RespectingPrivacy && time(0) - LastPrivacyCheck > Config->PrivacyCheckInterval)
{
RecheckPrivacy(&Neighbourhood, &CollationBuffers, &BespokeTemplate);
}
sleep(GLOBAL_UPDATE_INTERVAL);
}
DiscardAllAndFreeConfig();
RemoveAndFreeWatchHandles(&WatchHandles);
Exit();
}