Annotation-System/cinera/cinera.c

18413 lines
654 KiB
C
Raw Permalink 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
if [ $(command -v ctime 2>/dev/null) ]; then
ctime -begin ${0%.*}.ctm
fi
#gcc -g -fsanitize=address -Wall -std=c99 -pipe $0 -o ${0%.*} -lcurl
gcc -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} -lcurl
#clang -fsanitize=address -g -Wall -std=c99 -pipe $0 -o ${0%.*} -lcurl
#clang -O2 -Wall -std=c99 -pipe $0 -o ${0%.*} -lcurl
if [ $(command -v ctime 2>/dev/null) ]; then
ctime -end ${0%.*}.ctm
fi
exit
#endif
#include <stdint.h>
typedef struct
{
uint32_t Major, Minor, Patch;
} version;
version CINERA_APP_VERSION = {
.Major = 0,
.Minor = 10,
.Patch = 30
};
#define __USE_XOPEN2K8 // NOTE(matt): O_NOFOLLOW
#include <fcntl.h> // NOTE(matt): open()
#undef __USE_XOPEN2K8
#include <stdarg.h> // NOTE(matt): varargs
#define __USE_POSIX // NOTE(matt): fileno()
#include <stdio.h> // NOTE(matt): printf, sprintf, vsprintf, fprintf, perror
#undef __USE_POSIX
#include <stdlib.h> // NOTE(matt): calloc, malloc, free
#include <getopt.h> // NOTE(matt): getopts
#include <curl/curl.h>
#include <time.h>
#include <sys/file.h> // NOTE(matt): flock
#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_XOPEN2K // NOTE(matt): readlink()
#include <unistd.h> // NOTE(matt): sleep()
#undef __USE_XOPEN2K
#define __USE_POSIX
#include <signal.h> // NOTE(matt): sigaction() and sigemptyset()
#undef __USE_POSIX
#define HMMLIB_IMPLEMENTATION
#include "hmmlib.h"
#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 NA 0
#define SECONDS_PER_HOUR 3600
#define SECONDS_PER_MINUTE 60
#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_CLASH_RESOLVER 0
#define DEBUG 0
#define DEBUG_MEM 0
//
////
typedef struct
{
char *Base;
uint64_t Length;
} string;
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;
// TODO(matt): We probably want our memory_book to be able to support strings that span multiple pages
// SearchQuotes() especially could benefit from this
typedef struct
{
int64_t PageCount;
uint64_t PageSize;
uint64_t DataWidthInBytes;
uint64_t ItemCount;
memory_page *Pages;
memory_pen_location *Pen;
} memory_book;
#define _memory_book(type) memory_book
typedef struct
{
memory_book Message[2];
memory_book *DesiredMessage;
memory_book *LastMessage;
uint64_t RepetitionCount;
bool LastMessageMayBeDeduplicated;
} message_control;
#define SwapPtrs(A, B) void *Temp = A; A = B; B = Temp;
message_control MESSAGE_CONTROL;
#define Print(...) fprintf(__VA_ARGS__); MESSAGE_CONTROL.LastMessageMayBeDeduplicated = FALSE
#define WriteToFile(...) fprintf(__VA_ARGS__)
bool PROFILING = 0;
clock_t TIMING_START;
#define START_TIMING_BLOCK(...) if(PROFILING) { Print(stdout, __VA_ARGS__); TIMING_START = clock(); }
#define END_TIMING_BLOCK() if(PROFILING) { Print(stdout, "\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 256 // TODO(matt): This was 32, upped to 256 to fit full paths of "raw" videos.
// It hasn't mattered, but will do when the database contains everything.
#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_NUMBER_LENGTH 16
#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)){ Print(stderr, "l.%d: \e[1;31mAssertion failure\e[0m\n", __LINE__); __asm__("int3"); } } while(0)
#define BreakHere() Print(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 MIN(A, B) A < B ? A : B
#define MAX(A, B) A > B ? A : B
#define Clamp(EndA, N, EndB) int Min = MIN(EndA, EndB); int Max = MAX(EndA, EndB); if(N < Min) { N = Min; } else if(N > Max) { N = Max; }
typedef int32_t hash32;
typedef hash32 asset_hash;
typedef struct
{
union
{
int A;
int Hours;
};
union
{
int B;
int Minutes;
};
union
{
int C;
int Seconds;
};
union
{
int D;
int Milliseconds;
};
} v4;
void
Clear(void *V, uint64_t Size)
{
char *Ptr = (char *)V;
for(int i = 0; i < Size; ++i)
{
*Ptr++ = 0;
}
}
// 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) \
Print(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*/) { Print(stderr, "iter %2i: %i (%s%i)\n", i, MemLoopNew, MemLoopNew > MemLoopOld ? "+" : "-", MemLoopNew - MemLoopOld); sleep(1); }\
#define MEM_LOOP_POST(String) \
MemLoopOld = MemLoopNew;\
}\
Print(stderr, "Done (%s): ", String);\
Colourise(MemLoopNew > MemLoopInitial ? CS_RED : CS_GREEN);\
Print(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
int DebugMemoryIndentLevel = 0;
#define MEM_TEST_TOP() int MemTestInitialValue = GetUsage(); int MemTestRunningValue = MemTestInitialValue;\
/*\
Colourise(CS_BLACK_BOLD);\
Print(stderr, "[%5i]", __LINE__);\
Colourise(CS_GREEN);\
Print(stderr, " ");\
Indent(DebugMemoryIndentLevel);\
Print(stderr, "%s() starts at %i kb\n", __func__, MemTestInitialValue);\
Colourise(CS_END);\
*/\
++DebugMemoryIndentLevel;
#define MEM_TEST_MID() if(GetUsage() > MemTestRunningValue)\
{\
Colourise(CS_BLACK_BOLD);\
Print(stderr, "[%5i]", __LINE__);\
Colourise(CS_YELLOW);\
Print(stderr, " ");\
Indent(DebugMemoryIndentLevel - 1);\
Print(stderr, "%s() is now at %i kb (+%i)\n", __func__, GetUsage(), GetUsage() - MemTestInitialValue);\
Colourise(CS_END);\
MemTestRunningValue = GetUsage();\
WaitForInput();\
}
#define MEM_TEST_END() MemTestRunningValue = GetUsage();\
--DebugMemoryIndentLevel;\
if(MemTestRunningValue > MemTestInitialValue)\
{\
Colourise(CS_BLACK_BOLD);\
Print(stderr, "[%5i]", __LINE__);\
Colourise(CS_RED);\
Print(stderr, " ");\
Indent(DebugMemoryIndentLevel);\
Print(stderr, "%s() ends at %i kb (+%i)\n", __func__, MemTestRunningValue, MemTestRunningValue - MemTestInitialValue);\
Colourise(CS_END);\
WaitForInput();\
}
#else
#define MEM_TEST_TOP()
#define MEM_TEST_MID()
#define MEM_TEST_END()
#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_FILE_LOCKED,
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_INIT,
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_UNHANDLED_REF_COMBO,
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_NAVIGATION",
"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_NAVIGATION,
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
{
buffer Buffer;
char *Path;
FILE *Handle;
bool Locking;
} file;
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)
{
Print(stderr, "%s", ColourStrings[C]);
}
uint64_t
StringLength(char *String)
{
uint64_t i = 0;
if(String)
{
while(String[i])
{
++i;
}
}
return i;
}
// NOTE(matt): For use with 0-terminated strings
string
Wrap0(char *String)
{
string Result = {};
Result.Base = String;
Result.Length = StringLength(String);
return Result;
}
// NOTE(matt): For use with implicitly 0-terminated strings, i.e. fixed-size char arrays that may either be full of data,
// or filled after their data with 0s
#define Wrap0i(S) Wrap0i_(S, sizeof(S))
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;
}
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)
{
while(*A && *B && *A == *B)
{
++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
StringsMatchSized(string A, int Size, string B)
{
int i = 0;
while(i < A.Length && i < B.Length && A.Base[i] == B.Base[i]) { ++i; }
if(i == Size)
{
return TRUE;
}
return FALSE;
}
bool
StringContains(string S, string Substring)
{
bool Result = FALSE;
if(S.Length >= Substring.Length)
{
string Test = S;
for(int64_t i = 0; i <= S.Length - Substring.Length; ++i, ++Test.Base, --Test.Length)
{
if(StringsMatchSized(Test, Substring.Length, Substring))
{
Result = TRUE;
break;
}
}
}
return Result;
}
int
StringContainsXOfChar(string S, char C)
{
int Result = 0;
for(int i = 0; i < S.Length; ++i)
{
Result += S.Base[i] == C;
}
return Result;
}
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;
}
int
CopyBytes(char *Dest, char *Src, int Count)
{
for(int i = 0; i < Count; ++i)
{
Dest[i] = Src[i];
}
return Count;
}
int
CopyStringToBarePtr(char *Dest, string Src)
{
return CopyBytes(Dest, Src.Base, Src.Length);
}
int
CopyStringCToBarePtr(colour_code Colour, char *Dest, string Src)
{
int Length = 0;
Length += CopyStringToBarePtr(Dest + Length, Wrap0(ColourStrings[Colour]));
Length += CopyStringToBarePtr(Dest + Length, Src);
Length += CopyStringToBarePtr(Dest + Length, Wrap0(ColourStrings[CS_END]));
return Length;
}
memory_book
InitBook(uint64_t DataWidthInBytes, uint64_t ItemsPerPage)
{
memory_book Result = {};
Result.PageSize = ItemsPerPage * DataWidthInBytes;
Result.DataWidthInBytes = DataWidthInBytes;
return Result;
}
memory_book
InitBookOfPointers(uint64_t ItemsPerPage)
{
memory_book Result = {};
Result.PageSize = ItemsPerPage * sizeof(uintptr_t);
Result.DataWidthInBytes = sizeof(uintptr_t);
return Result;
}
memory_book
InitBookOfStrings(uint64_t PageSizeInBytes)
{
memory_book Result = {};
Result.PageSize = PageSizeInBytes;
Result.DataWidthInBytes = 1;
return Result;
}
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
FreePens(memory_book *M)
{
memory_pen_location *This = M->Pen;
while(This)
{
M->Pen = M->Pen->Prev;
Free(This);
This = M->Pen;
}
}
void
FreeBook(memory_book *M)
{
for(int i = 0; i < M->PageCount; ++i)
{
FreePage(&M->Pages[i]);
}
FreeAndResetCount(M->Pages, M->PageCount);
FreePens(M);
memory_book Zero = {};
*M = Zero;
}
void
FreeAndReinitialiseBook(memory_book *M)
{
uint64_t PageSize = M->PageSize;
uint64_t DataWidthInBytes = M->DataWidthInBytes;
FreeBook(M);
M->PageSize = PageSize;
M->DataWidthInBytes = DataWidthInBytes;
}
void
ResetBook(memory_book *M)
{
for(int i = 0; i < M->PageCount; ++i)
{
memory_page *This = &M->Pages[i];
This->Ptr = This->Base;
}
FreePens(M);
}
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;
}
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
ExtendStringCInBook(memory_book *M, colour_code Colour, string S)
{
ExtendStringInBook(M, Wrap0(ColourStrings[Colour]));
ExtendStringInBook(M, S);
ExtendStringInBook(M, Wrap0(ColourStrings[CS_END]));
}
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;
}
void *
MakeSpaceInBook(memory_book *M)
{
return GetPlaceInBook(M, M->ItemCount++);
}
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);
}
bool
PagesMatch(memory_page *A, memory_page *B)
{
return StringsMatch(Wrap0i_(A->Base, A->Ptr - A->Base), Wrap0i_(B->Base, B->Ptr - B->Base));
}
bool
BooksMatch(memory_book *A, memory_book *B)
{
bool Result = A->PageCount == B->PageCount;
int i = 0;
for(; Result == TRUE && i < A->PageCount; ++i)
{
if(!PagesMatch(&A->Pages[i], &B->Pages[i]))
{
Result = FALSE;
break;
}
}
if(Result == TRUE && i != A->PageCount)
{
Result = FALSE;
}
return Result;
}
void
PrintPage(memory_page *P)
{
Print(stderr, "%.*s", (int)(P->Ptr - P->Base), P->Base);
}
void
PrintBookOfStrings(memory_book *M)
{
for(int i = 0; i < M->PageCount; ++i)
{
PrintPage(&M->Pages[i]);
}
}
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;
}
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;
}
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';
}
}
file
InitFile(string *Directory, string *Filename, extension_id Extension, bool Locking)
{
file Result = {};
if(Directory)
{
ExtendString0(&Result.Path, *Directory);
ExtendString0(&Result.Path, Wrap0("/"));
}
ExtendString0(&Result.Path, *Filename);
if(Extension != EXT_NULL)
{
ExtendString0(&Result.Path, ExtensionStrings[Extension]);
}
Result.Locking = Locking;
return Result;
}
void
CloseFile(file *F, bool CloseLocking)
{
if(F->Handle)
{
fclose(F->Handle);
F->Handle = 0;
if(F->Locking && !CloseLocking)
{
F->Handle = fopen(F->Path, "r");
flock(fileno(F->Handle), LOCK_EX | LOCK_NB);
}
}
}
bool
TryLock(file *F) // USAGE: File shall already be open, if possible
{
bool Result = TRUE;
if(F->Locking && F->Handle)
{
if(flock(fileno(F->Handle), LOCK_EX | LOCK_NB) == -1)
{
Result = FALSE;
}
}
return Result;
}
rc
ReadFileIntoBuffer(file *F)
{
rc Result = RC_ERROR_FILE;
if(F->Path)
{
if(F->Handle || (F->Handle = fopen(F->Path, "r")))
{
if(TryLock(F))
{
fseek(F->Handle, 0, SEEK_END);
F->Buffer.Size = ftell(F->Handle);
F->Buffer.Location = malloc(F->Buffer.Size);
F->Buffer.Ptr = F->Buffer.Location;
fseek(F->Handle, 0, SEEK_SET);
fread(F->Buffer.Location, F->Buffer.Size, 1, F->Handle);
CloseFile(F, FALSE);
Result = RC_SUCCESS;
}
else
{
Result = RC_ERROR_FILE_LOCKED;
}
}
}
return Result;
}
char *
ReadFileIntoMemory0(FILE *F)
{
char *Result = 0;
if(F)
{
fseek(F, 0, SEEK_END);
int Length = ftell(F);
Result = malloc(Length + 1);
fseek(F, 0, SEEK_SET);
fread(Result, Length, 1, F);
Result[Length] = 0;
}
return Result;
}
int
PrintString(string S)
{
return Print(stderr, "%.*s", (int)S.Length, S.Base);
}
void
PrintStringN(string S)
{
Print(stderr, "\n%.*s", (int)S.Length, S.Base);
}
void
PrintStringI(string S, uint64_t Indentation)
{
for(int i = 0; i < Indentation; ++i)
{
Print(stderr, " ");
}
PrintString(S);
}
int
PrintStringC(colour_code Colour, string String)
{
Colourise(Colour);
int Result = PrintString(String);
Colourise(CS_END);
return Result;
}
void
PrintStringCN(colour_code Colour, string String, bool PrependNewline, bool AppendNewline)
{
if(PrependNewline) { Print(stderr, "\n"); }
Colourise(Colour);
PrintString(String);
Colourise(CS_END);
if(AppendNewline) { Print(stderr, "\n"); }
}
void
PrintC(colour_code Colour, char *String)
{
Colourise(Colour);
Print(stderr, "%s", String);
Colourise(CS_END);
}
#if DEBUG_PRINT_FUNCTION_NAMES
void
PrintFunctionName(char *N)
{
PrintC(CS_MAGENTA, N);
Print(stderr, "\n");
}
void
PrintLinedFunctionName(int LineNumber, char *N)
{
Colourise(CS_BLACK_BOLD);
Print(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_MINUS,
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",
"MINUS",
"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
};
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;
char *IdentifierDescription_Role;
char *IdentifierDescription_Credit;
bool IdentifierDescriptionDisplayed;
bool IdentifierDescription_MediumDisplayed;
bool LocalVariableDescriptionDisplayed;
bool IdentifierDescription_RoleDisplayed;
bool IdentifierDescription_CreditDisplayed;
} config_identifier;
config_identifier ConfigIdentifiers[] =
{
{ "" },
{ "abbrev_dotted_initial_and_surname", "The dotted-initial(s) and surname of a person's name, used to override the auto-derived one, e.g. J. R. R. Tolkien (from John Ronald Reuel Tolkien) or J. du Pré (from Jacqueline du Pré)" },
{ "abbrev_given_or_nickname", "The given or quoted nickname of a person's name, used to override the auto-derived one, e.g. Charlotte (from Charlotte Brontë) or Ry (from Ryland Peter \"Ry\" Cooder)" },
{ "abbrev_initial", "The initials of a person's name, used to override the auto-derived ones, e.g. KB (from Kate Bush)" },
{ "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, but indexers may \"uncredit\" people in the [video] node of a HMML file."
},
{ "credit", "The ID of a person (see also person) who contributed to a project. They will then appear in the credits menu of each entry in the project. Note that setting a credit in the configuration \
file credits this person for the entire project, but indexers may \"uncredit\" people in the [video] node of a HMML file." },
{ "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_cc_lang", "The ISO 639-1 language code of the closed captions to enable by default. May be overridden by setting the cc_lang in the video node of an HMML file." },
{ "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." },
{ "deny_bespoke_templates", "Indexers may use the \"template\" attribute in the video node of an .hmml file to set a bespoke template for that entry, superseding any configured player_template. Setting \"deny_bespoke_templates\" to true prevents this." },
{ "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, but indexers may \"uncredit\" people in the [video] node of a HMML file."
},
{ "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, but indexers may \"uncredit\" people in the [video] node of a HMML file."
},
{ "instance_title", "The name of the instance, used as the cinera:title on the global search page." },
{ "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.",
0,
"The name of the role as it appears in the credits menu."
},
{ "non-speaking", "We try to abbreviate the names of speakers to cite them more concisely in the timestamps. Set this to \"true\" to prevent abbreviating the names of people in non-speaking roles, and so avoid erroneous clashes." },
{ "numbering_filename_prefix", "This works in conjunction with the \"filename_derived\" numbering_method. We take the base filename, strip this prefix (default \"$project\") from the start and use the rest as the number." },
{ "numbering_method", "Possible numbering methods: \"auto\" (see also numbering_start and numbering_zero_pad), \"filename_derived\" (see also numbering_filename_prefix), \"hmml_specified\"." },
{ "numbering_scheme", "Possible numbering schemes: \"calendrical\", \"linear\" (the default), \"seasonal\". Currently this setting does nothing." },
{ "numbering_start", "This works in conjunction with the \"auto\" numbering scheme. The number (default 1) denotes the displayed position of the project's first entry recorded in Cinera. Setting it to 0 makes the entries 0-indexed." },
{ "numbering_unit", "This 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." },
{ "numbering_zero_pad", "This works in conjunction with the \"auto\" numbering scheme. It zero-pads the entries' numbers so that they all, from the smallest to largest, contain the same number of digits." },
{ "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: credit).",
0,
"The ID of the person within which 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." },
{ "plural", "The irregular plural form of the role's name. May be left blank for regular plurals (in English, those ending in \"s\")." },
{ "position",
"The position in the credits list in which this role should reside. Positive numbers position the role from the list's start. Negative numbers from the end. Roles lacking a defined position \
fill the slots left vacant by positioned roles in the order in which they are configured.",
},
{ "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."
},
{ "quote_username", "The username by which insobot recognises a person, for the purpose of retrieving quotes attributed to them." },
{ "role",
"This is the role for which a person deserves credit (see also: credit).",
0,
0,
0,
"The ID of the role 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." },
{ "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."
},
{ "suppress_prompts", "When issuing an error, Cinera prompts the user, giving them time to read the error. Setting this to \"true\" prevents this behaviour." },
{ "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." },
{ "vod_platform", "Possible VOD platforms: \"direct\" (for .webm, .mp4, etc. files), \"vimeo\", \"youtube\"." },
{ "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_ABBREV_DOTTED_INITIAL_AND_SURNAME,
IDENT_ABBREV_GIVEN_OR_NICKNAME,
IDENT_ABBREV_INITIAL,
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_CREDIT,
IDENT_CSS_PATH,
IDENT_DB_LOCATION,
IDENT_DEFAULT_CC_LANG,
IDENT_DEFAULT_MEDIUM,
IDENT_DENY,
IDENT_DENY_BESPOKE_TEMPLATES,
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_INSTANCE_TITLE,
IDENT_JS_PATH,
IDENT_LINEAGE,
IDENT_LINEAGE_WITHOUT_ORIGIN,
IDENT_LOG_LEVEL,
IDENT_MEDIUM,
IDENT_NAME,
IDENT_NON_SPEAKING,
IDENT_NUMBERING_FILENAME_PREFIX,
IDENT_NUMBERING_METHOD,
IDENT_NUMBERING_SCHEME,
IDENT_NUMBERING_START,
IDENT_NUMBERING_UNIT,
IDENT_NUMBERING_ZERO_PAD,
IDENT_ORIGIN,
IDENT_OWNER,
IDENT_PERSON,
IDENT_PLAYER_LOCATION,
IDENT_PLAYER_TEMPLATE,
IDENT_PLURAL,
IDENT_POSITION,
IDENT_PRIVACY_CHECK_INTERVAL,
IDENT_PROJECT,
IDENT_QUOTE_USERNAME,
IDENT_ROLE,
IDENT_QUERY_STRING,
IDENT_SEARCH_LOCATION,
IDENT_SEARCH_TEMPLATE,
IDENT_SINGLE_BROWSER_TAB,
IDENT_STREAM_PLATFORM,
IDENT_SUPPORT,
IDENT_SUPPRESS_PROMPTS,
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_VOD_PLATFORM,
IDENT_URL,
IDENT_COUNT,
} config_identifier_id;
void
Indent(uint64_t Indent)
{
for(int i = 0; i < INDENT_WIDTH * Indent; ++i)
{
Print(stderr, " ");
}
}
void
IndentedCarriageReturn(int IndentationLevel)
{
Print(stderr, "\n");
Indent(IndentationLevel);
}
void
AlignText(int Alignment)
{
for(int i = 0; i < Alignment; ++i)
{
Print(stderr, " ");
}
}
void
NewParagraph(int IndentationLevel)
{
Print(stderr, "\n");
IndentedCarriageReturn(IndentationLevel);
}
void
NewSection(char *Title, int *IndentationLevel)
{
IndentedCarriageReturn(*IndentationLevel);
if(Title)
{
Print(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)
{
Print(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)
{
Print(stderr, "\n");
for(int i = 0; i < CurrentColumn; ++i)
{
Print(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;
_memory_book(token) Token;
uint64_t CurrentIndex;
uint64_t CurrentLine;
} tokens;
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;
uint8_t Mapping:6;
uint8_t Unused:2;
} 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;
char *SeverityStrings[] =
{
"error",
"warning",
};
typedef enum
{
S_ERROR,
S_WARNING,
} severity;
char *ErrorDomainStrings[] =
{
"Config",
"Indexing",
"System",
};
typedef enum
{
ED_CONFIG,
ED_INDEXING,
ED_SYSTEM,
} error_domain;
void
ErrorFilenameAndLineNumber(string *Filename, uint64_t LineNumber, severity Severity, error_domain Domain)
{
Print(stderr, "\n"
"┌─ ");
switch(Severity)
{
case S_ERROR: Colourise(CS_ERROR); break;
case S_WARNING: Colourise(CS_WARNING); break;
}
Print(stderr, "%s %s", ErrorDomainStrings[Domain], SeverityStrings[Severity]);
Colourise(CS_END);
if(Filename)
{
if(LineNumber > 0)
{
Print(stderr, " on line ");
Colourise(CS_BLUE_BOLD);
Print(stderr, "%lu", LineNumber);
Colourise(CS_END);
Print(stderr, " of ");
}
else
{
Print(stderr, " with ");
}
PrintStringC(CS_CYAN, *Filename);
}
Print(stderr, "\n"
"└─╼ ");
}
// TODO(matt): Get more errors going through Error()
void
ConfigError(string *Filename, uint64_t LineNumber, severity Severity, char *Message, string *Received)
{
ErrorFilenameAndLineNumber(Filename, LineNumber, Severity, ED_CONFIG);
Print(stderr,
"%s", Message);
if(Received)
{
PrintStringC(CS_MAGENTA_BOLD, *Received);
}
Print(stderr, "\n");
}
void
ConfigErrorField(string *Filename, uint64_t LineNumber, severity Severity, config_identifier_id FieldID,
string *Received, char *Message)
{
ErrorFilenameAndLineNumber(Filename, LineNumber, Severity, ED_CONFIG);
Print(stderr,
"%s%s%s value %s%.*s%s %s\n",
ColourStrings[CS_YELLOW_BOLD], ConfigIdentifiers[FieldID].String, ColourStrings[CS_END],
ColourStrings[CS_MAGENTA_BOLD], (int)Received->Length, Received->Base, ColourStrings[CS_END],
Message);
}
void
ConfigErrorUnset(config_identifier_id FieldID)
{
ErrorFilenameAndLineNumber(0, 0, S_ERROR, ED_CONFIG);
Print(stderr,
"Unset %s\n", ConfigIdentifiers[FieldID].String);
}
void
ConfigErrorUnsetFieldOf(string *Filename, uint64_t LineNumber,
config_identifier_id UnsetFieldID,
config_identifier_id ScopeKey, string ScopeID)
{
ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_CONFIG);
Print(stderr,
"Unset %s%s%s of %s%s%s: %s%.*s%s\n",
ColourStrings[CS_YELLOW_BOLD], ConfigIdentifiers[UnsetFieldID].String, ColourStrings[CS_END],
ColourStrings[CS_YELLOW_BOLD], ConfigIdentifiers[ScopeKey].String, ColourStrings[CS_END],
ColourStrings[CS_GREEN_BOLD], (int)ScopeID.Length, ScopeID.Base, ColourStrings[CS_END]);
}
void
ConfigErrorSizing(string *Filename, uint64_t LineNumber, config_identifier_id FieldID, string *Received, uint64_t MaxSize)
{
ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_CONFIG);
Print(stderr, "%s value is too long (%lu/%lu characters): ", ConfigIdentifiers[FieldID].String, Received->Length, MaxSize);
PrintStringC(CS_MAGENTA_BOLD, *Received);
Print(stderr, "\n");
}
void
ConfigErrorLockedConfigLocation(string *Filename, uint64_t LineNumber, string *Received)
{
ErrorFilenameAndLineNumber(Filename, LineNumber, Filename ? S_WARNING : S_ERROR, ED_CONFIG);
Print(stderr, "%s file %s%.*s%s is in use by another Cinera instance\n", Filename ? "Included" : "Config", ColourStrings[CS_MAGENTA_BOLD], (int)Received->Length, Received->Base, ColourStrings[CS_END]);
}
void
ConfigErrorUnopenableConfigLocation(string *Filename, uint64_t LineNumber, string *Received)
{
ErrorFilenameAndLineNumber(Filename, LineNumber, Filename ? S_WARNING : S_ERROR, ED_CONFIG);
Print(stderr, "%s file %s%.*s%s could not be opened: %s\n", Filename ? "Included" : "Config", ColourStrings[CS_MAGENTA_BOLD], (int)Received->Length, Received->Base, ColourStrings[CS_END], strerror(errno));
}
void
ConfigErrorLockedDBLocation(string *Filename, uint64_t LineNumber, string *Received)
{
ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_CONFIG);
Print(stderr, "File at db_location %s%.*s%s is in use by another Cinera instance\n", ColourStrings[CS_MAGENTA_BOLD], (int)Received->Length, Received->Base, ColourStrings[CS_END]);
}
void
ConfigErrorInt(string *Filename, uint64_t LineNumber, severity Severity, char *Message, uint64_t Number)
{
ErrorFilenameAndLineNumber(Filename, LineNumber, Severity, ED_CONFIG);
Print(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)
{
token *This = GetPlaceInBook(&T->Token, T->CurrentIndex);
string Filepath = Wrap0(T->File.Path);
ErrorFilenameAndLineNumber(&Filepath, This->LineNumber, S_ERROR, ED_CONFIG);
Print(stderr,
"Syntax error: Received ");
if(This->Content.Base)
{
PrintStringC(CS_RED, This->Content);
}
else
{
Print(stderr, "%s%ld%s", ColourStrings[CS_BLUE_BOLD], This->int64_t, ColourStrings[CS_END]);
}
Print(stderr,
" but Expected ");
PrintStringC(CS_GREEN, Wrap0(TokenStrings[GreaterExpectation]));
if(LesserExpectation)
{
Print(stderr,
" or ");
PrintStringC(CS_GREEN, Wrap0(TokenStrings[LesserExpectation]));
}
Print(stderr,
"\n");
}
void
IndexingError(string Filename, uint64_t LineNumber, severity Severity, char *Message, string *Received)
{
ErrorFilenameAndLineNumber(&Filename, LineNumber, Severity, ED_INDEXING);
// TODO(matt): Typeset the Message?
Print(stderr,
"%s", Message);
if(Received)
{
PrintStringC(CS_MAGENTA_BOLD, *Received);
}
Print(stderr, "\n");
}
void
PrintTimecode(FILE *Dest, v4 Timecode)
{
Colourise(CS_BLUE_BOLD);
if(Timecode.Hours)
{
Print(Dest, "%i:%02i:%02i", Timecode.Hours, Timecode.Minutes, Timecode.Seconds);
}
else
{
Print(Dest, "%i:%02i", Timecode.Minutes, Timecode.Seconds);
}
Colourise(CS_END);
}
void
IndexingChronologyError(string *Filename, uint64_t LineNumber, v4 ThisTimecode, v4 PrevTimecode)
{
severity Severity = S_ERROR;
ErrorFilenameAndLineNumber(Filename, LineNumber, Severity, ED_INDEXING);
Print(stderr,
"Timecode ");
PrintTimecode(stderr, ThisTimecode);
Print(stderr, " is chronologically earlier than previous timecode (");
PrintTimecode(stderr, PrevTimecode);
Print(stderr, ")\n");
}
void
IndexingQuoteError(string *Filename, uint64_t LineNumber, string Author, uint64_t QuoteID)
{
ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_INDEXING);
Print(stderr, "Quote not found: ");
Colourise(CS_MAGENTA_BOLD);
Print(stderr, "#");
PrintString(Author);
Print(stderr, " %ld", QuoteID);
Colourise(CS_END);
Print(stderr, "\n");
}
void
IndexingErrorSizing(string *Filename, uint64_t LineNumber, char *Key, string Received, uint64_t MaxSize)
{
ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_INDEXING);
Print(stderr, "%s value is too long (%lu/%lu characters): ", Key, Received.Length, MaxSize);
PrintStringC(CS_MAGENTA_BOLD, Received);
Print(stderr, "\n");
}
void
IndexingErrorClash(string *Filename, uint64_t LineNumber, severity Severity, char *Key, string Received, string ExistingEntryBaseFilename)
{
ErrorFilenameAndLineNumber(Filename, LineNumber, Severity, ED_INDEXING);
Print(stderr, "%s value %s%.*s%s clashes with that of existing entry: ", Key, ColourStrings[CS_MAGENTA_BOLD], (int)Received.Length, Received.Base, ColourStrings[CS_END]);
PrintStringC(CS_MAGENTA, ExistingEntryBaseFilename);
Print(stderr, "\n");
}
void
PrintValidLanguageCodeChars(void)
{
Print(stderr,
" Valid characters:\n"
" a to z\n"
" A to Z\n"
" 0 to 9\n"
" - (hyphen)\n");
}
void
IndexingErrorInvalidLanguageCode(string *Filename, uint64_t LineNumber, char *Key, string Received)
{
ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_INDEXING);
Print(stderr, "%s value %s%.*s%s contains invalid character(s)\n",
Key, ColourStrings[CS_MAGENTA_BOLD], (int)Received.Length, Received.Base, ColourStrings[CS_END]);
}
void
IndexingErrorEmptyField(string *Filename, uint64_t LineNumber, char *Key)
{
ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_INDEXING);
Print(stderr, "%s field cannot be empty\n", Key);
}
void
IndexingErrorInvalidSubstring(string *Filename, uint64_t LineNumber, char *Key, string Received, string InvalidSubstring)
{
ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_INDEXING);
Print(stderr, "%s value %s%.*s%s contains invalid substring: ", Key, ColourStrings[CS_MAGENTA_BOLD], (int)Received.Length, Received.Base, ColourStrings[CS_END]);
PrintStringC(CS_MAGENTA, InvalidSubstring);
Print(stderr, "\n");
}
void
IndexingErrorCustomSizing(string *Filename, uint64_t LineNumber, int CustomIndex, string Received)
{
ErrorFilenameAndLineNumber(Filename, LineNumber, S_ERROR, ED_INDEXING);
Print(stderr, "custom%d value is too long (%lu/%i characters): ", CustomIndex, Received.Length, CustomIndex < 12 ? MAX_CUSTOM_SNIPPET_SHORT_LENGTH : MAX_CUSTOM_SNIPPET_LONG_LENGTH);
PrintStringC(CS_MAGENTA_BOLD, Received);
Print(stderr, "\n");
if(Received.Length < MAX_CUSTOM_SNIPPET_LONG_LENGTH)
{
Print(stderr, "Consider using custom12 to custom15, which can hold %d characters\n", MAX_CUSTOM_SNIPPET_LONG_LENGTH);
}
else
{
Print(stderr, "Consider using a bespoke template for longer amounts of localised information\n");
}
}
void
SystemError(string *Filename, uint64_t LineNumber, severity Severity, char *Message, string *Received)
{
ErrorFilenameAndLineNumber(Filename, LineNumber, Severity, ED_SYSTEM);
// TODO(matt): Typeset the Message?
Print(stderr,
"%s", Message);
if(Received)
{
PrintStringC(CS_MAGENTA_BOLD, *Received);
}
Print(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(string *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);
Print(stderr, " Valid log levels:\n");
for(int i = 0; i < LOG_COUNT; ++i)
{
Print(stderr, " %s\n", LogLevelStrings[i]);
}
return LOG_COUNT;
}
void WaitForInput(void); // NOTE(matt): Forward declared
void
FreeBuffer(buffer *Buffer)
{
/* */ MEM_TEST_TOP();
/* +MEM */ Free(Buffer->Location);
/* */ MEM_TEST_MID();
Buffer->Ptr = 0;
Buffer->Size = 0;
//Buffer->ID = 0;
Buffer->IndentLevel = 0;
MEM_TEST_END();
}
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(string *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);
Print(stderr, " Valid genres:\n");
for(int i = 0; i < GENRE_COUNT; ++i)
{
Print(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(string *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);
Print(stderr, " Valid numbering schemes:\n");
for(int i = 0; i < NS_COUNT; ++i)
{
Print(stderr, " %s\n", NumberingSchemeStrings[i]);
}
return NS_COUNT;
}
char *NumberingMethodStrings[] =
{
"auto",
"filename_derived",
"hmml_specified",
};
typedef enum
{
NM_AUTO,
NM_FILENAME_DERIVED,
NM_HMML_SPECIFIED,
NM_COUNT
} numbering_method;
numbering_method
GetNumberingMethodFromString(string *Filename, token *T)
{
for(int i = 0; i < NM_COUNT; ++i)
{
if(!StringsDifferLv0(T->Content, NumberingMethodStrings[i])) { return i; }
}
ConfigError(Filename, T->LineNumber, S_ERROR, "Unknown numbering method: ", &T->Content);
Print(stderr, " Valid numbering methods:\n");
for(int i = 0; i < NM_COUNT; ++i)
{
Print(stderr, " %s\n", NumberingMethodStrings[i]);
}
return NM_COUNT;
}
typedef struct
{
uint32_t StartingNumber;
bool ZeroPadded;
} numbering_auto_params;
typedef struct
{
string Prefix;
} numbering_filename_derived_params;
typedef struct
{
string Unit;
numbering_method Method;
numbering_scheme Scheme;
numbering_auto_params Auto;
numbering_filename_derived_params FilenameDerived;
} numbering;
bool
GetBoolFromString(string *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);
Print(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
IsValidLanguageCode(string S)
{
// TODO(matt): Add an upper limit to the length
bool Result = TRUE;
for(int i = 0; i < S.Length; ++i)
{
char C = S.Base[i];
if(!((C >= '0' && C <= '9') || (C >= 'a' && C <= 'z') || (C >= 'A' && C <= 'Z') || C == '-'))
{
Result = FALSE;
break;
}
}
return Result;
}
bool
IsNumber(char C)
{
return (C >= '0' && C <= '9');
}
int64_t
ParseArtVariantsString(string *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)
{
Print(stderr, " Valid variant_types:\n");
int FirstValidVariant = 1;
for(int i = FirstValidVariant; i < CAV_COUNT; ++i)
{
Print(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(string *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);
Print(stderr, " Valid icon_types:\n");
int FirstValidIconType = 1;
for(int i = FirstValidIconType; i < IT_COUNT; ++i)
{
Print(stderr, " %s\n", IconTypeStrings[i]);
}
return IT_COUNT;
}
char *VODPlatformStrings[] =
{
0,
"direct",
"vimeo",
"youtube",
};
typedef enum
{
VP_DEFAULT_UNSET,
VP_DIRECT,
VP_VIMEO,
VP_YOUTUBE,
VP_COUNT,
} vod_platform;
vod_platform
GetVODPlatformFromString(string *Filename, token *T)
{
for(int i = 0; i < VP_COUNT; ++i)
{
if(!StringsDifferLv0(T->Content, VODPlatformStrings[i])) { return i; }
}
ConfigError(Filename, T->LineNumber, S_ERROR, "Unknown vod_platform: ", &T->Content);
Print(stderr, " Valid vod_platforms:\n");
int FirstValidVODPlatform = 1;
for(int i = FirstValidVODPlatform; i < VP_COUNT; ++i)
{
Print(stderr, " %s\n", VODPlatformStrings[i]);
}
return VP_COUNT;
}
vod_platform
GetVODPlatform(string VODPlatform)
{
for(int i = 0; i < VP_COUNT; ++i)
{
if(!StringsDifferLv0(VODPlatform, VODPlatformStrings[i])) { return i; }
}
return VP_COUNT;
}
bool
IsValidVODPlatform(vod_platform V)
{
return V > VP_DEFAULT_UNSET && V < VP_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
{
// NOTE(matt): PrevStart is Absolute (or relative to start of file),
// the others are Relative to PrevStart
unsigned int PrevStart, NextStart; // 8
unsigned short int PrevEnd, NextEnd; // 4
} link_insertion_offsets; // 12
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];
} 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 Number[MAX_NUMBER_LENGTH];
char Reserved[30];
} 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;
hash32 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 Entry;
} db_project_and_entry_index5;
typedef struct
{
db_project_and_entry_index5 Index;
uint32_t Position;
} db_landmark5;
typedef struct
{
file File;
file_signposted Metadata;
bool Ready;
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
typedef enum
{
B_ASET,
B_PROJ,
} block_id;
uint32_t
GetFourFromBlockID(block_id ID)
{
uint32_t Result = 0;
switch(ID)
{
case B_ASET: Result = FOURCC("ASET"); break;
case B_PROJ: Result = FOURCC("PROJ"); break;
}
return Result;
}
// TODO(matt): Increment CINERA_DB_VERSION!
typedef enum
{
DBS_BLOCK,
DBS_ENTRY,
DBS_HEADERED_SECTION,
DBS_INDEXER_FIELD,
} db_structure_item_type;
typedef enum
{
IDX_SINGLE,
IDX_COMPOUND,
} db_structure_indexer_type;
typedef struct
{
db_structure_item_type Type;
void *Item;
} db_structure_item;
typedef struct
{
string Name;
uint32_t Offset;
uint32_t SizeOf;
db_structure_item Parent;
} db_structure_field;
typedef struct
{
db_structure_indexer_type Type;
db_structure_field Field;
} db_structure_indexer;
db_structure_field
PackDBStructureField(string Name, uint32_t Offset, uint32_t SizeOf)
{
db_structure_field Result = { .Name = Name, .Offset = Offset, .SizeOf = SizeOf };
return Result;
}
typedef enum
{
DST_SELF,
DST_CHUNK,
} db_structure_sort_type;
typedef struct
{
db_structure_field Field;
db_structure_sort_type Type;
} db_structure_sorting_field;
typedef struct
{
string Name;
uint32_t SizeOf;
_memory_book(db_structure_indexer) IndexerFields;
db_structure_item Parent;
} db_structure_datatype;
typedef struct
{
db_structure_datatype Self;
db_structure_sorting_field SortingField;
db_structure_field EntryCount;
_memory_book(db_structure_item) IndexedIntoBy;
db_structure_datatype Entry;
db_structure_field ChildCount;
db_structure_item Parent;
} db_structure_headered_section;
typedef struct
{
db_structure_headered_section HeaderedSection;
db_structure_field Count;
_memory_book(db_structure_item) IndexedIntoBy;
uint32_t ID;
string PrintableName;
uint32_t SizeOf;
db_structure_datatype Entry;
} db_structure_block;
typedef struct
{
uint32_t HexSignature;
uint32_t SizeOf;
db_structure_field BlockCount;
} db_structure_header;
typedef struct
{
_memory_book(db_structure_block) Blocks;
db_structure_header Header;
uint32_t Version;
} db_structure;
typedef struct
{
_memory_book(db_structure) Structure;
memory_book Strings;
} db_structures;
db_structures DBStructures;
db_structure *CurrentDBStructure;
db_structure *PreviousDBStructure;
db_structure_block *
InitDBStructureBlock(db_structure *S, memory_book *Strings,
char *ID, uint32_t SizeOf,
char *CountName, uint32_t CountOffset, uint32_t CountSizeOf,
char *EntryDataTypeName, uint32_t EntrySizeOf)
{
db_structure_block *Result = MakeSpaceInBook(&S->Blocks);
Result->ID = FOURCC(ID);
Result->PrintableName = WriteStringInBook(Strings, Wrap0(ID));
Result->SizeOf = SizeOf;
Result->Count = PackDBStructureField(WriteStringInBook(Strings, Wrap0(CountName)),
CountOffset,
CountSizeOf);
if(EntrySizeOf)
{
Result->Entry.Name = WriteStringInBook(Strings, Wrap0(EntryDataTypeName));
Result->Entry.SizeOf = EntrySizeOf;
Result->Entry.Parent.Type = DBS_BLOCK;
Result->Entry.Parent.Item = Result;
Result->Entry.IndexerFields = InitBook(sizeof(db_structure_indexer), 4);
}
Result->IndexedIntoBy = InitBook(sizeof(db_structure_item), 4);
return Result;
}
db_structure_headered_section *
InitDBHeaderedSection(db_structure_block *B, memory_book *Strings,
char *Name, uint32_t SizeOf,
char *EntryCountName, uint32_t EntryCountOffset, uint32_t EntryCountSizeOf,
char *ChildCountName, uint32_t ChildCountOffset, uint32_t ChildCountSizeOf,
char *EntryDataTypeName, uint32_t EntrySizeOf)
{
db_structure_headered_section *Result = &B->HeaderedSection;
Result->Self.Name = WriteStringInBook(Strings, Wrap0(Name));
Result->Self.SizeOf = SizeOf;
if(EntryCountSizeOf)
{
Result->EntryCount = PackDBStructureField(WriteStringInBook(Strings, Wrap0(EntryCountName)),
EntryCountOffset, EntryCountSizeOf);
Result->Entry.Name = WriteStringInBook(Strings, Wrap0(EntryDataTypeName));
Result->Entry.SizeOf = EntrySizeOf;
Result->Entry.Parent.Type = DBS_HEADERED_SECTION;
Result->Entry.Parent.Item = Result;
Result->Entry.IndexerFields = InitBook(sizeof(db_structure_indexer), 4);
}
if(ChildCountSizeOf)
{
Result->ChildCount = PackDBStructureField(WriteStringInBook(Strings, Wrap0(ChildCountName)),
ChildCountOffset, ChildCountSizeOf);
}
Result->IndexedIntoBy = InitBook(sizeof(db_structure_item), 4);
Result->Self.IndexerFields = InitBook(sizeof(db_structure_indexer), 4);
Result->Parent.Type = DBS_BLOCK;
Result->Parent.Item = B;
return Result;
}
db_structure_indexer *
DBStructureAddIndexerField(db_structure_datatype *D, memory_book *Strings,
db_structure_item_type ParentType,
char *Name, uint32_t Offset, uint32_t SizeOf, db_structure_indexer_type Type)
{
db_structure_indexer *Result = MakeSpaceInBook(&D->IndexerFields);
Result->Field.Name = WriteStringInBook(Strings, Wrap0(Name));
Result->Field.Offset = Offset;
Result->Field.SizeOf = SizeOf;
Result->Type = Type;
Result->Field.Parent.Type = ParentType;
Result->Field.Parent.Item = D;
return Result;
}
void
BlockIsIndexedIntoBy(db_structure_block *B, db_structure_item_type Type, void *Indexer)
{
db_structure_item *New = MakeSpaceInBook(&B->IndexedIntoBy);
New->Type = Type;
New->Item = Indexer;
}
void
HeaderedSectionIsIndexedIntoBy(db_structure_headered_section *H, db_structure_item_type Type, void *Indexer)
{
db_structure_item *New = MakeSpaceInBook(&H->IndexedIntoBy);
New->Type = Type;
New->Item = Indexer;
}
void
InitDBStructure4(void)
{
db_structure *S = MakeSpaceInBook(&DBStructures.Structure);
memory_book *Strings = &DBStructures.Strings;
S->Version = 4;
S->Header.HexSignature = FOURCC("CNRA");
S->Header.SizeOf = sizeof(db_header4);
db_header4 db_header4_;
S->Header.BlockCount = PackDBStructureField(
WriteStringInBook(Strings, Wrap0("BlockCount")),
offsetof(db_header4, BlockCount),
sizeof(db_header4_.BlockCount));
S->Blocks = InitBook(sizeof(db_structure_block), 4);
db_header_entries4 db_header_entries4_;
db_structure_block *BlockNTRY = InitDBStructureBlock(S, Strings,
"NTRY", sizeof(db_header_entries4_),
"Count", offsetof(db_header_entries4, Count), sizeof(db_header_entries4_.Count),
"db_entry4", sizeof(db_entry4));
db_header_assets4 db_header_assets4_;
db_structure_block *BlockASET = InitDBStructureBlock(S, Strings,
"ASET", sizeof(db_header_assets4_),
"Count", offsetof(db_header_assets4, Count), sizeof(db_header_assets4_.Count),
0, 0);
db_asset4 db_asset4_;
db_structure_headered_section *HeaderAssets = InitDBHeaderedSection(BlockASET, Strings,
"db_asset4", sizeof(db_asset4_),
"LandmarkCount", offsetof(db_asset4, LandmarkCount), sizeof(db_asset4_.LandmarkCount),
0, 0, 0,
"db_landmark4", sizeof(db_landmark4));
BlockIsIndexedIntoBy(BlockNTRY, DBS_ENTRY, &HeaderAssets->Entry);
}
db_structure *
InitDBStructure5(void)
{
db_structure *S = MakeSpaceInBook(&DBStructures.Structure);
memory_book *Strings = &DBStructures.Strings;
S->Version = 5;
S->Header.HexSignature = FOURCC("CNRA");
S->Header.SizeOf = sizeof(db_header5);
db_header5 db_header5_;
S->Header.BlockCount = PackDBStructureField(
WriteStringInBook(Strings, Wrap0("BlockCount")),
offsetof(db_header5, BlockCount),
sizeof(db_header5_.BlockCount));
S->Blocks = InitBook(sizeof(db_structure_block), 4);
db_block_projects5 db_block_projects5_;
db_structure_block *BlockPROJ = InitDBStructureBlock(S, Strings,
"PROJ", sizeof(db_block_projects5_),
"Count", offsetof(db_block_projects5, Count), sizeof(db_block_projects5_.Count),
0, 0);
db_header_project5 db_header_project5_;
db_structure_headered_section *HeaderProjects = InitDBHeaderedSection(BlockPROJ, Strings,
"db_header_project5", sizeof(db_header_project5_),
"EntryCount", offsetof(db_header_project5, EntryCount), sizeof(db_header_project5_.EntryCount),
"ChildCount", offsetof(db_header_project5, ChildCount), sizeof(db_header_project5_.ChildCount),
"db_entry5", sizeof(db_entry5));
db_structure_indexer *ProjectsArtIndex = DBStructureAddIndexerField(&HeaderProjects->Self, Strings,
DBS_HEADERED_SECTION,
"ArtIndex", offsetof(db_header_project5, ArtIndex), sizeof(db_header_project5_.ArtIndex),
IDX_SINGLE);
db_structure_indexer *ProjectsIconIndex = DBStructureAddIndexerField(&HeaderProjects->Self, Strings,
DBS_HEADERED_SECTION,
"IconIndex", offsetof(db_header_project5, IconIndex), sizeof(db_header_project5_.IconIndex),
IDX_SINGLE);
db_entry5 db_entry5_;
db_structure_indexer *EntryArtIndex = DBStructureAddIndexerField(&HeaderProjects->Entry, Strings,
DBS_ENTRY,
"ArtIndex", offsetof(db_entry5, ArtIndex), sizeof(db_entry5_.ArtIndex),
IDX_SINGLE);
db_block_assets5 db_block_assets5_;
db_structure_block *BlockASET = InitDBStructureBlock(S, Strings,
"ASET", sizeof(db_block_assets5_),
"Count", offsetof(db_block_assets5, Count), sizeof(db_block_assets5_.Count),
0, 0);
db_asset5 db_asset5_;
db_structure_headered_section *HeaderAssets = InitDBHeaderedSection(BlockASET, Strings,
"db_asset5", sizeof(db_asset5_),
"LandmarkCount", offsetof(db_asset5, LandmarkCount), sizeof(db_asset5_.LandmarkCount),
0, 0, 0,
"db_landmark5", sizeof(db_landmark5));
db_landmark5 db_landmark5_;
db_structure_indexer *LandmarkProjectAndEntryIndex = DBStructureAddIndexerField(&HeaderAssets->Entry, Strings,
DBS_ENTRY,
"Index", offsetof(db_landmark5, Index), sizeof(db_landmark5_.Index),
IDX_COMPOUND);
BlockIsIndexedIntoBy(BlockPROJ, DBS_INDEXER_FIELD, LandmarkProjectAndEntryIndex);
HeaderedSectionIsIndexedIntoBy(HeaderProjects, DBS_INDEXER_FIELD, LandmarkProjectAndEntryIndex);
BlockIsIndexedIntoBy(BlockASET, DBS_INDEXER_FIELD, ProjectsArtIndex);
BlockIsIndexedIntoBy(BlockASET, DBS_INDEXER_FIELD, ProjectsIconIndex);
BlockIsIndexedIntoBy(BlockASET, DBS_INDEXER_FIELD, EntryArtIndex);
return S;
}
void
InitDBStructures(void)
{
DBStructures.Structure = InitBook(sizeof(db_structure), 4);
DBStructures.Strings = InitBookOfStrings(Kilobytes(4));
InitDBStructure4();
CurrentDBStructure = InitDBStructure5();
}
uint64_t
ReadDBField(void *ParentInDB, db_structure_field F)
{
uint64_t Result = 0;
char *Ptr = ParentInDB;
Ptr += F.Offset;
memcpy(&Result, Ptr, F.SizeOf);
return Result;
}
db_structure_block *
GetDBStructureBlock(uint32_t BlockID)
{
// NOTE(matt): Usage: To use an older database version, temporarily set CurrentDBStructure before calling this function
db_structure_block *Result = 0;
for(int i = 0; i < CurrentDBStructure->Blocks.ItemCount; ++i)
{
db_structure_block *This = GetPlaceInBook(&CurrentDBStructure->Blocks, i);
if(This->ID == BlockID)
{
Result = This;
break;
}
}
return Result;
}
db_structure_headered_section *
GetDBStructureHeaderedSection(block_id BlockID)
{
// NOTE(matt): Usage: To use an older database version, temporarily set CurrentDBStructure before calling this function
return &GetDBStructureBlock(GetFourFromBlockID(BlockID))->HeaderedSection;
}
void
PrintFour(FILE *Dest, uint32_t Four)
{
Colourise(CS_YELLOW);
Print(Dest, "%c%c%c%c", Four, Four >> 8, Four >> 16, Four >> 24);
Colourise(CS_END);
}
void
PrintDBStructureDataType(FILE *Dest, db_structure_datatype DataType)
{
Print(Dest, "%s%.*s%s (%s%u%s byte%s)",
ColourStrings[CS_MAGENTA], (int)DataType.Name.Length, DataType.Name.Base, ColourStrings[CS_END],
ColourStrings[CS_BLUE_BOLD], DataType.SizeOf, ColourStrings[CS_END],
DataType.SizeOf == 1 ? "" : "s");
}
void
TypesetDBStructureField(FILE *Dest, int IndentLevel, db_structure_field Field)
{
if(Field.SizeOf)
{
IndentedCarriageReturn(IndentLevel);
Print(Dest, "%s%.*s%s: %s%u%s byte%s wide @ %s%u%s-byte offset",
ColourStrings[CS_GREEN], (int)Field.Name.Length, Field.Name.Base, ColourStrings[CS_END],
ColourStrings[CS_BLUE_BOLD], Field.SizeOf, ColourStrings[CS_END],
Field.SizeOf == 1 ? "" : "s",
ColourStrings[CS_BLUE_BOLD], Field.Offset, ColourStrings[CS_END]);
}
}
void
PrintDBBlockIDandSize(FILE *Dest, int IndentLevel, uint32_t ID, uint32_t SizeOf)
{
PrintFour(Dest, ID);
Print(Dest, " (%s%u%s bytes)", ColourStrings[CS_BLUE_BOLD], SizeOf, ColourStrings[CS_END]);
}
void
PrintDBStructureHeader(FILE *Dest, int IndentLevel, db_structure_header *Header)
{
IndentedCarriageReturn(IndentLevel);
PrintDBBlockIDandSize(Dest, IndentLevel, Header->HexSignature, Header->SizeOf);
++IndentLevel;
TypesetDBStructureField(Dest, IndentLevel, Header->BlockCount);
--IndentLevel;
}
typedef struct
{
string *Name;
db_structure_item_type StructuralItemType;
db_structure_indexer_type IndexerType;
} db_indexer_name_and_type;
void
PrintDBStructureItemLineage(FILE *Dest, db_structure_item *Item)
{
db_structure_item *This = Item;
memory_book Lineage = InitBook(sizeof(db_indexer_name_and_type), 4);
while(This)
{
db_indexer_name_and_type *New = MakeSpaceInBook(&Lineage);
New->StructuralItemType = This->Type;
switch(This->Type)
{
case DBS_BLOCK:
{
db_structure_block *It = (db_structure_block *)This->Item;
New->Name = &It->PrintableName;
This = 0;
} break;
case DBS_ENTRY:
{
db_structure_datatype *It = (db_structure_datatype *)This->Item;
New->Name = &It->Name;
This = &It->Parent;
} break;
case DBS_HEADERED_SECTION:
{
db_structure_headered_section *It = (db_structure_headered_section *)This->Item;
New->Name = &It->Self.Name;
This = &It->Parent;
} break;
case DBS_INDEXER_FIELD:
{
db_structure_indexer *It = (db_structure_indexer *)This->Item;
New->Name = &It->Field.Name;
New->IndexerType = It->Type;
This = &It->Field.Parent;
} break;
}
}
db_indexer_name_and_type *Cursor = GetPlaceInBook(&Lineage, Lineage.ItemCount - 1);
PrintString(*Cursor->Name);
for(int i = Lineage.ItemCount - 2; i >= 0; --i)
{
Cursor = GetPlaceInBook(&Lineage, i);
Print(Dest, "%s", Cursor->StructuralItemType == DBS_INDEXER_FIELD ? "." : "/");
PrintString(*Cursor->Name);
if(Cursor->IndexerType == IDX_COMPOUND)
{
Print(Dest, " [compound]");
}
}
FreeBook(&Lineage);
}
void
TypesetIndexers(FILE *Dest, int IndentLevel, memory_book *IndexedIntoBy)
{
if(IndexedIntoBy->ItemCount)
{
IndentedCarriageReturn(IndentLevel);
Colourise(CS_YELLOW_BOLD);
Print(Dest, "╿ is indexed into by:");
for(int i = 0; i < IndexedIntoBy->ItemCount - 1; ++i)
{
IndentedCarriageReturn(IndentLevel);
db_structure_item *This = GetPlaceInBook(IndexedIntoBy, i);
Print(Dest, "├── ");
PrintDBStructureItemLineage(Dest, This);
}
IndentedCarriageReturn(IndentLevel);
db_structure_item *This = GetPlaceInBook(IndexedIntoBy, IndexedIntoBy->ItemCount - 1);
Print(Dest, "└── ");
PrintDBStructureItemLineage(Dest, This);
Colourise(CS_END);
}
}
void
PrintDBStructureHeaderedSection(FILE *Dest, int IndentLevel, db_structure_headered_section *H)
{
IndentedCarriageReturn(IndentLevel);
PrintDBStructureDataType(Dest, H->Self);
++IndentLevel;
TypesetDBStructureField(Dest, IndentLevel, H->EntryCount);
TypesetIndexers(Dest, IndentLevel, &H->IndexedIntoBy);
TypesetDBStructureField(Dest, IndentLevel, H->ChildCount);
IndentedCarriageReturn(IndentLevel);
PrintDBStructureDataType(Dest, H->Entry);
--IndentLevel;
}
void
PrintDBStructureBlock(FILE *Dest, int IndentLevel, db_structure_block *Block)
{
IndentedCarriageReturn(IndentLevel);
PrintFour(Dest, Block->ID);
Print(Dest, " (%s%u%s bytes)", ColourStrings[CS_BLUE_BOLD], Block->SizeOf, ColourStrings[CS_END]);
++IndentLevel;
TypesetDBStructureField(Dest, IndentLevel, Block->Count);
TypesetIndexers(Dest, IndentLevel, &Block->IndexedIntoBy);
if(Block->HeaderedSection.Self.SizeOf)
{
PrintDBStructureHeaderedSection(Dest, IndentLevel, &Block->HeaderedSection);
}
if(Block->Entry.SizeOf)
{
IndentedCarriageReturn(IndentLevel);
PrintDBStructureDataType(Dest, Block->Entry);
}
--IndentLevel;
}
void
PrintDBStructure(FILE *Dest, int IndentLevel, db_structure *S)
{
IndentedCarriageReturn(IndentLevel);
Print(Dest, "Version %s%u%s", ColourStrings[CS_MAGENTA_BOLD], S->Version, ColourStrings[CS_END]);
PrintDBStructureHeader(Dest, IndentLevel, &S->Header);
++IndentLevel;
for(int i = 0; i < S->Blocks.ItemCount; ++i)
{
db_structure_block *Block = GetPlaceInBook(&S->Blocks, i);
PrintDBStructureBlock(Dest, IndentLevel, Block);
}
--IndentLevel;
}
void
PrintDBStructures(FILE *Dest)
{
int IndentLevel = 0;
for(int i = 0; i < DBStructures.Structure.ItemCount; ++i)
{
db_structure *S = GetPlaceInBook(&DBStructures.Structure, i);
PrintDBStructure(Dest, IndentLevel, S);
}
Print(Dest, "\n");
}
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];
asset_hash 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;
_memory_book(landmark) Search;
_memory_book(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_clear.js" },
{ ASSET_JS, "cinera_search_pre.js" },
{ ASSET_JS, "cinera_search_post.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_CLEAR,
ASSET_JS_SEARCH_PRE,
ASSET_JS_SEARCH_POST,
ASSET_JS_PLAYER_PRE,
ASSET_JS_PLAYER_POST,
BUILTIN_ASSETS_COUNT,
} builtin_asset_id;
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_GLOBAL_NAV,
// TAG_PATHED_NAV,
TAG_PROJECT,
TAG_PROJECT_ID,
TAG_PROJECT_LINEAGE,
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_GLOBAL_NAV__",
//"__CINERA_PATHED_NAV__",
"__CINERA_PROJECT__",
"__CINERA_PROJECT_ID__",
"__CINERA_PROJECT_LINEAGE__",
"__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 project project; // NOTE(matt): Forward declared. Consider reorganising the code?
typedef struct
{
project *ChildrenOf;
navigation_type Type;
} navigation_spec;
typedef struct
{
buffer Buffer;
navigation_spec Spec;
} navigation_buffer;
typedef struct
{
int Offset;
template_tag_code TagCode;
asset *Asset;
navigation_buffer *Nav;
} tag_offset;
typedef enum
{
TEMPLATE_NULL,
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;
_memory_book(tag_offset) Tags;
_memory_book(navigation_buffer) NavBuffer;
} template_metadata;
typedef struct
{
file File;
template_metadata Metadata;
} template;
void
FreeTemplateNavBuffers(template *T)
{
for(int i = 0; i < T->Metadata.NavBuffer.ItemCount; ++i)
{
navigation_buffer *This = GetPlaceInBook(&T->Metadata.NavBuffer, i);
FreeBuffer(&This->Buffer);
}
}
void
ClearTemplateMetadata(template *Template)
{
FreeTemplateNavBuffers(Template);
FreeBook(&Template->Metadata.NavBuffer);
FreeBook(&Template->Metadata.Tags);
template_metadata Zero = {};
Template->Metadata = Zero;
}
void
FreeFileBufferAndPath(file *F)
{
FreeBuffer(&F->Buffer);
Free(F->Path);
}
void
FreeFile(file *F, bool CloseLocking)
{
CloseFile(F, CloseLocking);
FreeFileBufferAndPath(F);
}
void
FreeSignpostedFile(file_signposted *F, bool CloseLocking)
{
FreeFile(&F->File, CloseLocking);
file_signposts Zero = {};
F->Signposts = Zero;
}
void
FreeTemplate(template *Template)
{
FreeFile(&Template->File, NA);
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;
}
string
TrimString(string S, uint32_t CharsFromStart, uint32_t CharsFromEnd)
{
string Result = S;
int OriginalLength = Result.Length;
Result.Length -= MIN(OriginalLength, (CharsFromStart + CharsFromEnd));
Result.Base += MIN(OriginalLength, CharsFromStart);
return Result;
}
string
TrimWhitespace(string S)
{
string Result = S;
while(Result.Length > 0 && *Result.Base == ' ')
{
++Result.Base;
--Result.Length;
}
while(Result.Length > 0 && Result.Base[Result.Length - 1] == ' ')
{
--Result.Length;
}
return Result;
}
char *WatchTypeStrings[] =
{
"WT_HMML",
"WT_ASSET",
"WT_CONFIG",
};
typedef enum
{
WT_HMML,
WT_ASSET,
WT_CONFIG,
} watch_type;
typedef struct
{
watch_type Type;
string Path;
extension_id Extension;
project *Project;
asset *Asset;
FILE *Handle;
} watch_file;
typedef struct
{
int Descriptor;
string WatchedPath;
string TargetPath;
_memory_book(watch_file) Files;
} watch_handle;
typedef struct
{
_memory_book(watch_handle) Handles;
memory_book Paths;
uint32_t DefaultEventsMask;
} watch_handles;
#include "cinera_config.c"
#define AFD 1
#define AFE 0
// NOTE(matt): Globals
bool GlobalRunning;
config *Config;
project *CurrentProject;
mode Mode;
arena MemoryArena;
_memory_book(asset) Assets;
int inotifyInstance;
watch_handles WatchHandles;
database DB;
time_t LastPrivacyCheck;
time_t LastQuoteFetch;
#define GLOBAL_UPDATE_INTERVAL 1
//
void
WaitForInput()
{
if(!Config || !Config->SuppressingPrompts)
{
Print(stderr, "Press Enter to continue...\n");
getchar();
}
}
bool
SeekConfirmation(char *Message, bool Default)
{
bool Result = Default;
if(!Config || !Config->SuppressingPrompts)
{
Print(stderr, "%s (%s)\n", Message, Default == TRUE ? "Y/n" : "y/N");
switch(getchar())
{
case 'n':
case 'N':
Result = FALSE; break;
case 'y':
case 'Y':
Result = TRUE; break;
default: break;
};
}
return Result;
}
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;
typedef struct
{
char *Name;
colour_code Colour;
} edit_type;
typedef enum
{
EDIT_INSERTION,
EDIT_APPEND,
EDIT_REINSERTION,
EDIT_DELETION,
EDIT_ADDITION,
EDIT_SKIP,
} edit_type_id;
edit_type EditTypes[] =
{
{ "Inserted", CS_ADDITION },
{ "Appended", CS_ADDITION },
{ "Reinserted", CS_REINSERTION },
{ "Deleted", CS_DELETION },
{ "Added", CS_ADDITION },
{ "Skipped", CS_ERROR },
};
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;
}
}
WriteToFile(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
WriteToFile(LogFile, "\n");
fclose(LogFile);
Free(LogPath);
}
}
void
LogEdit(edit_type_id EditType, string Lineage, string EntryID, string *EntryTitle, bool Private)
{
if(Private)
{
LogError(LOG_NOTICE, "Privately %s %.*s/%.*s", EditTypes[EditType].Name, (int)Lineage.Length, Lineage.Base, (int)EntryID.Length, EntryID.Base);
}
else
{
LogError(LOG_NOTICE, "%s %.*s/%.*s - %.*s", EditTypes[EditType].Name, (int)Lineage.Length, Lineage.Base, (int)EntryID.Length, EntryID.Base, (int)EntryTitle->Length, EntryTitle->Base);
}
}
#define CINERA_HSL_TRANSPARENT_HUE 65535
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;
}
bool
IsValidHSLColour(hsl_colour Colour)
{
return (Colour.Hue >= 0 && Colour.Hue < 360)
&& (Colour.Saturation >= 0 && Colour.Saturation <= 100)
&& (Colour.Lightness >= 0 && Colour.Lightness <= 100);
}
#define SLASH 1
#define NULLTERM 1
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 + NULLTERM];
char Custom1[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM];
char Custom2[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM];
char Custom3[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM];
char Custom4[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM];
char Custom5[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM];
char Custom6[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM];
char Custom7[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM];
char Custom8[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM];
char Custom9[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM];
char Custom10[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM];
char Custom11[MAX_CUSTOM_SNIPPET_SHORT_LENGTH + NULLTERM];
char Custom12[MAX_CUSTOM_SNIPPET_LONG_LENGTH + NULLTERM];
char Custom13[MAX_CUSTOM_SNIPPET_LONG_LENGTH + NULLTERM];
char Custom14[MAX_CUSTOM_SNIPPET_LONG_LENGTH + NULLTERM];
char Custom15[MAX_CUSTOM_SNIPPET_LONG_LENGTH + NULLTERM];
char Title[MAX_TITLE_LENGTH + NULLTERM];
char URLSearch[MAX_BASE_URL_LENGTH + SLASH + MAX_RELATIVE_PAGE_LOCATION_LENGTH + NULLTERM];
char URLPlayer[MAX_BASE_URL_LENGTH + SLASH + MAX_RELATIVE_PAGE_LOCATION_LENGTH + SLASH + MAX_BASE_FILENAME_LENGTH + NULLTERM];
char VideoID[MAX_VOD_ID_LENGTH + NULLTERM];
vod_platform VODPlatform;
} buffers;
rc
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;
Print(stdout, " ClaimBuffer(%s): %ld\n"
" Total ClaimedMemory: %ld (%.2f%%, leaving %ld free)\n\n", BufferIDStrings[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
Print(stdout, "DeclaimBuffer(%s)\n"
" Used: %li / %ld (%.2f%%)\n"
"\n"
" Total ClaimedMemory: %ld\n\n",
BufferIDStrings[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 admin@miblo.net
LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", BufferIDStrings[Buffer->ID], PercentageUsed);
Print(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 admin@miblo.net
LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", BufferIDStrings[Buffer->ID], PercentageUsed);
Print(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;
Print(stdout, "Rewinding %s\n"
" Used: %ld / %ld (%.2f%%)\n\n",
BufferIDStrings[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;
typedef struct
{
uint32_t Date;
string Text;
} quote_info;
typedef struct
{
v4 Timecode;
int Identifier;
} identifier;
typedef struct
{
string RefTitle;
string URL;
string Source;
char *ID;
_memory_book(identifier) Identifier;
} ref_info;
typedef struct
{
string Marker;
string WrittenText;
hsl_colour Colour;
} category_info;
#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)
{
Print(stdout, "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)
{
Print(stdout, "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)
{
Print(stdout, "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");
}
Dest += CopyBytes(Dest, String.Base, String.Length);
*Dest = '\0';
return String.Length;
}
#define ClearCopyStringNoFormatOrTerminate(Dest, DestSize, String) ClearCopyStringNoFormatOrTerminate_(__LINE__, Dest, DestSize, String)
int
ClearCopyStringNoFormatOrTerminate_(int LineNumber, char *Dest, int DestSize, string String)
{
if(String.Length > DestSize)
{
Print(stdout, "ClearCopyStringNoFormatOrTerminate() call on line %d has been passed a buffer too small (%d bytes) to contain %ld-character string:\n"
"%.*s\n", LineNumber, DestSize, String.Length, (int)String.Length, String.Base);
__asm__("int3");
}
Clear(Dest, DestSize);
return CopyBytes(Dest, String.Base, 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 BytesToWrite = 0;
while(String[BytesToWrite] && String[BytesToWrite] != Terminator)
{
++BytesToWrite;
}
if(BytesToWrite >= DestSize)
{
Print(stdout, "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, BytesToWrite, BytesToWrite, String);
__asm__("int3");
}
Dest += CopyBytes(Dest, String, BytesToWrite);
*Dest = '\0';
return BytesToWrite;
}
#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)
{
Print(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;
}
int
DigitsInTimecode(v4 Timecode)
{
int Result = 0;
int ColonChar = 1;
if(Timecode.Hours > 0)
{
Result += DigitsInInt(&Timecode.Hours);
Result += ColonChar;
Result += MAX(2, DigitsInInt(&Timecode.Minutes));
Result += ColonChar;
Result += MAX(2, DigitsInInt(&Timecode.Seconds));
}
else
{
Result += DigitsInInt(&Timecode.Minutes);
Result += ColonChar;
Result += MAX(2, DigitsInInt(&Timecode.Seconds));
}
return Result;
}
#define CopyTimecodeToBuffer(Dest, Timecode) CopyTimecodeToBuffer_(__LINE__, Dest, Timecode)
void
CopyTimecodeToBuffer_(int LineNumber, buffer *Dest, v4 Timecode)
{
if(DigitsInTimecode(Timecode) + (Dest->Ptr - Dest->Location) >= Dest->Size)
{
Print(stderr, "CopyTimecodeToBuffer(%s) call on line %d cannot accommodate timecode:\n", BufferIDStrings[Dest->ID], LineNumber);
PrintTimecode(stderr, Timecode);
Print(stderr, "\n");
__asm__("int3");
}
else
{
if(Timecode.Hours > 0)
{
CopyStringToBuffer(Dest, "%i:%02i:%02i", Timecode.Hours, Timecode.Minutes, Timecode.Seconds);
}
else
{
CopyStringToBuffer(Dest, "%i:%02i", Timecode.Minutes, Timecode.Seconds);
}
}
}
#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))
{
Print(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");
}
Dest->Ptr += CopyBytes(Dest->Ptr, String.Base, String.Length);
*Dest->Ptr = '\0';
}
#define CopyStringToBufferNoTerminate(Dest, Src) CopyStringToBufferNoTerminate_(__LINE__, Dest, Src)
void
CopyStringToBufferNoTerminate_(int LineNumber, buffer *Dest, string Src)
{
if((Dest->Ptr - Dest->Location + Src.Length) > Dest->Size)
{
Print(stderr, "CopyStringToBufferNoTerminate(%s) call on line %d cannot accommodate %ld-character string:\n"
"%.*s\n", BufferIDStrings[Dest->ID], LineNumber, Src.Length, (int)Src.Length, Src.Base);
__asm__("int3");
}
Dest->Ptr += CopyBytes(Dest->Ptr, Src.Base, Src.Length);
}
#define CopyStringToBufferNoFormatL(Dest, Length, String) CopyStringToBufferNoFormatL_(__LINE__, Dest, Length, String)
void
CopyStringToBufferNoFormatL_(int LineNumber, buffer *Dest, int Length, char *String)
{
if(Dest->Ptr - Dest->Location + Length + 1 >= Dest->Size)
{
Print(stderr, "CopyStringToBufferNoFormat(%s) call on line %d cannot accommodate %ld(+1)-character string:\n"
"%s\n", BufferIDStrings[Dest->ID], LineNumber, StringLength(String), String);
__asm__("int3");
}
Dest->Ptr += CopyBytes(Dest->Ptr, String, Length);
*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)
{
Print(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 CopyStringToBufferHTMLSafeSurrounded(Dest, Pre, String, Post) CopyStringToBufferHTMLSafeSurrounded_(__LINE__, Dest, Pre, String, Post)
void
CopyStringToBufferHTMLSafeSurrounded_(int LineNumber, buffer *Dest, string Pre, string String, string Post)
{
// NOTE(matt): Only the String gets HTML-encoded. The Pre and Post parts get copied as-is
CopyStringToBufferNoFormat_(LineNumber, Dest, Pre);
CopyStringToBufferHTMLSafe_(LineNumber, Dest, String);
CopyStringToBufferNoFormat_(LineNumber, Dest, Post);
}
#define CopyStringToBufferHTMLSafeBreakingOnSlash(Dest, String) CopyStringToBufferHTMLSafeBreakingOnSlash_(__LINE__, Dest, String)
void
CopyStringToBufferHTMLSafeBreakingOnSlash_(int LineNumber, buffer *Dest, string String)
{
int Length = String.Length;
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++ = '#'; *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.Base[i]; break;
}
}
if(Dest->Ptr - Dest->Location >= Dest->Size)
{
Print(stderr, "CopyStringToBufferHTMLSafeBreakingOnSlash(%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");
}
*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)
{
Print(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)
{
Print(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");
}
Dest->Ptr += CopyBytes(Dest->Ptr, Src->Location, Src->Ptr - Src->Location);
*Dest->Ptr = '\0';
}
// NOTE(matt): Clash Detection
//
char *ResolutionStatusStrings[] =
{
"RS_NOT_STARTED",
" RS_SEEN",
" RS_PENDING",
" RS_RESOLVED",
" RS_REPORTED",
};
typedef enum
{
RS_NOT_STARTED, // Earliest
RS_SEEN,
RS_PENDING,
RS_RESOLVED,
RS_REPORTED, // Latest
} resolution_status;
typedef struct
{
project *Project;
char CandidateBaseFilename[MAX_BASE_FILENAME_LENGTH];
char CurrentOutputValue[MAX_ENTRY_OUTPUT_LENGTH];
char DesiredOutputValue[MAX_ENTRY_OUTPUT_LENGTH];
char IncumbentBaseFilename[MAX_BASE_FILENAME_LENGTH];
resolution_status Status;
bool Vacating;
bool Superseding;
} clash_entry;
char *ChainStructureStrings[] =
{
"Closed Loop",
"Open Ended",
};
typedef enum
{
CS_CLOSED_LOOP,
CS_OPEN_ENDED
} chain_structure;
typedef struct
{
memory_book Book[2];
memory_book *Main;
memory_book *Holder;
memory_book Chain;
chain_structure ChainStructure;
bool ChainIsResolvable;
bool Resolving;
} clash_resolver;
void
InitClashResolver(clash_resolver *ClashResolver)
{
ClashResolver->Book[0] = InitBook(sizeof(clash_entry), 8);
ClashResolver->Book[1] = InitBook(sizeof(clash_entry), 8);
ClashResolver->Main = &ClashResolver->Book[0];
ClashResolver->Holder = &ClashResolver->Book[1];
ClashResolver->Chain = InitBookOfPointers(8);
ClashResolver->ChainStructure = CS_OPEN_ENDED;
ClashResolver->Resolving = FALSE;
}
void
ResetClashResolver(clash_resolver *R)
{
FreeAndReinitialiseBook(&R->Book[0]);
FreeAndReinitialiseBook(&R->Book[1]);
R->Main = &R->Book[0];
R->Holder = &R->Book[1];
FreeAndReinitialiseBook(&R->Chain);
R->ChainStructure = CS_OPEN_ENDED;
R->Resolving = FALSE;
}
clash_entry *
GetClash(memory_book *C, project *Project, string CandidateBaseFilename)
{
clash_entry *Result = 0;
for(int i = 0; i < C->ItemCount; ++i)
{
clash_entry *This = GetPlaceInBook(C, i);
if(Project == This->Project && StringsMatch(CandidateBaseFilename, Wrap0i(This->CandidateBaseFilename)))
{
Result = This;
break;
}
}
return Result;
}
clash_entry *
GetFellowCandidateOf(memory_book *C, clash_entry *Candidate)
{
clash_entry *Result = 0;
string IncumbentBaseFilename = Wrap0i(Candidate->IncumbentBaseFilename);
for(int i = 0; i < C->ItemCount; ++i)
{
clash_entry *Fellow = GetPlaceInBook(C, i);
if(Candidate != Fellow
&& Candidate->Project == Fellow->Project
&& (Fellow->Status < RS_PENDING || Fellow->Status == RS_REPORTED)
&& StringsMatch(IncumbentBaseFilename, Wrap0i(Fellow->IncumbentBaseFilename)))
{
Result = Fellow;
break;
}
}
return Result;
}
clash_entry *
GetClashCandidateOf(memory_book *C, clash_entry *Incumbent)
{
clash_entry *Result = 0;
string CurrentOutputValue = Wrap0i(Incumbent->CurrentOutputValue);
for(int i = 0; i < C->ItemCount; ++i)
{
clash_entry *Candidate = GetPlaceInBook(C, i);
if(Incumbent != Candidate
&& Candidate->Project == Incumbent->Project
&& !Candidate->Vacating
&& (Candidate->Status < RS_PENDING || Candidate->Status == RS_REPORTED)
&& StringsMatch(CurrentOutputValue, Wrap0i(Candidate->DesiredOutputValue)))
{
Result = Candidate;
break;
}
}
return Result;
}
clash_entry *
GetClashIncumbentOf(memory_book *C, clash_entry *Candidate)
{
clash_entry *Result = 0;
string IncumbentBaseFilename = Wrap0i(Candidate->IncumbentBaseFilename);
for(int i = 0; i < C->ItemCount; ++i)
{
clash_entry *Incumbent = GetPlaceInBook(C, i);
if(Candidate != Incumbent
&& Candidate->Project == Incumbent->Project
&& (Incumbent->Status < RS_PENDING || Incumbent->Status == RS_REPORTED)
&& StringsMatch(IncumbentBaseFilename, Wrap0i(Incumbent->CandidateBaseFilename)))
{
Result = Incumbent;
break;
}
}
return Result;
}
typedef enum
{
CR_FELLOW,
CR_INCUMBENT,
} clash_relationship;
typedef struct
{
clash_relationship Relationship;
clash_entry *Entry;
} related_clash_entry;
related_clash_entry
GetFellowOrClashIncumbentOf(memory_book *C, clash_entry *Candidate)
{
related_clash_entry Result = {};
Result.Entry = GetFellowCandidateOf(C, Candidate);
if(!Result.Entry) { Result.Entry = GetClashIncumbentOf(C, Candidate); Result.Relationship = CR_INCUMBENT; }
return Result;
}
void
AbsolveIncumbentOf(memory_book *C, clash_entry *Candidate)
{
clash_entry *Incumbent = GetClashIncumbentOf(C, Candidate);
if(Incumbent)
{
Incumbent->Status = RS_NOT_STARTED;
if(!GetFellowCandidateOf(C, Candidate) && Wrap0i(Incumbent->IncumbentBaseFilename).Length == 0)
{
Incumbent->Status = RS_RESOLVED;
}
Clear(Candidate->IncumbentBaseFilename, sizeof(Candidate->IncumbentBaseFilename));
}
}
void
VacatePotentialClash(memory_book *C, project *Project, string OutgoingIncumbentBaseFilename)
{
clash_entry *OutgoingIncumbent = GetClash(C, Project, OutgoingIncumbentBaseFilename);
if(OutgoingIncumbent)
{
AbsolveIncumbentOf(C, OutgoingIncumbent);
OutgoingIncumbent->Vacating = TRUE;
}
}
void
ResolveClash(memory_book *C, project *Project, string BaseFilename)
{
clash_entry *This = GetClash(C, Project, BaseFilename);
if(This)
{
AbsolveIncumbentOf(C, This);
if(!GetClashCandidateOf(C, This))
{
This->Status = RS_RESOLVED;
}
else
{
This->Status = RS_NOT_STARTED;
}
}
}
void
PushClash(memory_book *C, project *Project, db_entry *IncumbentEntry, string CandidateBaseFilename, string CurrentOutputValue, string DesiredOutputValue)
{
clash_entry *This = GetClash(C, Project, CandidateBaseFilename);
if(!This)
{
This = MakeSpaceInBook(C);
}
else if(StringsDiffer(Wrap0i(This->DesiredOutputValue), DesiredOutputValue))
{
AbsolveIncumbentOf(C, This);
}
This->Project = Project;
ClearCopyStringNoFormatOrTerminate(This->CandidateBaseFilename, sizeof(This->CandidateBaseFilename), CandidateBaseFilename);
ClearCopyStringNoFormatOrTerminate(This->CurrentOutputValue, sizeof(This->CurrentOutputValue), CurrentOutputValue);
ClearCopyStringNoFormatOrTerminate(This->DesiredOutputValue, sizeof(This->DesiredOutputValue), DesiredOutputValue);
ClearCopyStringNoFormatOrTerminate(This->IncumbentBaseFilename, sizeof(This->IncumbentBaseFilename), Wrap0i(IncumbentEntry->HMMLBaseFilename));
This->Status = RS_NOT_STARTED;
if(IncumbentEntry)
{
string IncumbentBaseFilename = Wrap0i(IncumbentEntry->HMMLBaseFilename);
clash_entry *Incumbent = GetClash(C, Project, IncumbentBaseFilename);
if(!Incumbent)
{
Incumbent = MakeSpaceInBook(C);
Incumbent->Project = Project;
ClearCopyStringNoFormatOrTerminate(Incumbent->CandidateBaseFilename, sizeof(Incumbent->CandidateBaseFilename), IncumbentBaseFilename);
string IncumbentOutput = Wrap0i(IncumbentEntry->OutputLocation);
ClearCopyStringNoFormatOrTerminate(Incumbent->CurrentOutputValue, sizeof(Incumbent->CurrentOutputValue), IncumbentOutput);
ClearCopyStringNoFormatOrTerminate(Incumbent->DesiredOutputValue, sizeof(Incumbent->DesiredOutputValue), IncumbentOutput);
}
else if(Incumbent->Status != RS_REPORTED)
{
Incumbent->Status = RS_NOT_STARTED;
}
}
}
clash_entry **
PushClashPtr(memory_book *C, clash_entry *E)
{
clash_entry **New = MakeSpaceInBook(C);
*New = E;
return New;
}
#if DEBUG_CLASH_RESOLVER
void
PrintClash(memory_book *C, clash_entry *E)
{
PrintC(CS_CYAN, ResolutionStatusStrings[E->Status]);
PrintC(E->Vacating ? CS_GREEN : CS_RED, " V");
PrintC(E->Superseding ? CS_GREEN : CS_RED, " S ");
PrintLineage(E->Project->Lineage, FALSE);
Print(stderr, "/");
PrintStringC(CS_MAGENTA, Wrap0i(E->CandidateBaseFilename));
Print(stderr, ": ");
string CurrentOutputValue = Wrap0i(E->CurrentOutputValue);
string DesiredOutputValue = Wrap0i(E->DesiredOutputValue);
if(CurrentOutputValue.Length > 0)
{
PrintStringC(E->Vacating ? CS_BLACK_BOLD : CS_WHITE, CurrentOutputValue);
}
else
{
PrintStringC(CS_YELLOW, Wrap0("(null)"));
}
if(
#if 0
!E->Vacating && StringsDiffer(CurrentOutputValue, DesiredOutputValue)
#else
1
#endif
)
{
Print(stderr, "");
string IncumbentBaseFilename = Wrap0i(E->IncumbentBaseFilename);
clash_entry *Incumbent = GetClash(C, E->Project, IncumbentBaseFilename);
if(Incumbent
#if 0
&& !Incumbent->Vacating
#else
|| IncumbentBaseFilename.Length > 0
#endif
)
{
PrintStringC(CS_RED_BOLD, DesiredOutputValue);
Print(stderr, " (");
PrintStringC(CS_MAGENTA, IncumbentBaseFilename);
Print(stderr, ")");
}
else
{
PrintStringC(CS_GREEN_BOLD, DesiredOutputValue);
}
}
}
void
PrintClashes(memory_book *C, int LineNumber)
{
if(C->ItemCount)
{
Print(stderr, "\n");
PrintLinedFunctionName(LineNumber, "PrintClashes()");
}
for(int i = 0; i < C->ItemCount; ++i)
{
clash_entry *This = GetPlaceInBook(C, i);
PrintClash(C, This);
Print(stderr, "\n");
}
}
void
PrintClashChain(clash_resolver *R)
{
if(R->Chain.ItemCount)
{
}
for(int i = 0; i < R->Chain.ItemCount; ++i)
{
clash_entry **This = GetPlaceInBook(&R->Chain, i);
Colourise(CS_BLACK_BOLD);
Print(stderr, " [%i] ", i);
Colourise(CS_END);
PrintClash(R->Main, *This);
Print(stderr, "\n");
}
}
#endif
//
// NOTE(matt): Clash Detection
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.ItemCount; ++i)
{
asset *Asset = GetPlaceInBook(&Assets, i);
if(PageType == PAGE_PLAYER)
{
for(int LandmarkIndex = 0; LandmarkIndex < Asset->Player.ItemCount; ++LandmarkIndex)
{
landmark *This = GetPlaceInBook(&Asset->Player, LandmarkIndex);
if(This->BufferID == Src->ID)
{
This->Offset += Dest->Ptr - Dest->Location;
This->BufferID = Dest->ID;
}
}
}
else
{
for(int LandmarkIndex = 0; LandmarkIndex < Asset->Search.ItemCount; ++LandmarkIndex)
{
landmark *This = GetPlaceInBook(&Asset->Search, LandmarkIndex);
if(This->BufferID == Src->ID)
{
This->Offset += Dest->Ptr - Dest->Location;
This->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)
{
Print(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);
Dest->Ptr += CopyBytes(Dest->Ptr, Src->Location, Src->Ptr - Src->Location);
*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)
{
Print(stderr, "CopyBufferSized(%s) call on line %d cannot accommodate %ld-character %s\n", BufferIDStrings[Dest->ID], LineNumber, Size, BufferIDStrings[Src->ID]);
__asm__("int3");
}
Dest->Ptr += CopyBytes(Dest->Ptr, Src->Location, Size);
}
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));
}
void
AppendInt64ToBuffer(buffer *B, int64_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, "%li", I);
CopyStringToBufferNoFormat(B, Wrap0(Temp));
}
void
AppendUint64ToBuffer(buffer *B, uint64_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, "%li", 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);
int wordexpResult = wordexp(WorkingPath, &Expansions, Flags);
if(wordexpResult)
{
switch(wordexpResult)
{
case WRDE_BADCHAR:
{
SystemError(0, 0, S_ERROR, "wordexp: Illegal occurrence of newline or one of |, &, ;, <, >, (, ), {, } in: ", &Path);
} break;
case WRDE_BADVAL:
{
SystemError(0, 0, S_ERROR, "wordexp: An undefined shell variable was referenced, and the WRDE_UNDEF flag told us to consider this an error, in: ", &Path);
} break;
case WRDE_CMDSUB:
{
SystemError(0, 0, S_ERROR, "wordexp: Command substitution requested, but the WRDE_NOCMD flag told us to consider this an error, in: ", &Path);
} break;
case WRDE_NOSPACE:
{
SystemError(0, 0, S_ERROR, "wordexp: Out of memory", 0);
} break;
case WRDE_SYNTAX:
{
SystemError(0, 0, S_ERROR, "wordexp: Shell syntax error, such as unbalanced parentheses or unmatched quotes, in: ", &Path);
} break;
}
}
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);
if(Result)
{
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, template_tag_code TagCode, asset *Asset, navigation_buffer *NavBuffer)
{
tag_offset *This = MakeSpaceInBook(&Template->Metadata.Tags);
This->Offset = Offset;
This->TagCode = TagCode;
This->Asset = Asset;
This->Nav = NavBuffer;
}
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, FALSE);
}
else
{
if(Location.Base[0] == '/')
{
Template->File = InitFile(0, &Location, EXT_NULL, FALSE);
}
else
{
Template->File = InitFile(Type == TEMPLATE_BESPOKE ? &CurrentProject->HMMLDir : &CurrentProject->TemplatesDir, &Location, EXT_NULL, FALSE);
}
}
Print(stderr, "%sPacking%s template: %s\n", ColourStrings[CS_ONGOING], ColourStrings[CS_END], Template->File.Path);
ReadFileIntoBuffer(&Template->File);
ClearTemplateMetadata(Template);
Template->Metadata.Tags = InitBook(sizeof(tag_offset), 16);
Template->Metadata.NavBuffer = InitBook(sizeof(navigation_buffer), 4);
}
v4
V4(int A, int B, int C, int D)
{
v4 Result = { .A = A, .B = B, .C = C, .D = D };
return Result;
}
float
TimecodeToDottedSeconds(v4 Timecode)
{
return (float)Timecode.Hours * SECONDS_PER_HOUR + (float)Timecode.Minutes * SECONDS_PER_MINUTE + (float)Timecode.Seconds + (float)Timecode.Milliseconds / 1000;
}
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;
}
}
typedef struct
{
hsl_colour Colour;
person *Person;
bool Seen;
} speaker;
typedef struct
{
_memory_book(speaker) Speakers;
abbreviation_scheme AbbrevScheme;
} speakers;
enum
{
CreditsError_NoCredentials,
CreditsError_NoHost,
CreditsError_NoIndexer,
CreditsError_NoRole,
} 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)
{
CloseFile(File, FALSE);
// 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
CloseFile(&File->File, FALSE);
// 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)
{
Print(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->Index.Project, Landmark->Index.Project) && FirstLandmarkOfRange->Index.Entry == Landmark->Index.Entry)
{
++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->Index.Project, Landmark->Index.Project) && LandmarkInRange->Index.Entry == Landmark->Index.Entry)
{
--Result.First;
--Landmark;
}
if(LandmarkInRange->Index.Entry != Landmark->Index.Entry)
{
++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->Index.Entry)
{
Result.First = Lower;
Result.Length = 0;
return Result;
}
// TODO(matt): Is there a slicker way of doing this?
if(EntryIndex > UpperLandmark->Index.Entry)
{
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->Index.Entry)
{
return GetIndexRange(Asset, ProjectRange, Lower);
}
if(EntryIndex == PivotLandmark->Index.Entry)
{
return GetIndexRange(Asset, ProjectRange, Pivot);
}
if(EntryIndex == UpperLandmark->Index.Entry)
{
return GetIndexRange(Asset, ProjectRange, Upper);
}
if(EntryIndex < PivotLandmark->Index.Entry) { 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)
{
Print(stderr, "%s%2i:%2i%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, uint16_t *Index)
{
if(Index)
{
Colourise(CS_BLACK_BOLD);
Print(stderr, "[%4u] ", *Index);
Colourise(CS_END);
}
PrintEntryIndex(L->Index.Project, L->Index.Entry);
Print(stderr, " %6u", L->Position);
}
void
PrintAsset(db_asset *A, uint16_t *Index)
{
if(Index)
{
Colourise(CS_BLACK_BOLD);
Print(stderr, "[%4u]", *Index);
Colourise(CS_END);
}
string FilenameL = Wrap0i(A->Filename);
Print(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)
{
Print(stderr, " ");
}
}
Print(stderr, " Associated: ");
if(Associated) { PrintC(CS_GREEN, "true"); }
else { PrintC(CS_RED, "false"); }
Print(stderr, " • Variants: ");
Colourise(CS_BLUE_BOLD);
Print(stderr, "%lu", Variants);
Colourise(CS_END);
Print(stderr, " • Dimensions: ");
Colourise(CS_BLUE_BOLD);
Print(stderr, "%u", A->Width);
Print(stderr, "×");
Print(stderr, "%u", A->Height);
Colourise(CS_END);
Print(stderr, "\n");
}
}
void
PrintAssetAndLandmarks(db_asset *A, uint16_t *Index)
{
Print(stderr, "\n"
"\n");
PrintAsset(A, Index);
db_landmark *FirstLandmark = LocateFirstLandmark(A);
for(uint16_t i = 0; i < A->LandmarkCount; ++i)
{
db_landmark *This = FirstLandmark + i;
if((i % 8) == 0)
{
Print(stderr, "\n");
}
else
{
PrintC(CS_BLACK_BOLD, "");
}
PrintLandmark(This, &i);
}
Print(stderr, "\n");
}
// TODO(matt): Almost definitely redo this using Locate*() functions...
void
SnipeChecksumAndCloseFile(file *HTMLFile, db_asset *Asset, int LandmarksInFile, asset_hash 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;
char ChecksumString[16];
ClearCopyString(ChecksumString, sizeof(ChecksumString), "%08x", Checksum);
CopyStringToBufferNoTerminate(&HTMLFile->Buffer, Wrap0(ChecksumString));
}
HTMLFile->Handle = fopen(HTMLFile->Path, "w");
fwrite(HTMLFile->Buffer.Location, HTMLFile->Buffer.Size, 1, HTMLFile->Handle);
FreeFile(HTMLFile, NA);
}
void *
SkipHeaderedSection(void *LocationInDB, db_structure_headered_section *Structure)
{
char *Ptr = LocationInDB;
uint64_t Count = ReadDBField(LocationInDB, Structure->EntryCount);
Ptr += Structure->Self.SizeOf + Structure->Entry.SizeOf * Count;
return Ptr;
}
void *
SkipHeaderedSectionRecursively(void *LocationInDB, db_structure_headered_section *Structure)
{
uint64_t ChildCount = ReadDBField(LocationInDB, Structure->ChildCount);
char *Ptr = SkipHeaderedSection(LocationInDB, Structure);
for(int i = 0; i < ChildCount; ++i)
{
Ptr = SkipHeaderedSectionRecursively(Ptr, Structure);
}
return Ptr;
}
typedef struct
{
uint64_t CurrentGeneration;
_memory_book(uint32_t) EntriesInGeneration;
} project_generations;
db_project_index
GetCurrentProjectIndex(project_generations *G)
{
db_project_index Result = {};
Result.Generation = G->CurrentGeneration;
Result.Index = *(uint32_t *)GetPlaceInBook(&G->EntriesInGeneration, G->CurrentGeneration);
return Result;
}
void
AddEntryToGeneration(project_generations *G, project *P)
{
if(G)
{
if(G->EntriesInGeneration.ItemCount <= G->CurrentGeneration)
{
MakeSpaceInBook(&G->EntriesInGeneration);
}
if(P)
{
P->Index = GetCurrentProjectIndex(G);
}
uint32_t *This = GetPlaceInBook(&G->EntriesInGeneration, G->CurrentGeneration);
++*This;
}
}
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->EntriesInGeneration.ItemCount; ++i)
{
uint32_t *This = GetPlaceInBook(&G->EntriesInGeneration, i);
Print(stderr, "%lu: %u%s\n", i, *This, IndicateCurrentGeneration && i == G->CurrentGeneration ? " [Current Generation]" : "");
}
}
void *
SkipBlock(void *Block)
{
void *Result = 0;
uint32_t FirstInt = *(uint32_t *)Block;
db_structure_block *BlockStructure = GetDBStructureBlock(FirstInt);
Assert(BlockStructure);
Result = (char *)Block;
Result += BlockStructure->SizeOf;
uint64_t Count = ReadDBField(Block, BlockStructure->Count);
if(BlockStructure->Entry.SizeOf)
{
Result += BlockStructure->Entry.SizeOf * Count;
}
else
{
for(int i = 0; i < Count; ++i)
{
Result = SkipHeaderedSectionRecursively(Result, &BlockStructure->HeaderedSection);
}
}
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 *Result = 0;
db_header_project *Child = LocateFirstChildProject(Header);
for(int i = 0; i < Header->ChildCount; ++i)
{
if(Accumulator->Generation == DesiredProject->Generation)
{
if(Accumulator->Index == DesiredProject->Index)
{
Result = Child;
break;
}
else
{
++Accumulator->Index;
}
}
++Accumulator->Generation;
db_header_project *Test = LocateProjectRecursively(Child, DesiredProject, Accumulator);
if(Test)
{
Result = Test;
break;
}
--Accumulator->Generation;
Child = SkipHeaderedSectionRecursively(Child, GetDBStructureHeaderedSection(B_PROJ));
}
return Result;
}
db_structure *
GetDBStructure(uint32_t Version)
{
db_structure *Result = 0;
for(int i = 0; i < DBStructures.Structure.ItemCount; ++i)
{
db_structure *This = GetPlaceInBook(&DBStructures.Structure, i);
if(This->Version == Version)
{
Result = This;
break;
}
}
return Result;
}
db_header_project *
LocateProject(db_project_index Project)
{
db_header_project *Result = 0;
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)
{
db_structure_headered_section *DBStructureProjects = GetDBStructureHeaderedSection(B_PROJ);
if(Project.Generation == Accumulator.Generation)
{
for(int i = 0; i < ProjectsBlock->Count; ++i)
{
if(Project.Index == Accumulator.Index)
{
Result = Child;
break;
}
Child = SkipHeaderedSectionRecursively(Child, DBStructureProjects);
++Accumulator.Index;
}
}
else
{
++Accumulator.Generation;
for(int i = 0; i < ProjectsBlock->Count; ++i)
{
db_header_project *Test = LocateProjectRecursively(Child, &Project, &Accumulator);
if(Test)
{
Result = Test;
break;
}
Child = SkipHeaderedSectionRecursively(Child, DBStructureProjects);
}
--Accumulator.Generation;
}
}
}
return Result;
}
db_entry *
LocateFirstEntry(db_header_project *P)
{
char *Ptr = (char *)P;
Ptr += sizeof(db_header_project);
return (db_entry *)Ptr;
}
db_entry *
LocateEntryOfProject(db_header_project *Project, int32_t EntryIndex)
{
db_entry *Result = 0;
if(Project && EntryIndex < Project->EntryCount)
{
char *Ptr = (char *)Project;
Ptr += sizeof(db_header_project) + sizeof(db_entry) * EntryIndex;
Result = (db_entry *)Ptr;
}
return Result;
}
db_entry *
LocateEntry(db_project_index DBProjectIndex, int32_t EntryIndex)
{
return LocateEntryOfProject(LocateProject(DBProjectIndex), EntryIndex);
}
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;
}
FILE *
OpenFileForWriting(file *F)
{
if(F->Locking && F->Handle)
{
fclose(F->Handle);
}
F->Handle = fopen(F->Path, "w");
TryLock(F);
return F->Handle;
}
// 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;
OpenFileForWriting(&DB.Metadata.File);
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
PrintLinedFunctionName(LineNumber, "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 = SkipHeaderedSectionRecursively(A, GetDBStructureHeaderedSection(B_ASET));
}
return A;
}
char *
ConstructDirectoryPath(string *BaseDir, string *PageLocation, string *EntryOutput)
{
char *Result = 0;
if(BaseDir)
{
ExtendString0(&Result, *BaseDir);
}
if(PageLocation && PageLocation->Length > 0)
{
if(BaseDir)
{
ExtendString0(&Result, Wrap0("/"));
}
ExtendString0(&Result, *PageLocation);
}
if(EntryOutput)
{
ExtendString0(&Result, Wrap0("/"));
ExtendString0(&Result, *EntryOutput);
}
return Result;
}
char *
ConstructHTMLIndexFilePath(string *BaseDir, string *PageLocation, string *EntryOutput)
{
char *Result = ConstructDirectoryPath(BaseDir, PageLocation, EntryOutput);
ExtendString0(&Result, Wrap0("/index.html"));
return Result;
}
rc
ReadSearchPageIntoBuffer(file *File, string *BaseDir, string *SearchLocation)
{
File->Path = ConstructHTMLIndexFilePath(BaseDir, SearchLocation, 0);
return ReadFileIntoBuffer(File);
}
string
GetGlobalSearchPageLocation(void)
{
string Result = {};
db_block_projects *ProjectsBlock = DB.Metadata.Signposts.ProjectsBlock.Ptr ? DB.Metadata.Signposts.ProjectsBlock.Ptr : LocateBlock(B_PROJ);
Result = Wrap0i(ProjectsBlock->GlobalSearchDir);
return Result;
}
rc
ReadGlobalSearchPageIntoBuffer(file *File)
{
string SearchLocationL = GetGlobalSearchPageLocation();
File->Path = ConstructHTMLIndexFilePath(0, &SearchLocationL, 0);
return ReadFileIntoBuffer(File);
}
rc
ReadPlayerPageIntoBuffer(file *File, string BaseDir, string PlayerLocation, string OutputLocation)
{
File->Path = ConstructHTMLIndexFilePath(&BaseDir, &PlayerLocation, &OutputLocation);
return ReadFileIntoBuffer(File);
}
rc
SnipeChecksumIntoHTML(db_asset *Asset, asset_hash 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->Index.Project);
file HTML = {};
rc FileReadRC = RC_INIT;
if(Landmark->Index.Entry >= 0)
{
db_entry *Entry = LocateEntry(Landmark->Index.Project, Landmark->Index.Entry);
if(Entry)
{
string BaseDir = Wrap0i(P->BaseDir);
string PlayerLocation = Wrap0i(P->PlayerLocation);
string OutputLocation = Wrap0i(Entry->OutputLocation);
FileReadRC = ReadPlayerPageIntoBuffer(&HTML, BaseDir, PlayerLocation, OutputLocation);
}
else
{
PrintC(CS_ERROR, "\nInvalid landmark (aborting update): ");
PrintAsset(Asset, 0);
PrintLandmark(Landmark, 0);
Result = RC_FAILURE;
break;
}
}
else
{
switch(Landmark->Index.Entry)
{
case SP_SEARCH:
{
if(P)
{
string BaseDir = Wrap0i(P->BaseDir);
string SearchLocation = Wrap0i(P->SearchLocation);
FileReadRC = ReadSearchPageIntoBuffer(&HTML, &BaseDir, &SearchLocation);
}
else
{
FileReadRC = ReadGlobalSearchPageIntoBuffer(&HTML);
}
} break;
default:
{
Colourise(CS_RED);
Print(stderr, "SnipeChecksumIntoHTML() does not know about special page with index %i\n", Landmark->Index.Entry);
Colourise(CS_END);
Assert(0);
} break;
}
}
if(FileReadRC == RC_SUCCESS)
{
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)
{
Print(stderr, "%s", WatchTypeStrings[W->Type]);
Print(stderr, "");
if(W->Extension != EXT_NULL)
{
Print(stderr, "*");
PrintString(ExtensionStrings[W->Extension]);
}
else
{
PrintString(W->Path);
}
if(W->Project)
{
Print(stderr, "");
PrintLineage(W->Project->Lineage, FALSE);
}
if(W->Asset)
{
Print(stderr, "");
Print(stderr, "%s", AssetTypeNames[W->Asset->Type]);
}
}
void
PrintWatchHandle(watch_handle *W)
{
Print(stderr, "%4d", W->Descriptor);
Print(stderr, "");
if(StringsDiffer(W->TargetPath, W->WatchedPath))
{
PrintStringC(CS_MAGENTA, W->WatchedPath);
Print(stderr, "\n ");
PrintStringC(CS_RED_BOLD, W->TargetPath);
}
else
{
PrintStringC(CS_GREEN_BOLD, W->TargetPath);
}
for(int i = 0; i < W->Files.ItemCount; ++i)
{
watch_file *This = GetPlaceInBook(&W->Files, i);
Print(stderr, "\n"
" ");
PrintWatchFile(This);
}
}
#define PrintWatchHandles() PrintWatchHandles_(__LINE__)
void
PrintWatchHandles_(int LineNumber)
{
typography T =
{
.UpperLeftCorner = "",
.UpperLeft = "",
.Horizontal = "",
.UpperRight = "",
.Vertical = "",
.LowerLeftCorner = "",
.LowerLeft = "",
.Margin = " ",
.Delimiter = ": ",
.Separator = "",
};
Print(stderr, "\n"
"%s%s%s [%i] PrintWatchHandles()", T.UpperLeftCorner, T.Horizontal, T.UpperRight, LineNumber);
for(int i = 0; i < WatchHandles.Handles.ItemCount; ++i)
{
watch_handle *This = GetPlaceInBook(&WatchHandles.Handles, i);
Print(stderr, "\n");
PrintWatchHandle(This);
}
Print(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;
}
watch_file *
PushWatchFileUniquely(watch_handle *Handle, string Filepath, extension_id Extension, watch_type Type, project *Project, asset *Asset)
{
watch_file *Result = 0;
bool Required = TRUE;
for(int i = 0; i < Handle->Files.ItemCount; ++i)
{
watch_file *This = GetPlaceInBook(&Handle->Files, i);
if(This->Type == Type)
{
if(Extension != EXT_NULL)
{
if(This->Extension == Extension)
{
Result = This;
Required = FALSE;
break;
}
}
else if(StringsMatch(This->Path, Filepath))
{
Result = This;
EraseCurrentStringFromBook(&WatchHandles.Paths);
Required = FALSE;
break;
}
}
}
if(Required)
{
watch_file *New = MakeSpaceInBook(&Handle->Files);
if(Extension != EXT_NULL)
{
New->Extension = Extension;
}
else
{
New->Path = Filepath;
}
New->Type = Type;
New->Project = Project;
New->Asset = Asset;
Result = New;
}
return Result;
}
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))
{
int PathLength = readlink(OriginalPath0, ResolvedSymlinkPath, 4096);
FullPath = Wrap0i_(ResolvedSymlinkPath, PathLength);
}
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;
}
watch_file *
PushWatchHandle(string Path, extension_id Extension, watch_type Type, project *Project, asset *Asset)
{
watch_file *Result = 0;
// 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.Handles.ItemCount; ++i)
{
watch_handle *This = GetPlaceInBook(&WatchHandles.Handles, i);
if(StringsMatch(TargetPath, This->TargetPath))
{
Watch = This;
EraseCurrentStringFromBook(&WatchHandles.Paths);
break;
}
}
if(!Watch)
{
Watch = MakeSpaceInBook(&WatchHandles.Handles);
Watch->Files = InitBook(sizeof(watch_file), 16);
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);
//PrintWatchHandles();
}
Result = PushWatchFileUniquely(Watch, Filename, Extension, Type, Project, Asset);
return Result;
}
bool
DescriptorIsRedundant(int Descriptor)
{
bool Result = TRUE;
for(int i = 0; i < WatchHandles.Handles.ItemCount; ++i)
{
watch_handle *This = GetPlaceInBook(&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.Handles.ItemCount; ++i)
{
watch_handle *This = GetPlaceInBook(&WatchHandles.Handles, i);
if(StringsMatch(Path, This->WatchedPath))
{
Result = This->Descriptor;
break;
}
}
return Result;
}
void
UpdateWatchHandles(int Descriptor)
{
for(int i = 0; i < WatchHandles.Handles.ItemCount; ++i)
{
watch_handle *This = GetPlaceInBook(&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);
string FilenameInMemory = Wrap0i(Asset->Filename);
if(StringsMatch(FilenameInDB, FilenameInMemory) && StoredAsset->Type == Asset->Type)
{
*Index = i;
Result = StoredAsset;
break;
}
Ptr = SkipHeaderedSectionRecursively(StoredAsset, GetDBStructureHeaderedSection(B_ASET));
}
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;
}
void
WriteEntireDatabase(neighbourhood *N)
{
// NOTE(matt): This may suffice when the only changes are to existing fixed-size variables,
// e.g. ProjectsBlock->GlobalSearchDir
OpenFileForWriting(&DB.Metadata.File);
fwrite(DB.Metadata.File.Buffer.Location, DB.Metadata.File.Buffer.Size, 1, DB.Metadata.File.Handle);
CycleSignpostedFile(&DB.Metadata);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
}
int
UpdateAssetInDB(asset *Asset)
{
MEM_TEST_TOP();
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)
{
if(StoredAsset->Associated != Asset->Associated ||
StoredAsset->Variants != Asset->Variants ||
StoredAsset->Width != Asset->Dimensions.Width ||
StoredAsset->Height != Asset->Dimensions.Height)
{
StoredAsset->Associated = Asset->Associated;
StoredAsset->Variants = Asset->Variants;
StoredAsset->Width = Asset->Dimensions.Width;
StoredAsset->Height = Asset->Dimensions.Height;
}
if(StoredAsset->Hash != Asset->Hash)
{
// NOTE(matt): Extra-db code start
char OldChecksum[16];
ClearCopyString(OldChecksum, sizeof(OldChecksum), "%08x", StoredAsset->Hash);
char NewChecksum[16];
ClearCopyString(NewChecksum, sizeof(NewChecksum), "%08x", Asset->Hash);
// NOTE(matt): Extra-db code end
StoredAsset->Hash = Asset->Hash;
// NOTE(matt): Extra-db code start
file AssetFile = {};
AssetFile.Path = ConstructAssetPath(&AssetFile, Wrap0i(StoredAsset->Filename), StoredAsset->Type);
ResolvePath(&AssetFile.Path);
string MessageEditType = MakeString("sss", ColourStrings[CS_ONGOING], "Updating", ColourStrings[CS_END]);
string Message = MakeString("sssssssssss",
" checksum ",
ColourStrings[CS_BLACK_BOLD], OldChecksum, ColourStrings[CS_END],
"",
ColourStrings[CS_BLUE_BOLD], NewChecksum, ColourStrings[CS_END],
" of ", AssetFile.Path, " in HTML files");
Print(stderr, "%.*s%.*s", (int)MessageEditType.Length, MessageEditType.Base, (int)Message.Length, Message.Base);
uint64_t MessageLength = MessageEditType.Length + Message.Length;
if(SnipeChecksumIntoHTML(StoredAsset, Asset->Hash) == RC_SUCCESS)
{
ClearTerminalRow(MessageLength);
PrintC(CS_REINSERTION, "Updated");
Print(stderr, "%.*s\n", (int)Message.Length, Message.Base);
}
FreeString(&Message);
// NOTE(matt): Extra-db code end
OpenFileForWriting(&DB.Metadata.File);
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
OpenFileForWriting(&DB.Metadata.File);
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;
ClearCopyStringNoFormatOrTerminate(StoredAsset.Filename, sizeof(StoredAsset.Filename), Wrap0i(Asset->Filename));
fwrite(&StoredAsset, sizeof(StoredAsset), 1, DB.Metadata.File.Handle);
AccumulateFileEditSize(&DB.Metadata, sizeof(StoredAsset));
Print(stderr, "%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;
}
}
MEM_TEST_END();
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)
{
landmark *This = MakeSpaceInBook(&Asset->Player);
This->Offset = Dest->Ptr - Dest->Location;
This->BufferID = Dest->ID;
}
else
{
landmark *This = MakeSpaceInBook(&Asset->Search);
This->Offset = Dest->Ptr - Dest->Location;
This->BufferID = Dest->ID;
}
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.ItemCount; ++AssetIndex)
{
asset *A = GetPlaceInBook(&Assets, AssetIndex);
FreeAndReinitialiseBook(&A->Player);
FreeAndReinitialiseBook(&A->Search);
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), 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, NA);
return AssetIndexInDB;
}
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, 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.ItemCount && !This->Known)
{
This->Player = InitBook(sizeof(landmark), 8);
This->Search = InitBook(sizeof(landmark), 8);
++Assets.ItemCount;
}
file File = {};
File.Path = ConstructAssetPath(&File, Filename, Type);
ReadFileIntoBuffer(&File);
if(File.Buffer.Location)
{
// TODO(matt): Print out the asset that we've hashed?
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);
Print(stdout, "%sNonexistent%s %s asset: %s\n", ColourStrings[CS_WARNING], ColourStrings[CS_END], AssetTypeNames[Type], File.Path);
}
FreeFile(&File, NA);
return This;
}
asset *
PushAsset(string Filename, asset_type Type, uint64_t Variants, bool Associated)
{
int i;
for(i = 0; i < Assets.ItemCount; ++i)
{
asset *Asset = GetPlaceInBook(&Assets, i);
if(!StringsDifferLv0(Filename, Asset->Filename) && Type == Asset->Type)
{
break;
}
}
return PlaceAsset(Filename, Type, Variants, Associated, i);
}
void
FreeAssets(memory_book *A)
{
for(int i = 0; i < A->ItemCount; ++i)
{
asset *Asset = GetPlaceInBook(&Assets, i);
FreeAndReinitialiseBook(&Asset->Search);
FreeAndReinitialiseBook(&Asset->Player);
}
FreeBook(A);
}
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)
{
Print(stdout, " %s└───────┴────┴─── Don't worry about this one. We'll generate it if needed%s\n", ColourStrings[CS_COMMENT], ColourStrings[CS_END]);
}
}
Assets.ItemCount = 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)
{
Assets = InitBook(sizeof(asset), 16);
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), Asset->Type, Asset->Variants, Asset->Associated);
Asset = SkipHeaderedSectionRecursively(Asset, GetDBStructureHeaderedSection(B_ASET));
}
}
}
void
ConstructResolvedAssetURL(buffer *Buffer, asset *Asset, page_type 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));
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
PickAbbreviationScheme(speakers *Speakers)
{
Speakers->AbbrevScheme = AS_NONE;
int SchemeCount = 3;
for(int SchemeIndex = 0; SchemeIndex < SchemeCount; ++SchemeIndex)
{
bool Clash = FALSE;
for(int i = 0; i < Speakers->Speakers.ItemCount; ++i)
{
speaker *A = GetPlaceInBook(&Speakers->Speakers, i);
for(int j = i + 1; j < Speakers->Speakers.ItemCount; ++j)
{
speaker *B = GetPlaceInBook(&Speakers->Speakers, j);
if(StringsMatch(A->Person->Abbreviations[SchemeIndex], B->Person->Abbreviations[SchemeIndex]))
{
Clash = TRUE;
break;
}
}
if(Clash) { break; }
}
if(!Clash)
{
Speakers->AbbrevScheme = SchemeIndex;
break;
}
}
}
void
SortSpeakersAndPickAbbreviationScheme(speakers *Speakers)
{
for(int i = 0; i < Speakers->Speakers.ItemCount; ++i)
{
speaker *A = GetPlaceInBook(&Speakers->Speakers, i);
for(int j = i + 1; j < Speakers->Speakers.ItemCount; ++j)
{
speaker *B = GetPlaceInBook(&Speakers->Speakers, j);
if(StringsDiffer(A->Person->ID, B->Person->ID) > 0)
{
person *Temp = B->Person;
B->Person = A->Person;
A->Person = Temp;
break;
}
}
}
for(int i = 0; i < Speakers->Speakers.ItemCount; ++i)
{
speaker *This = GetPlaceInBook(&Speakers->Speakers, i);
StringToColourHash(&This->Colour, This->Person->ID);
}
PickAbbreviationScheme(Speakers);
}
person *
GetPersonFromConfig(string Person)
{
person *Result = 0;
for(int i = 0; i < Config->Person.ItemCount; ++i)
{
person *This = GetPlaceInBook(&Config->Person, i);
if(!StringsDifferCaseInsensitive(This->ID, Person))
{
Result = This;
break;
}
}
return Result;
}
asset *
GetAsset(string Filename, asset_type AssetType)
{
asset *Result = 0;
for(int i = 0; i < Assets.ItemCount; ++i)
{
asset *This = GetPlaceInBook(&Assets, i);
if(StringsMatch(Filename, Wrap0i(This->Filename))
&& AssetType == This->Type)
{
Result = This;
break;
}
}
return Result;
}
void
PushSupportIconAssets()
{
for(int i = 0; i < Config->Person.ItemCount; ++i)
{
person *Person = GetPlaceInBook(&Config->Person, i);
for(int j = 0; j < Person->Support.ItemCount; ++j)
{
support *Support = GetPlaceInBook(&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 SkipHeaderedSectionRecursively(Child, GetDBStructureHeaderedSection(B_PROJ));
}
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 = SkipHeaderedSectionRecursively(This, GetDBStructureHeaderedSection(B_ASET));
}
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), 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->Medium.ItemCount; ++i)
{
medium *This = GetPlaceInBook(&P->Medium, i);
PushMediumIconAsset(This);
}
for(int i = 0; i < P->Child.ItemCount; ++i)
{
PushProjectAssets(GetPlaceInBook(&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->Project.ItemCount; ++i)
{
PushProjectAssets(GetPlaceInBook(&Config->Project, i));
}
}
void
PushConfiguredAssets()
{
PushSupportIconAssets();
PushThemeAssets();
}
// TODO(matt): Remove once we're ready to deprecate the old annotator=person style of crediting
//
char *RoleStrings[] =
{
"Cohost",
"Guest",
"Host",
"Indexer",
};
char *RoleIDStrings[] ={
"cohost",
"guest",
"host",
"indexer",
};
typedef enum
{
R_COHOST,
R_GUEST,
R_HOST,
R_INDEXER,
} role_id;
//
////
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);
}
}
}
speaker *
GetSpeaker(memory_book *Speakers, string Username)
{
speaker *Result = 0;
if(Username.Length > 0)
{
for(int i = 0; i < Speakers->ItemCount; ++i)
{
speaker *This = GetPlaceInBook(Speakers, i);
if(!StringsDifferCaseInsensitive(This->Person->ID, Username))
{
Result = This;
break;
}
}
}
return Result;
}
void
FreeSpeakers(speakers *Speakers)
{
FreeBook(&Speakers->Speakers);
}
void
PushCredentials(buffer *CreditsMenu, memory_book *Speakers, person *Actor, role *Role, bool *RequiresCineraJS)
{
if(!Role->NonSpeaking)
{
speaker *This = GetSpeaker(Speakers, Actor->ID);
if(!This)
{
This = MakeSpaceInBook(Speakers);
This->Person = Actor;
}
}
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");
string Name = Actor->Name.Length > 0 ? Actor->Name : Actor->ID;
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,
(int)Role->Name.Length, Role->Name.Base,
(int)Name.Length, Name.Base);
}
else
{
CopyStringToBuffer(CreditsMenu,
" <div class=\"person\">\n"
" <div class=\"role\">%.*s</div>\n"
" <div class=\"name\">%.*s</div>\n"
" </div>\n",
(int)Role->Name.Length, Role->Name.Base,
(int)Name.Length, Name.Base);
}
// TODO(matt): Handle multiple support platforms!
// AFD
if(Actor->Support.ItemCount > 0)
{
support *Support = GetPlaceInBook(&Actor->Support, 0);
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
ErrorCredentials(string HMMLFilepath, string Actor, role *Role)
{
ErrorFilenameAndLineNumber(&HMMLFilepath, 0, S_ERROR, ED_INDEXING);
Print(stderr, "No credentials for%s%s%.*s%s: %s%.*s%s\n",
ColourStrings[CS_YELLOW_BOLD],
Role ? " " : "", Role ? (int)Role->Name.Length : 0, Role ? Role->Name.Base : "",
ColourStrings[CS_END],
ColourStrings[CS_MAGENTA_BOLD], (int)Actor.Length, Actor.Base, ColourStrings[CS_END]);
Print(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();
}
void
ErrorRole(string HMMLFilepath, string Role)
{
ErrorFilenameAndLineNumber(&HMMLFilepath, 0, S_ERROR, ED_INDEXING);
if(Role.Length == 0)
{
Print(stderr, "Credit lacks a role\n");
Print(stderr, " A complete credit takes the form:\n"
" credit = creditable_person:their_role\n");
}
else
{
Print(stderr, "No such role: %s%.*s%s\n",
ColourStrings[CS_YELLOW_BOLD],
(int)Role.Length, Role.Base,
ColourStrings[CS_END]);
Print(stderr, "Perhaps you'd like to add a new role to your config file, e.g.:\n"
" role = \"%.*s\"\n"
" {\n"
" name = \"Roller\";\n"
" plural = \"Rollae\";\n"
" position = -1;\n"
" }\n", (int)Role.Length, Role.Base);
}
WaitForInput();
}
typedef enum
{
MAT_NULL,
MAT_CREDIT,
MAT_CUSTOM,
} misc_attribute_type;
typedef struct
{
char *ID;
misc_attribute_type Type;
union {
int CustomIndex;
role_id RoleID;
};
} misc_attribute;
// TODO(matt): Migrate away from handling annotator / cohost / guest here
misc_attribute MiscAttributes[] = {
{ "annotator", MAT_CREDIT, { R_INDEXER } },
{ "co-host", MAT_CREDIT, { R_COHOST } },
{ "guest", MAT_CREDIT, { R_GUEST } },
{ "custom0", MAT_CUSTOM, { 0 } },
{ "custom1", MAT_CUSTOM, { 1 } },
{ "custom2", MAT_CUSTOM, { 2 } },
{ "custom3", MAT_CUSTOM, { 3 } },
{ "custom4", MAT_CUSTOM, { 4 } },
{ "custom5", MAT_CUSTOM, { 5 } },
{ "custom6", MAT_CUSTOM, { 6 } },
{ "custom7", MAT_CUSTOM, { 7 } },
{ "custom8", MAT_CUSTOM, { 8 } },
{ "custom9", MAT_CUSTOM, { 9 } },
{ "custom10", MAT_CUSTOM, { 10 } },
{ "custom11", MAT_CUSTOM, { 11 } },
{ "custom12", MAT_CUSTOM, { 12 } },
{ "custom13", MAT_CUSTOM, { 13 } },
{ "custom14", MAT_CUSTOM, { 14 } },
{ "custom15", MAT_CUSTOM, { 15 } },
};
misc_attribute *
GetMiscAttribute(HMML_VideoCustomMetaData *A)
{
misc_attribute *Result = 0;
for(int i = 0; i < ArrayCount(MiscAttributes); ++i)
{
misc_attribute *This = MiscAttributes + i;
if(!StringsDiffer0(This->ID, A->key))
{
Result = This;
break;
}
}
return Result;
}
bool
CreditsMatch(credit *A, credit *B)
{
return A->Person == B->Person && A->Role == B->Role;
}
credit *
GetCredit(memory_book *Credits, credit *C)
{
credit *Result = 0;
for(int i = 0; i < Credits->ItemCount; ++i)
{
credit *This = GetPlaceInBook(Credits, i);
if(CreditsMatch(C, This))
{
Result = This;
break;
}
}
return Result;
}
bool
Uncredited(HMML_VideoMetaData *Metadata, credit *C)
{
bool Result = FALSE;
for(int i = 0; i < Metadata->uncredit_count; ++i)
{
HMML_Credit *This = Metadata->uncredits + i;
if(StringsMatch(C->Person->ID, Wrap0(This->name)) && StringsMatch(C->Role->ID, Wrap0(This->role)))
{
Result = TRUE;
break;
}
}
return Result;
}
bool
IsValidCredit(credit *Credit)
{
return Credit->Person && Credit->Role;
}
void
MergeCredits(memory_book *Credits, project *CurrentProject, HMML_VideoMetaData *Metadata)
{
// TODO(matt): Sort by SortName
for(int ProjectCreditIndex = 0; ProjectCreditIndex < CurrentProject->Credit.ItemCount; ++ProjectCreditIndex)
{
credit *Credit = GetPlaceInBook(&CurrentProject->Credit, ProjectCreditIndex);
if(IsValidCredit(Credit) && !Uncredited(Metadata, Credit) && !GetCredit(Credits, Credit))
{
credit *New = MakeSpaceInBook(Credits);
New->Person = Credit->Person;
New->Role = Credit->Role;
}
}
for(int CreditIndex = 0; CreditIndex < Metadata->credit_count; ++CreditIndex)
{
HMML_Credit *This = Metadata->credits + CreditIndex;
credit Credit = {};
Credit.Person = GetPersonFromConfig(Wrap0(This->name));
Credit.Role = GetRoleByID(Config, Wrap0(This->role));
if(IsValidCredit(&Credit) && !Uncredited(Metadata, &Credit) && !GetCredit(Credits, &Credit))
{
credit *New = MakeSpaceInBook(Credits);
New->Person = Credit.Person;
New->Role = Credit.Role;
}
}
// TODO(matt): Delete this loop once we're ready to deprecate the old annotator=person style of crediting
//
for(int CustomIndex = 0; CustomIndex < Metadata->custom_count; ++CustomIndex)
{
HMML_VideoCustomMetaData *This = Metadata->custom + CustomIndex;
misc_attribute *ThisAttribute = GetMiscAttribute(This);
if(ThisAttribute && ThisAttribute->Type == MAT_CREDIT)
{
credit Credit = {};
Credit.Person = GetPersonFromConfig(Wrap0(This->value));
Credit.Role = GetRoleByID(Config, Wrap0(RoleIDStrings[ThisAttribute->RoleID]));
if(IsValidCredit(&Credit) && !Uncredited(Metadata, &Credit) && !GetCredit(Credits, &Credit))
{
credit *New = MakeSpaceInBook(Credits);
New->Person = Credit.Person;
New->Role = Credit.Role;
}
}
}
}
int
BuildCredits(string HMMLFilepath, buffer *CreditsMenu, HMML_VideoMetaData *Metadata, speakers *Speakers, bool *RequiresCineraJS)
{
memory_book Credits = InitBook(sizeof(credit), 4);
MergeCredits(&Credits, CurrentProject, Metadata);
for(int RoleIndex = 0; RoleIndex < Config->Role.ItemCount; ++RoleIndex)
{
role *Role = GetPlaceInBook(&Config->Role, RoleIndex);
for(int CreditIndex = 0; CreditIndex < Credits.ItemCount; ++CreditIndex)
{
credit *This = GetPlaceInBook(&Credits, CreditIndex);
if(!StringsDiffer(Role->ID, This->Role->ID))
{
PushCredentials(CreditsMenu, &Speakers->Speakers, This->Person, Role, RequiresCineraJS);
}
}
}
FreeBook(&Credits);
// NOTE(matt): As we only cite the speaker when there are a multiple of them, we only
// need to SortSpeakersAndPickAbbreviationScheme() in the same situation
if(Speakers->Speakers.ItemCount > 1)
{
SortSpeakersAndPickAbbreviationScheme(Speakers);
}
if(CreditsMenu->Ptr > CreditsMenu->Location)
{
CopyStringToBuffer(CreditsMenu,
" </div>\n"
" </div>\n");
}
return RC_SUCCESS;
}
void
InsertCategory(_memory_book(category_info) *GlobalTopics, _memory_book(category_info) *LocalTopics, _memory_book(category_info) *GlobalMedia, _memory_book(category_info) *LocalMedia, string Marker, hsl_colour *Colour)
{
medium *Medium = GetMediumFromProject(CurrentProject, Marker);
if(Medium)
{
int MediumIndex;
bool MadeLocalSpace = FALSE;
for(MediumIndex = 0; MediumIndex < LocalMedia->ItemCount; ++MediumIndex)
{
category_info *This = GetPlaceInBook(LocalMedia, MediumIndex);
if(StringsMatch(Medium->ID, This->Marker))
{
return;
}
if((StringsDiffer(Medium->Name, This->WrittenText)) < 0)
{
MakeSpaceInBook(LocalMedia);
MadeLocalSpace = TRUE;
int CategoryCount;
for(CategoryCount = LocalMedia->ItemCount - 1; CategoryCount > MediumIndex; --CategoryCount)
{
category_info *Src = GetPlaceInBook(LocalMedia, CategoryCount - 1);
category_info *Dest = GetPlaceInBook(LocalMedia, CategoryCount);
Dest->Marker = Src->Marker;
Dest->WrittenText = Src->WrittenText;
}
category_info *New = GetPlaceInBook(LocalMedia, CategoryCount);
New->Marker = Medium->ID;
New->WrittenText = Medium->Name;
break;
}
}
if(!MadeLocalSpace) { MakeSpaceInBook(LocalMedia); }
if(MediumIndex == LocalMedia->ItemCount - 1)
{
category_info *New = GetPlaceInBook(LocalMedia, MediumIndex);
New->Marker = Medium->ID;
New->WrittenText = Medium->Name;
}
bool MadeGlobalSpace = FALSE;
for(MediumIndex = 0; MediumIndex < GlobalMedia->ItemCount; ++MediumIndex)
{
category_info *This = GetPlaceInBook(GlobalMedia, MediumIndex);
if(StringsMatch(Medium->ID, This->Marker))
{
return;
}
if((StringsDiffer(Medium->Name, This->WrittenText)) < 0)
{
MakeSpaceInBook(GlobalMedia);
MadeGlobalSpace = TRUE;
int CategoryCount;
for(CategoryCount = GlobalMedia->ItemCount - 1; CategoryCount > MediumIndex; --CategoryCount)
{
category_info *Src = GetPlaceInBook(GlobalMedia, CategoryCount - 1);
category_info *Dest = GetPlaceInBook(GlobalMedia, CategoryCount);
Dest->Marker = Src->Marker;
Dest->WrittenText = Src->WrittenText;
}
category_info *New = GetPlaceInBook(GlobalMedia, CategoryCount);
New->Marker = Medium->ID;
New->WrittenText = Medium->Name;
break;
}
}
if(!MadeGlobalSpace) { MakeSpaceInBook(GlobalMedia); }
if(MediumIndex == GlobalMedia->ItemCount - 1)
{
category_info *New = GetPlaceInBook(GlobalMedia, MediumIndex);
New->Marker = Medium->ID;
New->WrittenText = Medium->Name;
}
}
else
{
bool MadeLocalSpace = FALSE;
int TopicIndex;
for(TopicIndex = 0; TopicIndex < LocalTopics->ItemCount; ++TopicIndex)
{
category_info *This = GetPlaceInBook(LocalTopics, TopicIndex);
if(StringsMatch(Marker, This->Marker))
{
return;
}
if((StringsDiffer(Marker, This->Marker)) < 0)
{
int CategoryCount;
MakeSpaceInBook(LocalTopics);
MadeLocalSpace = TRUE;
for(CategoryCount = LocalTopics->ItemCount - 1; CategoryCount > TopicIndex; --CategoryCount)
{
category_info *Src = GetPlaceInBook(LocalTopics, CategoryCount - 1);
category_info *Dest = GetPlaceInBook(LocalTopics, CategoryCount);
*Dest = *Src;
}
category_info *New = GetPlaceInBook(LocalTopics, CategoryCount);
New->Marker = Marker;
if(Colour)
{
New->Colour = *Colour;
}
break;
}
}
if(!MadeLocalSpace) { MakeSpaceInBook(LocalTopics); }
if(TopicIndex == LocalTopics->ItemCount - 1)
{
category_info *New = GetPlaceInBook(LocalTopics, TopicIndex);
New->Marker = Marker;
if(Colour)
{
New->Colour = *Colour;
}
}
bool MadeGlobalSpace = FALSE;
for(TopicIndex = 0; TopicIndex < GlobalTopics->ItemCount; ++TopicIndex)
{
category_info *This = GetPlaceInBook(GlobalTopics, TopicIndex);
if(StringsMatch(Marker, This->Marker))
{
return;
}
// NOTE(matt): This successfully sorts "nullTopic" at the end, but maybe figure out a more general way to force the
// order of stuff, perhaps blocks of dudes that should sort to the start / end
if(((StringsDiffer(Marker, This->Marker)) < 0 || StringsMatch(This->Marker, Wrap0("nullTopic"))))
{
if(StringsDiffer(Marker, Wrap0("nullTopic"))) // NOTE(matt): This test (with the above || condition) forces nullTopic never to be inserted, only appended
{
MakeSpaceInBook(GlobalTopics);
MadeGlobalSpace = TRUE;
int CategoryCount;
for(CategoryCount = GlobalTopics->ItemCount - 1; CategoryCount > TopicIndex; --CategoryCount)
{
category_info *Src = GetPlaceInBook(GlobalTopics, CategoryCount - 1);
category_info *Dest = GetPlaceInBook(GlobalTopics, CategoryCount);
*Dest = *Src;
}
category_info *New = GetPlaceInBook(GlobalTopics, CategoryCount);
New->Marker = Marker;
if(Colour)
{
New->Colour = *Colour;
}
break;
}
}
}
if(!MadeGlobalSpace) { MakeSpaceInBook(GlobalTopics); }
if(TopicIndex == GlobalTopics->ItemCount - 1)
{
category_info *New = GetPlaceInBook(GlobalTopics, TopicIndex);
New->Marker = Marker;
if(Colour)
{
New->Colour = *Colour;
}
}
}
}
void
BuildTimestampClass(buffer *TimestampClass, _memory_book(category_info) *LocalTopics, _memory_book(category_info) *LocalMedia, string DefaultMedium)
{
category_info *FirstLocalTopic = GetPlaceInBook(LocalTopics, 0);
if(LocalTopics->ItemCount == 1 && StringsMatch(FirstLocalTopic->Marker, Wrap0("nullTopic")))
{
// NOTE(matt): Stack-string
char SanitisedMarker[FirstLocalTopic->Marker.Length + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)FirstLocalTopic->Marker.Length, FirstLocalTopic->Marker.Base);
SanitisePunctuation(SanitisedMarker);
CopyStringToBuffer(TimestampClass, " cat_%s", SanitisedMarker);
}
else
{
for(int i = 0; i < LocalTopics->ItemCount; ++i)
{
category_info *This = GetPlaceInBook(LocalTopics, i);
// NOTE(matt): Stack-string
char SanitisedMarker[This->Marker.Length + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)This->Marker.Length, This->Marker.Base);
SanitisePunctuation(SanitisedMarker);
CopyStringToBuffer(TimestampClass, " cat_%s",
SanitisedMarker);
}
}
category_info *FirstLocalMedium = GetPlaceInBook(LocalMedia, 0);
if(LocalMedia->ItemCount == 1 && StringsMatch(DefaultMedium, FirstLocalMedium->Marker))
{
// NOTE(matt): Stack-string
char SanitisedMarker[FirstLocalMedium->Marker.Length + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)FirstLocalMedium->Marker.Length, FirstLocalMedium->Marker.Base);
SanitisePunctuation(SanitisedMarker);
CopyStringToBuffer(TimestampClass, " %s", SanitisedMarker);
}
else
{
for(int i = 0; i < LocalMedia->ItemCount; ++i)
{
category_info *This = GetPlaceInBook(LocalMedia, i);
// NOTE(matt): Stack-string
char SanitisedMarker[This->Marker.Length + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)This->Marker.Length, This->Marker.Base);
SanitisePunctuation(SanitisedMarker);
medium *Medium = GetMediumFromProject(CurrentProject, This->Marker);
if(Medium)
{
if(Medium->Hidden)
{
CopyStringToBuffer(TimestampClass, " off_%s skip", SanitisedMarker);
}
else
{
CopyStringToBuffer(TimestampClass, " %s", SanitisedMarker);
}
}
}
}
CopyStringToBuffer(TimestampClass, "\"");
}
void
BuildCategoryIcons(buffer *CategoryIcons, _memory_book(category_info) *LocalTopics, _memory_book(category_info) *LocalMedia, string DefaultMedium, bool *RequiresCineraJS)
{
bool CategoriesSpan = FALSE;
category_info *FirstLocalTopic = GetPlaceInBook(LocalTopics, 0);
category_info *FirstLocalMedium = GetPlaceInBook(LocalMedia, 0);
if(!(LocalTopics->ItemCount == 1 && StringsMatch(FirstLocalTopic->Marker, Wrap0("nullTopic"))
&& LocalMedia->ItemCount == 1 && StringsMatch(DefaultMedium, FirstLocalMedium->Marker)))
{
CategoriesSpan = TRUE;
CopyStringToBuffer(CategoryIcons, "<span class=\"cineraCategories\">");
}
if(!(LocalTopics->ItemCount == 1 && StringsMatch(FirstLocalTopic->Marker, Wrap0("nullTopic"))))
{
for(int i = 0; i < LocalTopics->ItemCount; ++i)
{
category_info *This = GetPlaceInBook(LocalTopics, i);
// NOTE(matt): Stack-string
char SanitisedMarker[This->Marker.Length + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)This->Marker.Length, This->Marker.Base);
SanitisePunctuation(SanitisedMarker);
CopyStringToBuffer(CategoryIcons,
"<div title=\"%.*s\" class=\"category %s\"",
(int)This->Marker.Length, This->Marker.Base,
SanitisedMarker);
if(IsValidHSLColour(This->Colour))
{
CopyStringToBuffer(CategoryIcons,
" data-hue=\"%u\" data-saturation=\"%u%%\"",
This->Colour.Hue, This->Colour.Saturation);
}
CopyStringToBuffer(CategoryIcons,
"></div>");
}
}
if(!(LocalMedia->ItemCount == 1 && StringsMatch(DefaultMedium, FirstLocalMedium->Marker)))
{
for(int i = 0; i < LocalMedia->ItemCount; ++i)
{
category_info *This = GetPlaceInBook(LocalMedia, i);
// NOTE(matt): Stack-string
char SanitisedMarker[This->Marker.Length + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)This->Marker.Length, This->Marker.Base);
SanitisePunctuation(SanitisedMarker);
medium *Medium = GetMediumFromProject(CurrentProject, This->Marker);
if(Medium && !Medium->Hidden)
{
CopyStringToBuffer(CategoryIcons, "<div title=\"%.*s\" class=\"categoryMedium %.*s\">",
(int)This->WrittenText.Length, This->WrittenText.Base,
(int)This->Marker.Length, This->Marker.Base);
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)
{
MEM_TEST_TOP();
Print(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);
/* */ MEM_TEST_MID();
/* +MEM */ Result = curl_easy_perform(curl);
/* */ MEM_TEST_MID();
if(Result)
{
Print(stderr, "%s\n", curl_easy_strerror(Result));
}
curl_easy_cleanup(curl);
curl = 0;
}
MEM_TEST_END();
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;
}
string
UnixTimeToDateString(memory_book *Book, int64_t Time)
{
string Result = {};
// NOTE(matt): Stack-string
char DayString[3] = { };
strftime(DayString, 3, "%e", gmtime(&Time));
Result = WriteStringInBook(Book, TrimWhitespace(Wrap0(DayString)));
int Day = String0ToInt(DayString);
// NOTE(matt): Stack-string
if(DayString[1] == '1' && Day != 11) { Result = ExtendStringInBook(Book, Wrap0("st ")); }
else if(DayString[1] == '2' && Day != 12) { Result = ExtendStringInBook(Book, Wrap0("nd ")); }
else if(DayString[1] == '3' && Day != 13) { Result = ExtendStringInBook(Book, Wrap0("rd ")); }
else { Result = ExtendStringInBook(Book, Wrap0("th ")); }
// NOTE(matt): Stack-string
char MonthYear[32] = {};
strftime(MonthYear, 32, "%B, %Y", gmtime(&Time));
Result = ExtendStringInBook(Book, Wrap0(MonthYear));
return Result;
}
rc
SearchQuotes(memory_book *Strings, 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 ','
Info->Date = StringToInt(InTime);
string Text = GetStringFromBufferT(QuoteStaging, '\n');
Info->Text = WriteStringInBook(Strings, Text);
Result = RC_SUCCESS;
break;
}
else
{
while(QuoteStaging->Ptr - QuoteStaging->Location < CacheSize && *QuoteStaging->Ptr != '\n')
{
++QuoteStaging->Ptr;
}
++QuoteStaging->Ptr;
}
}
return Result;
}
rc
BuildQuote(memory_book *Strings, quote_info *Info, string Speaker, int ID, bool ShouldFetchQuotes)
{
MEM_TEST_TOP();
rc Result = RC_SUCCESS;
// TODO(matt): Suss out the Speaker more sensibly
if(Speaker.Length > 0)
{
// 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);
// 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;
FILE *QuoteCache = fopen(QuoteCachePath, "a+");
if(QuoteCache)
{
CacheAvailable = TRUE;
}
else
{
MakeDir(Wrap0i(QuoteCacheDir));
QuoteCache = fopen(QuoteCachePath, "a+");
if(QuoteCache)
{
CacheAvailable = TRUE;
}
else
{
// TODO(matt): SystemError();
Print(stderr, "Unable to open quote cache %s: %s\n", QuoteCachePath, strerror(errno));
}
}
buffer QuoteStaging = {};
QuoteStaging.ID = BID_QUOTE_STAGING;
QuoteStaging.Size = Kilobytes(256);
QuoteStaging.Location = malloc(QuoteStaging.Size);
QuoteStaging.Ptr = QuoteStaging.Location;
int CacheSize = 0;
if(QuoteStaging.Location)
{
#if DEBUG_MEM
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
WriteToFile(MemLog, " Allocated QuoteStaging (%ld)\n", QuoteStaging.Size);
fclose(MemLog);
Print(stdout, " Allocated QuoteStaging (%ld)\n", QuoteStaging.Size);
#endif
if(!ShouldFetchQuotes)
{
if(CacheAvailable)
{
fseek(QuoteCache, 0, SEEK_END);
CacheSize = ftell(QuoteCache);
fseek(QuoteCache, 0, SEEK_SET);
fread(QuoteStaging.Location, CacheSize, 1, QuoteCache);
fclose(QuoteCache);
rc SearchQuotesResult = SearchQuotes(Strings, &QuoteStaging, CacheSize, Info, ID);
if(SearchQuotesResult == RC_UNFOUND)
{
ShouldFetchQuotes = TRUE;
}
}
else
{
ShouldFetchQuotes = TRUE;
}
}
if(ShouldFetchQuotes)
{
QuoteStaging.Ptr = QuoteStaging.Location;
/* */ MEM_TEST_MID();
/* +MEM */ CURLcode CurlQuotesResult = CurlQuotes(&QuoteStaging, QuotesURL);
/* */ MEM_TEST_MID();
if(CurlQuotesResult == CURLE_OK)
{
LastQuoteFetch = time(0);
CacheSize = QuoteStaging.Ptr - QuoteStaging.Location;
Result = SearchQuotes(Strings, &QuoteStaging, CacheSize, Info, ID);
}
else
{
Result = RC_UNFOUND;
}
if(CacheAvailable)
{
QuoteCache = fopen(QuoteCachePath, "w");
fwrite(QuoteStaging.Location, CacheSize, 1, QuoteCache);
fclose(QuoteCache);
}
}
FreeBuffer(&QuoteStaging);
}
else
{
Result = RC_ERROR_MEMORY;
if(CacheAvailable) { fclose(QuoteCache); }
}
MEM_TEST_END();
}
return Result;
}
rc
GenerateTopicColours(neighbourhood *N, string Topic, hsl_colour *Dest)
{
rc Result = RC_SUCCESS;
// 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)
{
if(StringsMatch(Topic, Wrap0("nullTopic")))
{
Dest->Hue = CINERA_HSL_TRANSPARENT_HUE;
}
else
{
StringToColourHash(Dest, Topic);
}
file Topics = {};
Topics.Path = 0;
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 = opendir(Topics.Path); // TODO(matt): open()
if(!CSSDirHandle)
{
if(!MakeDir(Wrap0(Topics.Path)))
{
LogError(LOG_ERROR, "Unable to create directory %s: %s", Topics.Path, strerror(errno));
Print(stderr, "Unable to create directory %s: %s\n", Topics.Path, strerror(errno));
Result = RC_ERROR_DIRECTORY;
};
}
closedir(CSSDirHandle);
if(Result == RC_SUCCESS)
{
*Ptr = '/';
ReadFileIntoBuffer(&Topics);
bool Exists = FALSE;
while(Topics.Buffer.Ptr - Topics.Buffer.Location < Topics.Buffer.Size)
{
Topics.Buffer.Ptr += sizeof(".category.")-1;
if(!StringsDifferT(SanitisedTopic, Topics.Buffer.Ptr, ' '))
{
Exists = TRUE;
break;
}
while(Topics.Buffer.Ptr - Topics.Buffer.Location < Topics.Buffer.Size && *Topics.Buffer.Ptr != '\n')
{
++Topics.Buffer.Ptr;
}
++Topics.Buffer.Ptr;
}
if(!Exists)
{
Topics.Handle = fopen(Topics.Path, "a+");
if(Topics.Handle)
{
if(StringsMatch(Topic, Wrap0("nullTopic")))
{
WriteToFile(Topics.Handle, ".category.%s { border: 1px solid transparent; background: transparent; }\n",
SanitisedTopic);
}
else
{
WriteToFile(Topics.Handle, ".category.%s { border: 1px solid hsl(%d, %d%%, %d%%); background: hsl(%d, %d%%, %d%%); }\n",
SanitisedTopic, Dest->Hue, Dest->Saturation, Dest->Lightness, Dest->Hue, Dest->Saturation, Dest->Lightness);
}
#if DEBUG_MEM
MemLog = fopen("/home/matt/cinera_mem", "a+");
WriteToFile(MemLog, " Freed Topics (%ld)\n", Topics.Buffer.Size);
fclose(MemLog);
Print(stdout, " Freed Topics (%ld)\n", Topics.Buffer.Size);
#endif
CloseFile(&Topics, NA);
asset *Asset = GetPlaceInBook(&Assets, 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);
}
}
else
{
perror(Topics.Path);
Result = RC_ERROR_FILE;
}
}
}
FreeFile(&Topics, NA);
}
return Result;
}
void
ResetConfigIdentifierDescriptionDisplayedBools(void)
{
for(int i = 0; i < IDENT_COUNT; ++i)
{
ConfigIdentifiers[i].IdentifierDescriptionDisplayed = FALSE;
ConfigIdentifiers[i].IdentifierDescription_MediumDisplayed = FALSE;
ConfigIdentifiers[i].LocalVariableDescriptionDisplayed = FALSE;
ConfigIdentifiers[i].IdentifierDescription_RoleDisplayed = FALSE;
ConfigIdentifiers[i].IdentifierDescription_CreditDisplayed = FALSE;
}
}
void
PrintHelpConfig(void)
{
ResetConfigIdentifierDescriptionDisplayedBools();
// Config Syntax
int IndentationLevel = 0;
NewSection("Configuration", &IndentationLevel);
NewSection("Assigning values", &IndentationLevel);
PrintC(CS_YELLOW_BOLD, "identifier");
Print(stderr, " = ");
PrintC(CS_GREEN_BOLD, "\"string\"");
Print(stderr, ";");
IndentedCarriageReturn(IndentationLevel);
PrintC(CS_YELLOW_BOLD, "identifier");
Print(stderr, " = ");
PrintC(CS_BLUE_BOLD, "number");
Print(stderr, ";");
IndentedCarriageReturn(IndentationLevel);
PrintC(CS_YELLOW_BOLD, "identifier");
Print(stderr, " = ");
PrintC(CS_GREEN_BOLD, "\"boolean\"");
Print(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");
Print(stderr, " = ");
PrintC(CS_GREEN_BOLD, "\"scope\"");
Print(stderr, " {");
IndentedCarriageReturn(IndentationLevel);
Print(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."));
memory_book TypeSpecs = InitTypeSpecs();
PrintTypeSpecs(&TypeSpecs, IndentationLevel);
//FreeTypeSpecs(&TypeSpecs);
EndSection(&IndentationLevel);
Print(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);
Print(stderr, "\n");
NewSection("Environment variables", &IndentationLevel);
TypesetString(INDENT_WIDTH *IndentationLevel, Wrap0("Run `export` to see all available environment variables"));
EndSection(&IndentationLevel);
EndSection(&IndentationLevel); // Variables
Print(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);
Print(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 = InitRootScopeTree();
SetTypeSpec(ScopeTree, &TypeSpecs);
SetDefaults(ScopeTree, &TypeSpecs);
PrintScopeTree(ScopeTree, IndentationLevel);
FreeScopeTree(ScopeTree);
FreeTypeSpecs(&TypeSpecs);
#endif
Print(stderr, "\n");
}
#define PrintHelp() PrintHelp_(Args[0], ConfigPath)
void
PrintHelp_(char *BinaryLocation, char *DefaultConfigPath)
{
// Options
Print(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_Old(char *BinaryLocation)
{
#if AFE
Print(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)
{
Print(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)
{
Print(stderr, "\t");
}
Print(stderr, "%s\n",
ProjectInfo[ProjectIndex].Medium);
}
Print(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 <timestamps directory>\n"
" Override default timestamps 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 += sizeof("-->")-1;
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, template_tag_code 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;
Print(stderr, "\n"
"┌─ ");
PrintC(CS_ERROR, "Template error");
Print(stderr, " in ");
PrintC(CS_CYAN, Template->File.Path);
Print(stderr, "\n"
"└─╼ ");
Print(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)
{
Print(stderr, "\n"
"┌─ ");
PrintC(CS_ERROR, "Template error");
Print(stderr, " in ");
PrintC(CS_CYAN, Template->File.Path);
Print(stderr, "\n"
"└─╼ ");
Print(stderr, "Asset file path is too long (we support paths up to %d characters):\n"
" ", MAX_ASSET_FILENAME_LENGTH);
PrintStringC(CS_MAGENTA_BOLD, AssetString);
Print(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;
default: 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:
{
Print(stderr, "\n"
"┌─ ");
PrintC(CS_ERROR, "Template error");
Print(stderr, " in ");
PrintC(CS_CYAN, Template->File.Path);
Print(stderr, "\n"
"└─╼ ");
Print(stderr, "Unclosed asset quoted string: \"");
string UnclosedString = { .Base = AssetString.Base, .Length = MIN(16, AssetString.Length) };
PrintStringC(CS_MAGENTA_BOLD, UnclosedString);
Print(stderr, "\"\n");
} break;
default: break;
}
}
}
return Result;
}
navigation_buffer *
GetNavBufferFromTemplate(template *Template, navigation_spec Spec)
{
navigation_buffer *Result = 0;
for(int i = 0; i < Template->Metadata.NavBuffer.ItemCount; ++i)
{
navigation_buffer *This = GetPlaceInBook(&Template->Metadata.NavBuffer, i);
if(Spec.Type == This->Spec.Type && Spec.ChildrenOf == This->Spec.ChildrenOf)
{
Result = This;
break;
}
}
return Result;
}
navigation_buffer *
PushNavBufferUniquely(template *Template, navigation_spec Spec)
{
navigation_buffer *Result = GetNavBufferFromTemplate(Template, Spec);
if(!Result)
{
Result = MakeSpaceInBook(&Template->Metadata.NavBuffer);
Result->Spec = Spec;
Result->Buffer.ID = BID_NAVIGATION;
}
return Result;
}
navigation_buffer *
ParseNavigationTag(template *Template, template_tag_code TagCode, project *Project)
{
buffer *B = &Template->File.Buffer;
B->Ptr += StringLength(TemplateTags[TagCode]);
ConsumeWhitespace(B);
navigation_spec Spec = {};
Spec.Type = NT_NULL;
// TODO(matt): This is temporary, until we have __CINERA_PATHED_NAV__
// Once we do, we'll be parsing the ChildrenOf out of the file and doing GetProjectByLineage()
if(!Project || TagCode == TAG_GLOBAL_NAV)
{
Spec.ChildrenOf = 0;
}
else if(TagCode == TAG_NAV)
{
// TODO(matt): With __CINERA_PATHED_NAV__ make sure we handle nonexistent project lineages
project *P = Project;
while(P->Parent)
{
P = P->Parent;
}
Spec.ChildrenOf = P;
}
if(AtHTMLCommentCloser(B))
{
Spec.Type = 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]))
{
Spec.Type = NavigationType;
break;
}
}
if(Spec.Type == NT_NULL)
{
Print(stderr, "\n"
"┌─ ");
PrintC(CS_ERROR, "Template error");
Print(stderr, " in ");
PrintC(CS_CYAN, Template->File.Path);
Print(stderr, "\n"
"└─╼ ");
Print(stderr, "Invalid navigation type \"");
PrintStringC(CS_MAGENTA_BOLD, NavigationString);
Print(stderr, "\"\n");
}
}
else
{
switch(ReturnCode)
{
case RC_ERROR_PARSING_UNCLOSED_QUOTED_STRING:
{
Print(stderr, "\n"
"┌─ ");
PrintC(CS_ERROR, "Template error");
Print(stderr, " in ");
PrintC(CS_CYAN, Template->File.Path);
Print(stderr, "\n"
"└─╼ ");
Print(stderr, "Unclosed navigation type quoted string: \"");
string UnclosedString = { .Base = NavigationString.Base, .Length = MIN(16, NavigationString.Length) };
PrintStringC(CS_MAGENTA_BOLD, UnclosedString);
Print(stderr, "\"\n");
} break;
default: break;
}
}
}
navigation_buffer *Result = 0;
if(Spec.Type != NT_NULL)
{
Result = PushNavBufferUniquely(Template, Spec);
}
return Result;
}
rc
PackTemplate(template *Template, string Location, template_type Type, project *Project)
{
// 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 += sizeof("!--")-1;
while(Template->File.Buffer.Ptr - Template->File.Buffer.Location < Template->File.Buffer.Size && StringsDifferT("-->", Template->File.Buffer.Ptr, 0))
{
for(template_tag_code 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_buffer *NavBuffer = 0;
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:
case TAG_GLOBAL_NAV:
{
NavBuffer = ParseNavigationTag(Template, TagIndex, Project);
if(!NavBuffer)
{
HaveErrors = TRUE;
HaveNavParsingErrors = TRUE;
}
else if(NavBuffer->Spec.Type == NT_DROPDOWN)
{
Template->Metadata.RequiresCineraJS = TRUE;
}
} goto RecordTag;
case TAG_PROJECT:
case TAG_PROJECT_ID:
case TAG_PROJECT_LINEAGE:
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, NavBuffer);
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]); };
Print(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]); };
Print(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);
Print(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);
string PlayerLocation = Wrap0i(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 HMMLFilepath, string Medium)
{
medium *Result = GetMediumFromProject(CurrentProject, Medium);
if(!Result)
{
typography Typography =
{
.UpperLeftCorner = "",
.UpperLeft = "",
.Horizontal = "",
.UpperRight = "",
.Vertical = "",
.LowerLeftCorner = "",
.LowerLeft = "",
.Margin = "",
.Delimiter = ": ",
.Separator = "",
};
IndexingError(HMMLFilepath, 0, S_ERROR, "Specified default medium not available: ", &Medium);
Print(stderr, "Valid media are:\n");
for(int i = 0; i < CurrentProject->Medium.ItemCount; ++i)
{
medium *This = GetPlaceInBook(&CurrentProject->Medium, i);
PrintMedium(&Typography, This, ": ", 2, TRUE);
}
Print(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 Result;
}
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;
Print(stdout, "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;
Print(stdout, " %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;
Print(stdout, "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;
Print(stdout, " %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;
Print(stdout, "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;
Print(stdout, " %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));
Print(stdout, "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;
Print(stdout, " %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);
Print(stdout, "\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;
Print(stdout, "\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);
Print(stdout, " %d•%s%d%s", LocalDB.Landmark.Entry, ColourStrings[CS_MAGENTA], LocalDB.Landmark.Position, ColourStrings[CS_END]);
}
Print(stdout, "\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)
{
Print(stderr, "%i", Index);
}
else
{
Print(stderr, "%s", GetAssetStringFromIndex(Index));
}
Colourise(CS_END);
}
void *
PrintProjectAndChildren(db_header_project *P, typography T)
{
string ID = Wrap0i(P->ID);
string Title = Wrap0i(P->Title);
string BaseDir = Wrap0i(P->BaseDir);
string BaseURL = Wrap0i(P->BaseURL);
string SearchLocation = Wrap0i(P->SearchLocation);
string PlayerLocation = Wrap0i(P->PlayerLocation);
string Theme = Wrap0i(P->Theme);
string Unit = Wrap0i(P->Unit);
Print(stderr, "\n");
PrintC(CS_YELLOW_BOLD, "\nID"); Print(stderr, ": "); PrintStringC(CS_GREEN_BOLD, ID);
PrintC(CS_YELLOW_BOLD, "\nTitle"); Print(stderr, ": "); PrintStringC(CS_GREEN_BOLD, Title);
PrintC(CS_YELLOW_BOLD, "\nBaseDir"); Print(stderr, ": "); PrintStringC(CS_GREEN_BOLD, BaseDir);
PrintC(CS_YELLOW_BOLD, "\nBaseURL"); Print(stderr, ": "); PrintStringC(CS_GREEN_BOLD, BaseURL);
PrintC(CS_YELLOW_BOLD, "\nSearchLocation"); Print(stderr, ": "); PrintStringC(CS_GREEN_BOLD, SearchLocation);
PrintC(CS_YELLOW_BOLD, "\nPlayerLocation"); Print(stderr, ": "); PrintStringC(CS_GREEN_BOLD, PlayerLocation);
PrintC(CS_YELLOW_BOLD, "\nTheme"); Print(stderr, ": "); PrintStringC(CS_GREEN_BOLD, Theme);
PrintC(CS_YELLOW_BOLD, "\nArt asset index"); Print(stderr, ": "); PrintAssetIndex(P->ArtIndex);
PrintC(CS_YELLOW_BOLD, "\nIcon asset index"); Print(stderr, ": "); PrintAssetIndex(P->IconIndex);
PrintC(CS_YELLOW_BOLD, "\nUnit"); Print(stderr, ": "); PrintStringC(CS_GREEN_BOLD, Unit);
char *Ptr = (char *)P;
Ptr += sizeof(db_header_project);
PrintC(CS_YELLOW_BOLD, "\nEntries");
Print(stderr, " (");
Colourise(CS_BLUE_BOLD);
Print(stderr, "%ld", P->EntryCount);
Colourise(CS_END);
Print(stderr, "):");
for(int EntryIndex = 0; EntryIndex < P->EntryCount; ++EntryIndex)
{
db_entry *E = (db_entry *)Ptr;
string EntryHMMLBaseFilename = Wrap0i(E->HMMLBaseFilename);
string EntryOutputLocation = Wrap0i(E->OutputLocation);
string EntryTitle = Wrap0i(E->Title);
string Number = Wrap0i(E->Number);
Print(stderr, "\n"
"%s%s%s%s%s ", T.UpperLeftCorner,
T.Horizontal, T.Horizontal, T.Horizontal,
T.UpperRight);
Colourise(CS_BLACK_BOLD); Print(stderr, "[%d] ", EntryIndex); Colourise(CS_END);
PrintStringC(CS_GREEN_BOLD, EntryHMMLBaseFilename);
Print(stderr, "\n%s%s", T.Vertical, T.Margin); PrintC(CS_YELLOW_BOLD, "OutputLocation"); Print(stderr, ": "); PrintStringC(CS_GREEN_BOLD, EntryOutputLocation);
Print(stderr, "\n%s%s", T.Vertical, T.Margin); PrintC(CS_YELLOW_BOLD, "Number"); Print(stderr, ": "); PrintStringC(CS_GREEN_BOLD, Number);
Print(stderr, "\n%s%s", T.Vertical, T.Margin); PrintC(CS_YELLOW_BOLD, "Title"); Print(stderr, ": "); PrintStringC(CS_GREEN_BOLD, EntryTitle);
Print(stderr, "\n%s%s", T.Vertical, T.Margin); PrintC(CS_YELLOW_BOLD, "Size"); Print(stderr, ": ");
Colourise(CS_BLUE_BOLD);
Print(stderr, "%d", E->Size);
Colourise(CS_END);
Print(stderr, "\n%s%s", T.Vertical, T.Margin); PrintC(CS_YELLOW_BOLD, "Link offsets"); Print(stderr, ": ");
Colourise(CS_BLUE_BOLD);
Print(stderr, "%d\t%d\t%d\t%d",
E->LinkOffsets.PrevStart,
E->LinkOffsets.PrevEnd,
E->LinkOffsets.NextStart,
E->LinkOffsets.NextEnd);
Colourise(CS_END);
Print(stderr, "\n%s%s", T.LowerLeft, T.Margin); PrintC(CS_YELLOW_BOLD, "Art asset index"); Print(stderr, ": "); PrintAssetIndex(E->ArtIndex);
Ptr += sizeof(db_entry);
}
if(P->EntryCount > 0) { Print(stderr, "\n"); }
PrintC(CS_YELLOW_BOLD, "\nChildren");
Print(stderr, " (");
Colourise(CS_BLUE_BOLD);
Print(stderr, "%ld", P->ChildCount);
Colourise(CS_END);
Print(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);
}
Print(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;
Print(stderr, "\n");
PrintC(CS_BLUE_BOLD, "Versions");
Print(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);
// TODO(matt): REMOVE
//
Config->SuppressingPrompts = FALSE;
WaitForInput();
//
////
}
else
{
Print(stderr, "\n"
"Invalid database file: %s", File->Path);
break;
}
}
Print(stderr, "\n");
}
}
void
ExamineDB(file *DB)
{
if(DB->Buffer.Location)
{
uint32_t FirstInt = *(uint32_t *)DB->Buffer.Location;
if(FirstInt != FOURCC("CNRA"))
{
switch(FirstInt)
{
case 1: ExamineDB1(DB); break;
case 2: ExamineDB2(DB); break;
case 3: ExamineDB3(DB); break;
default: Print(stdout, "Invalid database file: %s\n", DB->Path); break;
}
}
else
{
uint32_t SecondInt = *(uint32_t *)(DB->Buffer.Location + sizeof(uint32_t));
switch(SecondInt)
{
case 4: ExamineDB4(DB); break;
case 5: ExamineDB5(DB); break;
default: Print(stdout, "Invalid database file: %s\n", DB->Path); break;
}
}
}
else
{
Print(stderr, "Unable to open database file %s: %s\n", DB->Path, strerror(errno));
}
}
#define HMMLCleanup() \
DeclaimPlayerBuffers(&PlayerBuffers); \
DeclaimIndexBuffers(&IndexBuffers); \
DeclaimMenuBuffers(&MenuBuffers)
bool
VideoIsPrivate(vod_platform VODPlatform, char *VideoID)
{
bool Result = FALSE;
// NOTE(matt): Currently only supports YouTube
if(VODPlatform == VP_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);
Print(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);
// TODO(matt): Handle the case when our API quota has depleted / expired
if((CurlReturnCode = curl_easy_perform(curl)))
{
Print(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')
{
// Print(stdout, "Private video: https://youtube.com/watch?v=%s\n", VideoID);
Result = TRUE;
}
else
{
VideoAPIResponse.Ptr = VideoAPIResponse.Location;
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"))
{
Result = FALSE;
}
else
{
Result = TRUE;
}
}
}
DeclaimBuffer(&VideoAPIResponse);
// Print(stdout, "Unlisted video: https://youtube.com/watch?v=%s\n", VideoID);
ClearTerminalRow(MessageLength);
}
return Result;
}
bool
IsCategorisedAFK(HMML_Timestamp *Timestamp)
{
for(int i = 0; i < Timestamp->marker_count; ++i)
{
if(!StringsDiffer0(Timestamp->markers[i].marker, "afk"))
{
return TRUE;
}
}
return FALSE;
}
bool
IsCategorisedAuthored(HMML_Timestamp *Timestamp)
{
for(int i = 0; i < Timestamp->marker_count; ++i)
{
if(!StringsDiffer0(Timestamp->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(string *BaseDir, string *SearchLocation, string ProjectID)
{
char *Result = ConstructDirectoryPath(BaseDir, SearchLocation, 0);
ExtendString0(&Result, Wrap0("/"));
ExtendString0(&Result, ProjectID);
ExtendString0(&Result, ExtensionStrings[EXT_INDEX]);
return Result;
}
void
DeleteSearchPageFromFilesystem(string BaseDir, string SearchLocation, string ProjectID) // NOTE(matt): Do we need to handle relocating, like the PlayerPage function?
{
char *SearchPagePath = ConstructHTMLIndexFilePath(&BaseDir, &SearchLocation, 0);
remove(SearchPagePath);
Free(SearchPagePath);
char *IndexFilePath = ConstructIndexFilePath(&BaseDir, &SearchLocation, ProjectID);
remove(IndexFilePath);
Free(IndexFilePath);
char *SearchDirectory = ConstructDirectoryPath(&BaseDir, &SearchLocation, 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);
Print(stderr, "/");
PrintStringC(CS_MAGENTA, EntryID);
if(AppendNewline) { Print(stderr, "\n"); }
}
void
ExtendLineageAndEntryIDInBook(memory_book *M, string Lineage, string EntryID, bool AppendNewline)
{
ExtendLineageInBook(M, Lineage, FALSE);
ExtendStringInBook(M, Wrap0("/"));
ExtendStringCInBook(M, CS_MAGENTA, EntryID);
if(AppendNewline) { ExtendStringInBook(M, Wrap0("\n")); }
}
void
PrintLineageAndEntry(string Lineage, string EntryID, string EntryTitle, bool AppendNewline)
{
PrintLineageAndEntryID(Lineage, EntryID, FALSE);
Colourise(CS_MAGENTA);
Print(stderr, " - ");
PrintString(EntryTitle);
Colourise(CS_END);
if(AppendNewline) { Print(stderr, "\n"); }
}
void
ExtendLineageAndEntryInBook(memory_book *M, string Lineage, string EntryID, string EntryTitle, bool AppendNewline)
{
ExtendLineageAndEntryIDInBook(M, Lineage, EntryID, FALSE);
ExtendStringInBook(M, Wrap0(ColourStrings[CS_MAGENTA]));
ExtendStringInBook(M, Wrap0(" - "));
ExtendStringInBook(M, EntryTitle);
ExtendStringInBook(M, Wrap0(ColourStrings[CS_END]));
if(AppendNewline) { ExtendStringInBook(M, Wrap0("\n")); }
}
void
ClearLastMessage(void)
{
// TODO(matt): Handle multi-line messages
Print(stderr, "\033[1A");
Print(stderr, "\033[K");
}
void
PrintEdit(edit_type_id EditType, string Lineage, string EntryID, string *EntryTitle, bool Private)
{
memory_book *Desired = MESSAGE_CONTROL.DesiredMessage;
ResetPen(Desired);
ExtendStringInBook(Desired, Wrap0(ColourStrings[EditTypes[EditType].Colour]));
ExtendStringInBook(Desired, Wrap0(Private ? "Privately " : ""));
ExtendStringInBook(Desired, Wrap0(EditTypes[EditType].Name));
ExtendStringInBook(Desired, Wrap0(ColourStrings[CS_END]));
ExtendStringInBook(Desired, Wrap0(" "));
if(EntryTitle)
{
ExtendLineageAndEntryInBook(Desired, CurrentProject->Lineage, EntryID, *EntryTitle, TRUE);
}
else
{
ExtendLineageAndEntryIDInBook(Desired, CurrentProject->Lineage, EntryID, TRUE);
}
if(MESSAGE_CONTROL.LastMessageMayBeDeduplicated && BooksMatch(MESSAGE_CONTROL.LastMessage, MESSAGE_CONTROL.DesiredMessage))
{
ClearLastMessage();
++MESSAGE_CONTROL.RepetitionCount;
Print(stderr, "%s[×%lu]%s ", ColourStrings[CS_BLACK_BOLD], MESSAGE_CONTROL.RepetitionCount, ColourStrings[CS_END]);
}
else
{
MESSAGE_CONTROL.RepetitionCount = 1;
}
PrintBookOfStrings(MESSAGE_CONTROL.DesiredMessage);
ResetBook(MESSAGE_CONTROL.LastMessage);
SwapPtrs(MESSAGE_CONTROL.DesiredMessage, MESSAGE_CONTROL.LastMessage);
MESSAGE_CONTROL.LastMessageMayBeDeduplicated = TRUE;
}
rc
DeletePlayerPageFromFilesystem(string BaseDir, string PlayerLocation, string EntryOutput, bool Relocating, bool Echo)
{
char *OutputDirectoryPath = ConstructDirectoryPath(&BaseDir, &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, "Unable to remove directory %s: %s", OutputDirectoryPath, strerror(errno));
Print(stderr, "\n"
" %sUnable to remove directory%s %s: %s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], OutputDirectoryPath, strerror(errno));
}
}
}
Free(OutputDirectoryPath);
return RC_SUCCESS;
}
speakers
InitSpeakers()
{
speakers Result = {};
Result.Speakers = InitBook(sizeof(speaker), 4);
return Result;
}
rc
ClaimMenuIndexAndPlayerBuffers(menu_buffers *MenuBuffers, index_buffers *IndexBuffers, player_buffers *PlayerBuffers)
{
rc Result = RC_SUCCESS;
if(ClaimBuffer(&MenuBuffers->Quote, BID_MENU_BUFFERS_QUOTE, Kilobytes(32)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; };
if(ClaimBuffer(&MenuBuffers->Reference, BID_MENU_BUFFERS_REFERENCE, Kilobytes(32)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; };
if(ClaimBuffer(&MenuBuffers->Filter, BID_MENU_BUFFERS_FILTER, Kilobytes(16)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; };
if(ClaimBuffer(&MenuBuffers->FilterTopics, BID_MENU_BUFFERS_FILTER_TOPICS, Kilobytes(8)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; };
if(ClaimBuffer(&MenuBuffers->FilterMedia, BID_MENU_BUFFERS_FILTER_MEDIA, Kilobytes(8)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; };
if(ClaimBuffer(&MenuBuffers->Credits, BID_MENU_BUFFERS_CREDITS, Kilobytes(8)) == RC_ARENA_FULL) { Result = 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) { Result = RC_ARENA_FULL; };
if(ClaimBuffer(&IndexBuffers->Header, BID_INDEX_BUFFERS_HEADER, Kilobytes(1)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; };
if(ClaimBuffer(&IndexBuffers->Class, BID_INDEX_BUFFERS_CLASS, 256) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; };
if(ClaimBuffer(&IndexBuffers->Data, BID_INDEX_BUFFERS_DATA, Kilobytes(1)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; };
if(ClaimBuffer(&IndexBuffers->Text, BID_INDEX_BUFFERS_TEXT, Kilobytes(4)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; };
if(ClaimBuffer(&IndexBuffers->CategoryIcons, BID_INDEX_BUFFERS_CATEGORY_ICONS, Kilobytes(1)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; };
if(ClaimBuffer(&PlayerBuffers->Menus, BID_PLAYER_BUFFERS_MENUS, Kilobytes(32)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; };
if(ClaimBuffer(&PlayerBuffers->Main, BID_PLAYER_BUFFERS_MAIN, Kilobytes(512)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; };
if(ClaimBuffer(&PlayerBuffers->Script, BID_PLAYER_BUFFERS_SCRIPT, Kilobytes(8)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; };
return Result;
}
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;
ref_info *
BuildReference(memory_book *Strings, _memory_book(ref_info) *ReferencesArray, HMML_Reference *Ref, rc *ReturnCode)
{
ref_info *Result = 0;
char *ID = 0;
if(Ref->isbn)
{
ID = Ref->isbn;
}
else if(Ref->url)
{
ID = Ref->url;
}
if(ID)
{
ref_info *This = MakeSpaceInBook(ReferencesArray);
This->Identifier = InitBook(sizeof(identifier), 4);
// TODO(matt): Just point into the Ref directly, rather than copying strings?
if(Ref->isbn)
{
This->ID = Ref->isbn;
if(!Ref->url) { This->URL = WriteStringInBook(Strings, Wrap0("https://isbndb.com/book/")); This->URL = ExtendStringInBook(Strings, Wrap0(Ref->isbn)); }
else { This->URL = WriteStringInBook(Strings, Wrap0(Ref->url)); }
}
else if(Ref->url)
{
This->ID = Ref->url;
This->URL = WriteStringInBook(Strings, Wrap0(Ref->url));
}
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
Result = This;
switch(Mask)
{
case (REF_TITLE | REF_AUTHOR | REF_PUBLISHER):
{
//CopyString(This->Source, sizeof(This->Source), "%s (%s)", Ref->author, Ref->publisher);
This->Source = WriteStringInBook(Strings, Wrap0(Ref->author));
This->Source = ExtendStringInBook(Strings, Wrap0(" ("));
This->Source = ExtendStringInBook(Strings, Wrap0(Ref->publisher));
This->Source = ExtendStringInBook(Strings, Wrap0(")"));
//CopyStringNoFormat(This->RefTitle, sizeof(This->RefTitle), Wrap0(Ref->title));
This->RefTitle = WriteStringInBook(Strings, Wrap0(Ref->title));
} break;
case (REF_AUTHOR | REF_SITE | REF_PAGE):
{
//CopyStringNoFormat(This->Source, sizeof(This->Source), Wrap0(Ref->site));
This->Source = WriteStringInBook(Strings, Wrap0(Ref->site));
//CopyString(This->RefTitle, sizeof(This->RefTitle), "%s: \"%s\"", Ref->author, Ref->page);
This->RefTitle = WriteStringInBook(Strings, Wrap0(Ref->author));
This->RefTitle = ExtendStringInBook(Strings, Wrap0(": \""));
This->RefTitle = ExtendStringInBook(Strings, Wrap0(Ref->page));
This->RefTitle = ExtendStringInBook(Strings, Wrap0("\""));
} break;
case (REF_PAGE | REF_TITLE):
{
//CopyStringNoFormat(This->Source, sizeof(This->Source), Wrap0(Ref->title));
This->Source = WriteStringInBook(Strings, Wrap0(Ref->title));
//CopyStringNoFormat(This->RefTitle, sizeof(This->RefTitle), Wrap0(Ref->page));
This->RefTitle = WriteStringInBook(Strings, Wrap0(Ref->page));
} break;
case (REF_SITE | REF_PAGE):
{
//CopyStringNoFormat(This->Source, sizeof(This->Source), Wrap0(Ref->site));
This->Source = WriteStringInBook(Strings, Wrap0(Ref->site));
//CopyStringNoFormat(This->RefTitle, sizeof(This->RefTitle), Wrap0(Ref->page));
This->RefTitle = WriteStringInBook(Strings, Wrap0(Ref->page));
} break;
case (REF_SITE | REF_TITLE):
{
//CopyStringNoFormat(This->Source, sizeof(This->Source), Wrap0(Ref->site));
This->Source = WriteStringInBook(Strings, Wrap0(Ref->site));
//CopyStringNoFormat(This->RefTitle, sizeof(This->RefTitle), Wrap0(Ref->title));
This->RefTitle = WriteStringInBook(Strings, Wrap0(Ref->title));
} break;
case (REF_TITLE | REF_AUTHOR):
{
//CopyStringNoFormat(This->Source, sizeof(This->Source), Wrap0(Ref->author));
This->Source = WriteStringInBook(Strings, Wrap0(Ref->author));
//CopyStringNoFormat(This->RefTitle, sizeof(This->RefTitle), Wrap0(Ref->title));
This->RefTitle = WriteStringInBook(Strings, Wrap0(Ref->title));
} break;
case (REF_ARTICLE | REF_AUTHOR):
{
//CopyStringNoFormat(This->Source, sizeof(This->Source), Wrap0(Ref->author));
This->Source = WriteStringInBook(Strings, Wrap0(Ref->author));
//CopyStringNoFormat(This->RefTitle, sizeof(This->RefTitle), Wrap0(Ref->article));
This->RefTitle = WriteStringInBook(Strings, Wrap0(Ref->article));
} break;
case (REF_TITLE | REF_PUBLISHER):
{
//CopyStringNoFormat(This->Source, sizeof(This->Source), Wrap0(Ref->publisher));
This->Source = WriteStringInBook(Strings, Wrap0(Ref->publisher));
//CopyStringNoFormat(This->RefTitle, sizeof(This->RefTitle), Wrap0(Ref->title));
This->RefTitle = WriteStringInBook(Strings, Wrap0(Ref->title));
} break;
case REF_TITLE:
{
//CopyStringNoFormat(This->RefTitle, sizeof(This->RefTitle), Wrap0(Ref->title));
This->RefTitle = WriteStringInBook(Strings, Wrap0(Ref->title));
} break;
case REF_SITE:
{
//CopyStringNoFormat(This->RefTitle, sizeof(This->RefTitle), Wrap0(Ref->site));
This->RefTitle = WriteStringInBook(Strings, Wrap0(Ref->site));
} break;
default: Result = 0; *ReturnCode = RC_UNHANDLED_REF_COMBO; break;
}
}
else
{
*ReturnCode = RC_INVALID_REFERENCE;
}
return Result;
}
ref_info *
GetReference(_memory_book(ref_info) *ReferencesArray, HMML_Reference *Reference)
{
ref_info *Result = 0;
char *ID = 0;
if(Reference->isbn) { ID = Reference->isbn; }
else if(Reference->url) { ID = Reference->url; }
if(ID)
{
for(int i = 0; i < ReferencesArray->ItemCount; ++i)
{
ref_info *This = GetPlaceInBook(ReferencesArray, i);
if(!StringsDiffer0(ID, This->ID))
{
Result = This;
break;
}
}
}
return Result;
}
ref_info *
GetOrBuildReference(memory_book *Strings, memory_book *ReferencesArray, HMML_Reference *Reference, rc *ReturnCode)
{
ref_info *Result = GetReference(ReferencesArray, Reference);
if(!Result)
{
Result = BuildReference(Strings, ReferencesArray, Reference, ReturnCode);
}
return Result;
}
void
FreeReferences(_memory_book(ref_info) *References)
{
for(int i = 0; i < References->ItemCount; ++i)
{
ref_info *This = GetPlaceInBook(References, i);
FreeBook(&This->Identifier);
}
FreeBook(References);
}
bool
HMMLBaseFilenameIs(neighbourhood *N, char *ID)
{
return StringsMatch(Wrap0i(N->WorkingThis.HMMLBaseFilename), Wrap0(ID));
}
bool
HMMLOutputLocationIs(neighbourhood *N, char *OutputLocation)
{
return StringsMatch(Wrap0i(N->WorkingThis.OutputLocation), Wrap0(OutputLocation));
}
bool
TimecodeIs(v4 Timecode, int Hours, int Minutes, int Seconds, int Milliseconds)
{
return Timecode.Hours == Hours && Timecode.Minutes == Minutes && Timecode.Seconds == Seconds && Timecode.Milliseconds == Milliseconds;
}
rc
ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, memory_book *Strings,
menu_buffers *MenuBuffers, index_buffers *IndexBuffers, player_buffers *PlayerBuffers,
medium *DefaultMedium, speakers *Speakers, string Author,
_memory_book(ref_info) *ReferencesArray,
bool *HasQuoteMenu, bool *HasReferenceMenu, bool *HasFilterMenu, bool *RequiresCineraJS,
int *QuoteIdentifier, int *RefIdentifier,
_memory_book(category_info) *Topics, _memory_book(category_info) *Media,
HMML_Timestamp *Timestamp, v4 *PreviousTimecode)
{
MEM_TEST_TOP();
// TODO(matt): Introduce and use a SystemError() in here
rc Result = RC_SUCCESS;
v4 Timecode = V4(Timestamp->h, Timestamp->m, Timestamp->s, Timestamp->ms);
if(TimecodeToDottedSeconds(Timecode) >= TimecodeToDottedSeconds(*PreviousTimecode))
{
*PreviousTimecode = Timecode;
memory_book LocalTopics = InitBook(sizeof(category_info), 8);
memory_book LocalMedia = InitBook(sizeof(category_info), 8);
quote_info QuoteInfo = { };
bool HasQuote = FALSE;
bool HasReference = FALSE;
RewindBuffer(&IndexBuffers->Master);
RewindBuffer(&IndexBuffers->Header);
RewindBuffer(&IndexBuffers->Class);
RewindBuffer(&IndexBuffers->Data);
RewindBuffer(&IndexBuffers->Text);
RewindBuffer(&IndexBuffers->CategoryIcons);
CopyStringToBuffer(&IndexBuffers->Header,
" <div data-timestamp=\"%.3f\"",
TimecodeToDottedSeconds(Timecode));
CopyStringToBuffer(&IndexBuffers->Class,
" class=\"marker");
speaker *Speaker = GetSpeaker(&Speakers->Speakers, Author);
if(!IsCategorisedAFK(Timestamp))
{
// NOTE(matt): I reckon it's fair to only cite the speaker when there are a multiple of them
if(Speakers->Speakers.ItemCount > 1 && Speaker && !IsCategorisedAuthored(Timestamp))
{
string DisplayName = !Speaker->Seen ? Speaker->Person->Name : Speaker->Person->Abbreviations[Speakers->AbbrevScheme];
CopyStringToBuffer(&IndexBuffers->Text,
"<span class=\"author\" data-hue=\"%d\" data-saturation=\"%d%%\"",
Speaker->Colour.Hue,
Speaker->Colour.Saturation);
if(Speaker->Seen)
{
CopyStringToBuffer(&IndexBuffers->Text, " title=\"");
CopyStringToBufferHTMLSafe(&IndexBuffers->Text, Speaker->Person->Name);
CopyStringToBuffer(&IndexBuffers->Text, "\"");
}
CopyStringToBuffer(&IndexBuffers->Text,
">%.*s</span>: ", (int)DisplayName.Length, DisplayName.Base);
Speaker->Seen = TRUE;
}
else if(Author.Length > 0)
{
if(!*HasFilterMenu)
{
*HasFilterMenu = TRUE;
}
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0("authored"), NULL);
hsl_colour AuthorColour;
StringToColourHash(&AuthorColour, 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,
(int)Author.Length, Author.Base);
}
}
char *InPtr = Timestamp->text;
int MarkerIndex = 0, RefIndex = 0;
while(*InPtr || RefIndex < Timestamp->reference_count)
{
if(MarkerIndex < Timestamp->marker_count &&
InPtr - Timestamp->text == Timestamp->markers[MarkerIndex].offset)
{
char *Readable = Timestamp->markers[MarkerIndex].parameter
? Timestamp->markers[MarkerIndex].parameter
: Timestamp->markers[MarkerIndex].marker;
HMML_MarkerType Type = Timestamp->markers[MarkerIndex].type;
if(Type == HMML_CATEGORY)
{
hsl_colour TopicColour = {};
Result = GenerateTopicColours(N, Wrap0(Timestamp->markers[MarkerIndex].marker), &TopicColour);
if(Result == RC_SUCCESS)
{
if(!*HasFilterMenu)
{
*HasFilterMenu = TRUE;
}
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0(Timestamp->markers[MarkerIndex].marker), &TopicColour);
CopyStringToBuffer(&IndexBuffers->Text, "%.*s", (int)StringLength(Readable), InPtr);
}
else
{
break;
}
}
else // NOTE(matt): HMML_MEMBER / HMML_PROJECT
{
// TODO(matt): That EDITION_NETWORK site database API-polling stuff
hsl_colour Colour;
StringToColourHash(&Colour, Wrap0(Timestamp->markers[MarkerIndex].marker));
CopyStringToBuffer(&IndexBuffers->Text,
"<span class=\"%s\" data-hue=\"%d\" data-saturation=\"%d%%\">%.*s</span>",
Timestamp->markers[MarkerIndex].type == HMML_MEMBER ? "member" : "project",
Colour.Hue, Colour.Saturation,
(int)StringLength(Readable), InPtr);
}
InPtr += StringLength(Readable);
++MarkerIndex;
}
if(Result == RC_SUCCESS)
{
while(RefIndex < Timestamp->reference_count &&
InPtr - Timestamp->text == Timestamp->references[RefIndex].offset)
{
HMML_Reference *CurrentRef = Timestamp->references + RefIndex;
if(!*HasReferenceMenu)
{
CopyStringToBuffer(&MenuBuffers->Reference,
" <div class=\"menu references\">\n"
" <span>References &#9660;</span>\n"
" <div class=\"refs references_container\">\n");
*HasReferenceMenu = TRUE;
}
ref_info *This = GetOrBuildReference(Strings, ReferencesArray, CurrentRef, &Result);
if(This)
{
identifier *New = MakeSpaceInBook(&This->Identifier);
New->Timecode = Timecode;
New->Identifier = *RefIdentifier;
}
else
{
switch(Result)
{
case RC_INVALID_REFERENCE:
{
IndexingError(Filepath, Timestamp->line, S_ERROR,
"Invalid [ref]. Please set either a \"url\" or \"isbn\"",
0);
} break;
case RC_UNHANDLED_REF_COMBO:
{
IndexingError(Filepath, Timestamp->line, S_ERROR,
"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",
0);
} break;
default: break;
}
break; // NOTE(matt): Out of the while()
}
CopyStringToBuffer(&IndexBuffers->Data, "%s%s", !HasReference ? " data-ref=\"" : "," , This->ID);
HasReference = TRUE;
CopyStringToBuffer(&IndexBuffers->Text, "<sup>%s%d</sup>",
RefIndex > 0 && Timestamp->references[RefIndex].offset == Timestamp->references[RefIndex-1].offset ? "," : "",
*RefIdentifier);
++RefIndex;
++*RefIdentifier;
}
if(Result != RC_SUCCESS)
{
break;
}
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(Result == RC_SUCCESS)
{
if(Timestamp->quote.present)
{
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;
bool ShouldFetchQuotes = FALSE;
if(Config->CacheDir.Length == 0 || time(0) - LastQuoteFetch > 60*60)
{
ShouldFetchQuotes = TRUE;
}
if(!Speaker && Speakers->Speakers.ItemCount > 0)
{
Speaker = GetPlaceInBook(&Speakers->Speakers, 0);
}
string QuoteUsername;
if(Timestamp->quote.author)
{
QuoteUsername = Wrap0(Timestamp->quote.author);
}
else if(Speaker)
{
QuoteUsername = Speaker->Person->QuoteUsername;
}
else
{
QuoteUsername = Author;
}
/* */ MEM_TEST_MID();
/* +MEM */ Result = BuildQuote(Strings, &QuoteInfo,
QuoteUsername, Timestamp->quote.id, ShouldFetchQuotes);
/* */ MEM_TEST_MID();
if(Result == RC_SUCCESS)
{
CopyStringToBuffer(&MenuBuffers->Quote,
" <a target=\"_blank\" data-id=\"&#%d;\" class=\"ref\" href=\"https://dev.abaines.me.uk/quotes/%.*s/%d\">\n"
" <span>\n"
" <span class=\"ref_content\">\n"
" <div class=\"source\">Quote %d</div>\n"
" <div class=\"ref_title\">",
*QuoteIdentifier,
(int)QuoteUsername.Length, QuoteUsername.Base,
Timestamp->quote.id,
Timestamp->quote.id);
CopyStringToBufferHTMLSafe(&MenuBuffers->Quote, QuoteInfo.Text);
string DateString = UnixTimeToDateString(Strings, QuoteInfo.Date);
CopyStringToBuffer(&MenuBuffers->Quote, "</div>\n"
" <div class=\"quote_byline\">&mdash;%.*s, %.*s</div>\n"
" </span>\n"
" <div class=\"ref_indices\">\n"
" <span data-timestamp=\"%.3f\" class=\"timecode\"><span class=\"ref_index\">[&#%d;]</span><span class=\"time\">",
(int)QuoteUsername.Length, QuoteUsername.Base,
(int)DateString.Length, DateString.Base, // TODO(matt): Convert Unixtime to date-string
TimecodeToDottedSeconds(Timecode),
*QuoteIdentifier);
CopyTimecodeToBuffer(&MenuBuffers->Quote, Timecode);
CopyStringToBuffer(&MenuBuffers->Quote, "</span></span>\n"
" </div>\n"
" </span>\n"
" </a>\n");
if(!Timestamp->text[0])
{
CopyStringToBuffer(&IndexBuffers->Text, "&#8220;");
CopyStringToBufferHTMLSafe(&IndexBuffers->Text, QuoteInfo.Text);
CopyStringToBuffer(&IndexBuffers->Text, "&#8221;");
}
CopyStringToBuffer(&IndexBuffers->Text, "<sup>&#%d;</sup>", *QuoteIdentifier);
++*QuoteIdentifier;
}
else if(Result == RC_UNFOUND)
{
IndexingQuoteError(&Filepath, Timestamp->line, QuoteUsername, Timestamp->quote.id);
}
}
if(Result == RC_SUCCESS)
{
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"%.3f\": \"", TimecodeToDottedSeconds(Timecode));
if(Timestamp->quote.present && !Timestamp->text[0])
{
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\u201C");
CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, QuoteInfo.Text);
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\u201D");
}
else
{
CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, Wrap0(Timestamp->text));
}
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\n");
while(MarkerIndex < Timestamp->marker_count)
{
hsl_colour TopicColour = {};
Result = GenerateTopicColours(N, Wrap0(Timestamp->markers[MarkerIndex].marker), &TopicColour);
if(Result == RC_SUCCESS)
{
if(!*HasFilterMenu)
{
*HasFilterMenu = TRUE;
}
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0(Timestamp->markers[MarkerIndex].marker), &TopicColour);
++MarkerIndex;
}
else
{
break;
}
}
if(Result == RC_SUCCESS)
{
if(LocalTopics.ItemCount == 0)
{
hsl_colour TopicColour = {};
Result = GenerateTopicColours(N, Wrap0("nullTopic"), &TopicColour);
if(Result == RC_SUCCESS)
{
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0("nullTopic"), &TopicColour);
}
}
if(Result == RC_SUCCESS)
{
if(LocalMedia.ItemCount == 0)
{
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, DefaultMedium->ID, NULL);
}
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\">");
CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode);
CopyStringToBuffer(&IndexBuffers->Master, "</span>");
CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Text, 0, PAGE_PLAYER);
if(LocalTopics.ItemCount > 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\">");
CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode);
CopyStringToBuffer(&IndexBuffers->Master, "</span>");
CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Text, 0, PAGE_PLAYER);
if(LocalTopics.ItemCount > 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\">");
CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode);
CopyStringToBuffer(&IndexBuffers->Master, "</span>");
CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Text, 0, PAGE_PLAYER);
if(LocalTopics.ItemCount > 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);
}
}
}
}
FreeBook(&LocalTopics);
FreeBook(&LocalMedia);
}
else
{
IndexingChronologyError(&Filepath, Timestamp->line, Timecode, *PreviousTimecode);
Result = RC_ERROR_HMML;
}
MEM_TEST_END();
return Result;
}
string
GetNumberFromHMMLBaseFilename(string ProjectID, string HMMLBaseFilename)
{
// TODO(matt): That rigorous notion of numbering, goddammit?!
string Result = {};
if(HMMLBaseFilename.Length > ProjectID.Length)
{
Result = TrimString(HMMLBaseFilename, ProjectID.Length, 0);
}
return Result;
}
typedef struct
{
pair_type Type;
union
{
string String;
int64_t int64_t;
bool bool;
};
} typed_value;
typedef struct
{
typed_value Value;
int DigitsInHighestNumber;
uint64_t ZeroPadding;
} numbering_info;
numbering_info
GetEntryNumbering(project *P, string HMMLBaseFilename, string SpecifiedNumber, uint64_t EntryIndex, uint64_t EntryCount)
{
// TODO(matt): Handle P->Numbering.Scheme
numbering_info Result = {};
switch(P->Numbering.Method)
{
case NM_AUTO:
{
Result.Value.Type = PT_INT64;
uint64_t ThisNumber = EntryIndex + P->Numbering.Auto.StartingNumber;
uint64_t HighestNumber = EntryCount + P->Numbering.Auto.StartingNumber - 1;
Result.Value.int64_t = ThisNumber;
if(P->Numbering.Auto.ZeroPadded)
{
Result.DigitsInHighestNumber = DigitsInInt(&HighestNumber);
Result.ZeroPadding = DigitsInInt(&HighestNumber) - DigitsInInt(&ThisNumber);
}
} break;
case NM_FILENAME_DERIVED:
{
Result.Value.Type = PT_STRING;
Result.Value.String = TrimString(HMMLBaseFilename, P->Numbering.FilenameDerived.Prefix.Length, 0);
} break;
case NM_HMML_SPECIFIED:
{
Result.Value.Type = PT_STRING;
Result.Value.String = SpecifiedNumber;
} break;
default: break;
}
return Result;
}
string
BestTitleForHTML(project *Project)
{
return Project->HTMLTitle.Length ? Project->HTMLTitle : Project->Title;
}
void
CopyProjectWelcomeStringToBuffer(buffer *B, string ProjectTitle)
{
CopyStringToBuffer(B,
" <div class=\"episodeMarker first\"><div>&#8226;</div><div>Welcome to <cite>%.*s</cite></div><div>&#8226;</div></div>\n", (int)ProjectTitle.Length, ProjectTitle.Base);
}
void
CopyProjectEndStringToBuffer(buffer *B, string ProjectTitle)
{
CopyStringToBuffer(B,
" <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);
}
db_entry *
FieldClashes_String(db_header_project *Header, uint32_t FieldOffset, uint32_t FieldSize, string NewHMMLBaseFilename, string NewString)
{
db_entry *Result = 0;
db_entry *Test = LocateFirstEntry(Header);
for(int i = 0; i < Header->EntryCount; ++i, ++Test)
{
char *Ptr = (char *)Test;
Ptr += FieldOffset;
if(StringsMatch(NewString, Wrap0i_(Ptr, FieldSize)))
{
if(StringsDiffer(NewHMMLBaseFilename, Wrap0i(Test->HMMLBaseFilename)))
{
Result = Test;
}
break;
}
}
return Result;
}
rc
HMMLToBuffers(buffers *CollationBuffers, template *BespokeTemplate, string BaseFilename, neighbourhood *N, clash_resolver *ClashResolver, uint64_t EntryCount)
{
MEM_TEST_TOP();
rc Result = RC_SUCCESS;
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));
CollationBuffers->VODPlatform = VP_DEFAULT_UNSET;
// TODO(matt): A "MakeString0OnStack()" sort of function?
// NOTE(matt): Stack-string
int NullTerminationBytes = 1;
char Filepath[CurrentProject->HMMLDir.Length + sizeof("/")-1 + BaseFilename.Length + ExtensionStrings[EXT_HMML].Length + NullTerminationBytes];
char *P = Filepath;
P += CopyStringToBarePtr(P, CurrentProject->HMMLDir);
P += CopyStringToBarePtr(P, Wrap0("/"));
P += CopyStringToBarePtr(P, BaseFilename);
P += CopyStringToBarePtr(P, ExtensionStrings[EXT_HMML]);
*P = '\0';
string FilepathL = Wrap0(Filepath);
FILE *InFile = fopen(Filepath, "r");
if(!InFile)
{
sleep(1);
InFile = fopen(Filepath, "r");
}
if(InFile)
{
char *HMMLContents = ReadFileIntoMemory0(InFile);
HMML_Output HMML = hmml_parse(HMMLContents);
Free(HMMLContents); // TODO(matt): Maybe we'll need to free this later?
fclose(InFile);
if(HMML.well_formed)
{
string Title = {};
// TODO(matt): Somehow generate private entries, just putting them in a secret location?
// NOTE(matt): WorkingThis.Size > 0 means that this entry was previously processed as non-private. Would we rather
// reregister such an entry as newly private? This test lets us avoid calling VideoIsPrivate() for all
// entries, on every cinera invocation, when !CurrentProject->IgnorePrivacy
if(N->WorkingThis.Size > 0 || CurrentProject->IgnorePrivacy || !VideoIsPrivate(CurrentProject->VODPlatform, HMML.metadata.id))
{
// TODO(matt): Do a catch-all function that checks for missing info at the head, such as everything from here
// to the loop and additionally an indexer
if(BaseFilename.Length > MAX_BASE_FILENAME_LENGTH)
{
IndexingErrorSizing(&FilepathL, 0, "Base filename", BaseFilename, MAX_BASE_FILENAME_LENGTH);
Result = RC_ERROR_HMML;
}
if(!HMML.metadata.title)
{
IndexingError(FilepathL, 0, S_ERROR, "The [video] node lacks a \"title\"", 0);
Result = RC_ERROR_HMML;
}
else if(StringLength(HMML.metadata.title) > MAX_TITLE_LENGTH)
{
IndexingErrorSizing(&FilepathL, 0, "Video title", Wrap0(HMML.metadata.title), MAX_TITLE_LENGTH);
Result = RC_ERROR_HMML;
}
else
{
ClearCopyStringNoFormatOrTerminate(CollationBuffers->Title, sizeof(CollationBuffers->Title), Wrap0(HMML.metadata.title));
Title = Wrap0(HMML.metadata.title);
}
for(int CreditIndex = 0; CreditIndex < HMML.metadata.credit_count; ++CreditIndex)
{
HMML_Credit *This = HMML.metadata.credits + CreditIndex;
role *Role = GetRoleByID(Config, Wrap0(This->role));
if(!Role)
{
ErrorRole(FilepathL, Wrap0(This->role));
Result = CreditsError_NoRole; // TODO(matt): Is this a fine error here?
}
person *Person = GetPersonFromConfig(Wrap0(This->name));
if(!Person)
{
ErrorCredentials(FilepathL, Wrap0(This->name), Role);
Result = CreditsError_NoCredentials; // TODO(matt): Is this a fine error here?
}
}
if(!HMML.metadata.id)
{
IndexingError(FilepathL, 0, S_ERROR, "The [video] node lacks an \"id\"", 0);
Result = RC_ERROR_HMML;
}
else
{
CopyString(CollationBuffers->VideoID, sizeof(CollationBuffers->VideoID), "%s", HMML.metadata.id);
}
vod_platform VODPlatform = VP_DEFAULT_UNSET;
if(HMML.metadata.vod_platform)
{
VODPlatform = GetVODPlatform(Wrap0(HMML.metadata.vod_platform));
}
if(!IsValidVODPlatform(VODPlatform) && IsValidVODPlatform(CurrentProject->VODPlatform))
{
VODPlatform = CurrentProject->VODPlatform;
}
if(!IsValidVODPlatform(VODPlatform))
{
IndexingError(FilepathL, 0, S_ERROR, "The [video] node lacks a valid \"vod_platform\"", 0);
Print(stderr, " Valid vod_platforms:\n");
int FirstValidVODPlatform = 1;
for(int i = FirstValidVODPlatform; i < VP_COUNT; ++i)
{
Print(stderr, " %s\n", VODPlatformStrings[i]);
}
Result = RC_ERROR_HMML;
}
else
{
CollationBuffers->VODPlatform = VODPlatform;
}
medium *DefaultMedium = CurrentProject->DefaultMedium;
if(HMML.metadata.medium)
{
DefaultMedium = MediumExists(FilepathL, Wrap0(HMML.metadata.medium));
if(!DefaultMedium && CurrentProject->DefaultMedium)
{
Print(stderr, "Falling back to the default_medium set in the config: ");
PrintStringCN(CS_MAGENTA_BOLD, CurrentProject->DefaultMedium->ID, FALSE, TRUE);
DefaultMedium = CurrentProject->DefaultMedium;
}
}
if(!DefaultMedium)
{
IndexingError(FilepathL, 0, S_ERROR, "No default_medium set in config, or available medium set in the [video] node", 0);
Result = RC_ERROR_HMML;
}
if(CurrentProject->Numbering.Method == NM_HMML_SPECIFIED)
{
if(!HMML.metadata.number)
{
IndexingError(FilepathL, 0, S_ERROR, "No number set in the [video] node", 0);
Result = RC_ERROR_HMML;
}
else if(StringLength(HMML.metadata.number) > sizeof(N->WorkingThis.Number))
{
IndexingErrorSizing(&FilepathL, 0, "number", Wrap0(HMML.metadata.number), MAX_NUMBER_LENGTH);
Result = RC_ERROR_HMML;
}
else
{
ClearCopyStringNoFormatOrTerminate(N->WorkingThis.Number, sizeof(N->WorkingThis.Number), Wrap0(HMML.metadata.number));
}
}
string CCLang = CurrentProject->DefaultCCLang;
if(HMML.metadata.cc_lang)
{
CCLang = Wrap0(HMML.metadata.cc_lang);
if(!IsValidLanguageCode(CCLang))
{
IndexingErrorInvalidLanguageCode(&FilepathL, 0, "cc_lang", CCLang);
PrintValidLanguageCodeChars();
Result = RC_ERROR_HMML;
}
}
string OutputLocation = {};
if(!HMML.metadata.output)
{
OutputLocation = BaseFilename;
}
else if(StringLength(HMML.metadata.output) > sizeof(N->WorkingThis.OutputLocation))
{
IndexingErrorSizing(&FilepathL, 0, "output", Wrap0(HMML.metadata.output), MAX_ENTRY_OUTPUT_LENGTH);
Result = RC_ERROR_HMML;
}
else if(StringLength(HMML.metadata.output) == 0)
{
IndexingErrorEmptyField(&FilepathL, 0, "output");
Result = RC_ERROR_HMML;
}
else if(StringContains(Wrap0(HMML.metadata.output), Wrap0("/")))
{
IndexingErrorInvalidSubstring(&FilepathL, 0, "output", Wrap0(HMML.metadata.output), Wrap0("/"));
Result = RC_ERROR_HMML;
}
else
{
OutputLocation = Wrap0(HMML.metadata.output);
}
if(Result == RC_SUCCESS) // Clash Tests
{
if(!ClashResolver->Resolving)
{
db_entry *ClashTest = FieldClashes_String(N->Project, offsetof(db_entry, OutputLocation), sizeof(N->WorkingThis.OutputLocation), BaseFilename, OutputLocation);
if(ClashTest)
{
// NOTE(matt): This clash is fatal because letting it go through would make the encroaching entry's
// HTML file overwrite that of an existing entry
PushClash(ClashResolver->Main, CurrentProject, ClashTest, BaseFilename, Wrap0i(N->WorkingThis.OutputLocation), OutputLocation);
//IndexingErrorClash(&FilepathL, 0, S_ERROR, "output", OutputLocation, Wrap0i(ClashTest->HMMLBaseFilename));
Result = RC_ERROR_HMML;
}
else
{
clash_entry *IncumbentSelf = GetClash(ClashResolver->Main, CurrentProject, BaseFilename);
if(IncumbentSelf)
{
ClearCopyStringNoFormatOrTerminate(IncumbentSelf->DesiredOutputValue, sizeof(IncumbentSelf->DesiredOutputValue), OutputLocation);
}
}
}
if(CurrentProject->Numbering.Method == NM_HMML_SPECIFIED)
{
db_entry *ClashTest = FieldClashes_String(N->Project, offsetof(db_entry, Number), sizeof(N->WorkingThis.Number), BaseFilename, Wrap0i(N->WorkingThis.Number));
if(ClashTest)
{
// NOTE(matt): This clash is nonfatal because the number has no purpose other than to be
// displayed beside the entry's title on the search page
IndexingErrorClash(&FilepathL, 0, S_WARNING, "number", Wrap0i(N->WorkingThis.Number), Wrap0i(ClashTest->HMMLBaseFilename));
}
}
}
string ProjectTitle = BestTitleForHTML(CurrentProject);
// 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.metadata.custom_count; ++CustomIndex)
{
HMML_VideoCustomMetaData *This = HMML.metadata.custom + CustomIndex;
misc_attribute *ThisAttribute = GetMiscAttribute(This);
if(ThisAttribute && ThisAttribute->Type == MAT_CUSTOM)
{
if(StringLength(This->value) > (ThisAttribute->CustomIndex < 12 ? MAX_CUSTOM_SNIPPET_SHORT_LENGTH : MAX_CUSTOM_SNIPPET_LONG_LENGTH))
{
IndexingErrorCustomSizing(&FilepathL, 0, ThisAttribute->CustomIndex, Wrap0(This->value));
Result = RC_ERROR_HMML;
}
else
{
switch(ThisAttribute->CustomIndex)
{
case 0: CopyStringNoFormat(CollationBuffers->Custom0, sizeof(CollationBuffers->Custom0), Wrap0(This->value)); break;
case 1: CopyStringNoFormat(CollationBuffers->Custom1, sizeof(CollationBuffers->Custom1), Wrap0(This->value)); break;
case 2: CopyStringNoFormat(CollationBuffers->Custom2, sizeof(CollationBuffers->Custom2), Wrap0(This->value)); break;
case 3: CopyStringNoFormat(CollationBuffers->Custom3, sizeof(CollationBuffers->Custom3), Wrap0(This->value)); break;
case 4: CopyStringNoFormat(CollationBuffers->Custom4, sizeof(CollationBuffers->Custom4), Wrap0(This->value)); break;
case 5: CopyStringNoFormat(CollationBuffers->Custom5, sizeof(CollationBuffers->Custom5), Wrap0(This->value)); break;
case 6: CopyStringNoFormat(CollationBuffers->Custom6, sizeof(CollationBuffers->Custom6), Wrap0(This->value)); break;
case 7: CopyStringNoFormat(CollationBuffers->Custom7, sizeof(CollationBuffers->Custom7), Wrap0(This->value)); break;
case 8: CopyStringNoFormat(CollationBuffers->Custom8, sizeof(CollationBuffers->Custom8), Wrap0(This->value)); break;
case 9: CopyStringNoFormat(CollationBuffers->Custom9, sizeof(CollationBuffers->Custom9), Wrap0(This->value)); break;
case 10: CopyStringNoFormat(CollationBuffers->Custom10, sizeof(CollationBuffers->Custom10), Wrap0(This->value)); break;
case 11: CopyStringNoFormat(CollationBuffers->Custom11, sizeof(CollationBuffers->Custom11), Wrap0(This->value)); break;
case 12: CopyStringNoFormat(CollationBuffers->Custom12, sizeof(CollationBuffers->Custom12), Wrap0(This->value)); break;
case 13: CopyStringNoFormat(CollationBuffers->Custom13, sizeof(CollationBuffers->Custom13), Wrap0(This->value)); break;
case 14: CopyStringNoFormat(CollationBuffers->Custom14, sizeof(CollationBuffers->Custom14), Wrap0(This->value)); break;
case 15: CopyStringNoFormat(CollationBuffers->Custom15, sizeof(CollationBuffers->Custom15), Wrap0(This->value)); break;
}
}
}
}
if(Result == RC_SUCCESS && !CurrentProject->DenyBespokeTemplates && HMML.metadata.template)
{
Result = PackTemplate(BespokeTemplate, Wrap0(HMML.metadata.template), TEMPLATE_BESPOKE, CurrentProject);
}
if(Result == RC_SUCCESS)
{
ClearCopyStringNoFormatOrTerminate(N->WorkingThis.OutputLocation, sizeof(N->WorkingThis.OutputLocation), OutputLocation);
buffer URLPlayer = {};
ClaimBuffer(&URLPlayer, BID_URL_PLAYER, MAX_BASE_URL_LENGTH + SLASH + MAX_RELATIVE_PAGE_LOCATION_LENGTH + SLASH + sizeof(N->WorkingThis.OutputLocation));
ConstructPlayerURL(&URLPlayer, N->Project, Wrap0i(N->WorkingThis.OutputLocation));
CopyString(CollationBuffers->URLPlayer, sizeof(CollationBuffers->URLPlayer), "%s", URLPlayer.Location);
DeclaimBuffer(&URLPlayer);
if(!ClashResolver->Resolving || ClashResolver->ChainStructure == CS_OPEN_ENDED)
{
if(N->This)
{
string OldOutputLocation = Wrap0i(N->This->OutputLocation);
string NewOutputLocation = Wrap0i(N->WorkingThis.OutputLocation);
if(StringsDiffer(OldOutputLocation, NewOutputLocation))
{
DeletePlayerPageFromFilesystem(CurrentProject->BaseDir, CurrentProject->PlayerLocation, OldOutputLocation, FALSE, TRUE);
VacatePotentialClash(ClashResolver->Main, CurrentProject, BaseFilename);
}
}
ResolveClash(ClashResolver->Main, CurrentProject, BaseFilename);
}
// TODO(matt): Handle art and art_variants, once .hmml supports them
#if DEBUG
Print(stdout,
"================================================================================\n"
"%.*s\n"
"================================================================================\n",
(int)BaseFilename.Length, BaseFilename.Base);
#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(ClaimMenuIndexAndPlayerBuffers(&MenuBuffers, &IndexBuffers, &PlayerBuffers) == RC_SUCCESS)
{
memory_book Strings = InitBookOfStrings(Kilobytes(4));
_memory_book(ref_info) ReferencesArray = InitBook(sizeof(ref_info), 8);
speakers Speakers = InitSpeakers();
memory_book Topics = InitBook(sizeof(category_info), 8);
memory_book Media = InitBook(sizeof(category_info), 8);
bool HasQuoteMenu = FALSE;
bool HasReferenceMenu = FALSE;
bool HasFilterMenu = FALSE;
int QuoteIdentifier = 0x3b1;
int RefIdentifier = 1;
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");
switch(CollationBuffers->VODPlatform)
{
case VP_VIMEO:
{
CopyStringToBuffer(&PlayerBuffers.Main, "<script src=\"https://player.vimeo.com/api/player.js\"></script>\n"
" ");
} break;
case VP_YOUTUBE:
{
CopyStringToBuffer(&PlayerBuffers.Main, "<script src=\"https://www.youtube.com/iframe_api\"></script>\n"
" ");
} break;
default: break;
}
CopyStringToBuffer(&PlayerBuffers.Main,
"<div class=\"cineraPlayerContainer\">\n"
" <div class=\"video_container\" data-platform=\"%s\" data-videoId=\"%s\"", VODPlatformStrings[CollationBuffers->VODPlatform], HMML.metadata.id);
if(CCLang.Length > 0)
{
CopyStringToBuffer(&PlayerBuffers.Main,
" data-ccLang=\"%.*s\"", (int)CCLang.Length, CCLang.Base);
}
CopyStringToBuffer(&PlayerBuffers.Main,
"></div>\n"
" <div class=\"markers_container %.*s\">\n", (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));
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
{
CopyProjectWelcomeStringToBuffer(&PlayerBuffers.Main, ProjectTitle);
}
}
N->WorkingThis.LinkOffsets.PrevEnd = (PlayerBuffers.Main.Ptr - PlayerBuffers.Main.Location - N->WorkingThis.LinkOffsets.PrevStart);
}
CopyStringToBuffer(&PlayerBuffers.Main,
" <div class=\"markers\">\n");
bool RequiresCineraJS = FALSE;
Result = BuildCredits(FilepathL, &MenuBuffers.Credits, &HMML.metadata, &Speakers, &RequiresCineraJS);
if(Result == RC_SUCCESS)
{
CopyStringToBuffer(&CollationBuffers->SearchEntry, "location: \"%.*s\"\n", (int)OutputLocation.Length, OutputLocation.Base);
CopyStringToBuffer(&CollationBuffers->SearchEntry, "number: \"");
numbering_info Number = GetEntryNumbering(CurrentProject, BaseFilename, Wrap0i(N->WorkingThis.Number), N->ThisIndex, EntryCount);
switch(Number.Value.Type)
{
case PT_STRING:
{
CopyStringToBuffer(&CollationBuffers->SearchEntry, "%.*s", (int)Number.Value.String.Length, Number.Value.String.Base);
} break;
case PT_INT64:
{
CopyStringToBuffer(&CollationBuffers->SearchEntry, "%0*li", Number.DigitsInHighestNumber, Number.Value.int64_t);
} break;
default: break;
}
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\n");
CopyStringToBuffer(&CollationBuffers->SearchEntry, "title: \"");
CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, Wrap0(HMML.metadata.title));
CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\n"
"markers:\n");
#if DEBUG
Print(stdout, "\n\n --- Entering Timestamps Loop ---\n\n\n\n");
#endif
v4 PreviousTimecode = {};
for(int TimestampIndex = 0; TimestampIndex < HMML.timestamp_count; ++TimestampIndex)
{
// TODO(matt): Thoroughly test this reorganisation
//
// Make sure to:
// • verify that the Privacy thing remains okay
// • manage that memory
//
#if DEBUG
Print(stdout, "%d\n", TimestampIndex);
#endif
HMML_Timestamp *Timestamp = HMML.timestamps + TimestampIndex;
string Author = {};
if(Timestamp->author) { Author = Wrap0(Timestamp->author); }
/* */ MEM_TEST_MID();
/* +MEM */ Result = ProcessTimestamp(CollationBuffers, N, Wrap0(Filepath), &Strings,
/* */ &MenuBuffers, &IndexBuffers, &PlayerBuffers,
/* */ DefaultMedium, &Speakers, Author, &ReferencesArray,
/* */ &HasQuoteMenu, &HasReferenceMenu, &HasFilterMenu, &RequiresCineraJS,
/* */ &QuoteIdentifier, &RefIdentifier,
/* */ &Topics, &Media,
/* */ Timestamp, &PreviousTimecode);
/* */ MEM_TEST_MID();
if(Result != RC_SUCCESS)
{
break;
}
}
if(Result == RC_SUCCESS)
{
DeclaimIndexBuffers(&IndexBuffers);
CopyStringToBuffer(&CollationBuffers->SearchEntry, "---\n");
N->WorkingThis.Size = CollationBuffers->SearchEntry.Ptr - CollationBuffers->SearchEntry.Location;
#if DEBUG
Print(stdout, "\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 < ReferencesArray.ItemCount; ++i)
{
ref_info *This = GetPlaceInBook(&ReferencesArray, i);
CopyStringToBuffer(&MenuBuffers.Reference,
" <a data-id=\"%s\" href=\"%.*s\" target=\"_blank\" class=\"ref\">\n"
" <span class=\"ref_content\">\n",
This->ID,
(int)This->URL.Length, This->URL.Base);
if(This->Source.Length > 0)
{
CopyStringToBuffer(&MenuBuffers.Reference,
" <div class=\"source\">");
CopyStringToBufferHTMLSafeBreakingOnSlash(&MenuBuffers.Reference, This->Source);
CopyStringToBuffer(&MenuBuffers.Reference, "</div>\n");
}
CopyStringToBuffer(&MenuBuffers.Reference,
" <div class=\"ref_title\">");
CopyStringToBufferHTMLSafeBreakingOnSlash(&MenuBuffers.Reference, This->RefTitle);
CopyStringToBuffer(&MenuBuffers.Reference, "</div>\n");
CopyStringToBuffer(&MenuBuffers.Reference,
" </span>\n");
for(int j = 0; j < This->Identifier.ItemCount;)
{
CopyStringToBuffer(&MenuBuffers.Reference,
" <div class=\"ref_indices\">\n ");
for(int k = 0; k < 3 && j < This->Identifier.ItemCount; ++k, ++j)
{
identifier *ThisIdentifier = GetPlaceInBook(&This->Identifier, j);
CopyStringToBuffer(&MenuBuffers.Reference,
"<span data-timestamp=\"%.3f\" class=\"timecode\"><span class=\"ref_index\">[%d]</span><span class=\"time\">", TimecodeToDottedSeconds(ThisIdentifier->Timecode), ThisIdentifier->Identifier);
CopyTimecodeToBuffer(&MenuBuffers.Reference, ThisIdentifier->Timecode);
CopyStringToBuffer(&MenuBuffers.Reference, "</span></span>");
}
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_header\">\n"
" <div class=\"filter_mode exclusive\">Filter mode: </div>\n"
" <div class=\"filter_titles\">\n");
if(Topics.ItemCount > 0)
{
CopyStringToBuffer(&MenuBuffers.Filter,
" <div class=\"filter_title\">Topics</div>\n");
}
if(Media.ItemCount > 0)
{
CopyStringToBuffer(&MenuBuffers.Filter,
" <div class=\"filter_title\">Media</div>\n");
}
CopyStringToBuffer(&MenuBuffers.Filter,
" </div>\n"
" </div>\n"
" <div class=\"filters\">\n");
if(Topics.ItemCount > 0)
{
CopyStringToBuffer(&MenuBuffers.Filter,
" <div class=\"filter_topics\">\n");
for(int i = 0; i < Topics.ItemCount; ++i)
{
category_info *This = GetPlaceInBook(&Topics, i);
// NOTE(matt): Stack-string
char SanitisedMarker[This->Marker.Length + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)This->Marker.Length, This->Marker.Base);
SanitisePunctuation(SanitisedMarker);
bool NullTopic = StringsMatch(This->Marker, Wrap0("nullTopic"));
CopyStringToBuffer(&MenuBuffers.FilterTopics,
" <div%s class=\"filter_content %s\">\n"
" <span class=\"icon category %s\"",
NullTopic ? " title=\"Timestamps that don't fit into the above topic(s) may be filtered using this pseudo-topic\"" : "",
SanitisedMarker,
SanitisedMarker);
if(IsValidHSLColour(This->Colour))
{
CopyStringToBuffer(&MenuBuffers.FilterTopics,
" data-hue=\"%u\" data-saturation=\"%u%%\"",
This->Colour.Hue, This->Colour.Saturation);
}
CopyStringToBuffer(&MenuBuffers.FilterTopics,
"></span><span class=\"cineraText\">%.*s</span>\n"
" </div>\n",
NullTopic ? (int)sizeof("(null topic)")-1 : (int)This->Marker.Length,
NullTopic ? "(null topic)" : This->Marker.Base);
}
CopyStringToBuffer(&MenuBuffers.FilterTopics,
" </div>\n");
CopyLandmarkedBuffer(&MenuBuffers.Filter, &MenuBuffers.FilterTopics, 0, PAGE_PLAYER);
}
if(Media.ItemCount > 0)
{
CopyStringToBuffer(&MenuBuffers.FilterMedia,
" <div class=\"filter_media\">\n");
for(int i = 0; i < Media.ItemCount; ++i)
{
category_info *This = GetPlaceInBook(&Media, i);
// NOTE(matt): Stack-string
char SanitisedMarker[This->Marker.Length + 1];
CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)This->Marker.Length, This->Marker.Base);
SanitisePunctuation(SanitisedMarker);
medium *Medium = GetMediumFromProject(CurrentProject, This->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=\"cineraHelp\">\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 timestamp</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 and <span class=\"help_title_key help_custom_index\">Index</span> Controls</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 help_custom_index\">w</span><br>\n"
" <span class=\"help_key help_custom_index\">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 help_custom_index\">j</span>\n"
" <span class=\"help_key help_custom_index\">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><br>\n"
" <div class=\"help_paragraph\">\n"
" <span class=\"help_key help_custom_index\">Esc</span> <span class=\"help_text\">Close menu / unfocus timestamp</span>\n"
" </div>\n"
" <br>\n");
CopyStringToBuffer(&PlayerBuffers.Menus,
" <h2>%sQuotes %sand%s References%s Menus and%s Index</h2>\n"
" <span class=\"help_key word\">Enter</span> <span class=\"help_text\">Jump to timestamp</span><br>\n",
// Q R
//
// 0 0 <h2><span off>Quotes and References Menus and</span> Index</h2>
// 0 1 <h2><span off>Quotes and</span> References Menus and Index</h2>
// 1 0 <h2>Quotes <span off>and References</span> Menus and Index</h2>
// 1 1 <h2>Quotes and References Menus and Index</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));
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
{
CopyProjectEndStringToBuffer(&PlayerBuffers.Main, ProjectTitle);
}
}
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"
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n",
CINERA_APP_VERSION.Major,
CINERA_APP_VERSION.Minor,
CINERA_APP_VERSION.Patch);
if(Topics.ItemCount || Media.ItemCount)
{
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
" <meta name=\"keywords\" content=\"");
if(Topics.ItemCount > 0)
{
for(int i = 0; i < Topics.ItemCount; ++i)
{
category_info *This = GetPlaceInBook(&Topics, i);
if(!StringsMatch(This->Marker, Wrap0("nullTopic")))
{
CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "%.*s, ", (int)This->Marker.Length, This->Marker.Base);
}
}
}
if(Media.ItemCount > 0)
{
for(int i = 0; i < Media.ItemCount; ++i)
{
category_info *This = GetPlaceInBook(&Media, i);
CopyStringToBuffer(&CollationBuffers->IncludesPlayer, "%.*s, ", (int)This->WrittenText.Length, This->WrittenText.Base);
}
}
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.Tags.ItemCount > 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>");
CopyStringToBuffer(&CollationBuffers->IncludesPlayer,
"\n"
"\n <meta property=\"cinera:type\" content=\"entry\">");
CopyStringToBufferHTMLSafeSurrounded(&CollationBuffers->IncludesPlayer,
Wrap0("\n <meta property=\"cinera:title\" content=\""), Title, Wrap0("\">"));
CopyStringToBufferHTMLSafeSurrounded(&CollationBuffers->IncludesPlayer,
Wrap0("\n <meta property=\"cinera:project\" content=\""), CurrentProject->Title, Wrap0("\">"));
CopyStringToBufferHTMLSafeSurrounded(&CollationBuffers->IncludesPlayer,
Wrap0("\n <meta property=\"cinera:project_lineage\" content=\""), CurrentProject->WrittenLineage, Wrap0("\">"));
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>");
CopyStringToBuffer(&CollationBuffers->Player, "<div class=\"cinera\">\n"
" ");
asset *JSClear = GetAsset(Wrap0(BuiltinAssets[ASSET_JS_CLEAR].Filename), ASSET_JS);
ConstructResolvedAssetURL(&URL, JSClear, PAGE_PLAYER);
CopyStringToBuffer(&CollationBuffers->Player,
"<script type=\"text/javascript\" src=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->Player, JSClear, PAGE_PLAYER, 0);
CopyStringToBuffer(&CollationBuffers->Player,
"\"></script>");
CopyStringToBuffer(&CollationBuffers->Player, "\n"
" ");
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);
}
}
FreeBook(&Strings);
FreeReferences(&ReferencesArray);
FreeSpeakers(&Speakers);
FreeBook(&Topics);
FreeBook(&Media);
}
else
{
Result = RC_ARENA_FULL;
}
HMMLCleanup();
}
}
else
{
N->WorkingThis.LinkOffsets.PrevStart = 0;
N->WorkingThis.LinkOffsets.PrevEnd = 0;
Result = RC_PRIVATE_VIDEO;
}
if(Result != RC_SUCCESS)
{
PrintEdit(EDIT_SKIP, CurrentProject->Lineage, BaseFilename, HMML.metadata.title ? &Title : 0, FALSE);
}
}
else
{
LogError(LOG_ERROR, "%s:%d: %s", Filepath, HMML.error.line, HMML.error.message);
IndexingError(FilepathL, HMML.error.line, S_ERROR, HMML.error.message, 0);
PrintEdit(EDIT_SKIP, CurrentProject->Lineage, BaseFilename, 0, FALSE);
Result = RC_ERROR_HMML;
}
hmml_free(&HMML);
}
else
{
// TODO(matt): SystemError()
LogError(LOG_ERROR, "Unable to open %s: %s", Filepath, strerror(errno));
Print(stderr, "Unable to open %s: %s\n", Filepath, strerror(errno));
Result = RC_ERROR_FILE;
}
MEM_TEST_END();
return Result;
}
typedef struct
{
char *String;
bool SelfClosing;
} html_element;
html_element HTMLElements[] =
{
{ "a", FALSE },
{ "br", TRUE },
{ "div", FALSE },
{ "h1", FALSE },
{ "h2", FALSE },
{ "img", TRUE },
{ "input", TRUE },
{ "label", FALSE },
{ "li", FALSE },
{ "nav", FALSE },
{ "p", FALSE },
{ "script", FALSE },
{ "span", FALSE },
{ "ul", FALSE },
};
typedef enum
{
NODE_A,
NODE_BR,
NODE_DIV,
NODE_H1,
NODE_H2,
NODE_IMG,
NODE_INPUT,
NODE_LABEL,
NODE_LI,
NODE_NAV,
NODE_P,
NODE_SCRIPT,
NODE_SPAN,
NODE_UL,
} html_element_id;
void
OpenNode(buffer *B, uint32_t *IndentationLevel, html_element_id Element, char *ID)
{
// NOTE(matt): Leaves the node open for further additions, e.g. attributes, classes
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)
{
// NOTE(matt): Closes the node
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(">"));
}
void
AppendScriptNode(buffer *B, buffer *URL, uint32_t *IndentationLevel, asset *Asset, bool Defer, page_type PageType)
{
OpenNodeNewLine(B, IndentationLevel, NODE_SCRIPT, 0);
AppendStringToBuffer(B, Wrap0(" src=\""));
ConstructResolvedAssetURL(URL, Asset, PageType);
AppendStringToBuffer(B, Wrap0(URL->Location));
DeclaimBuffer(URL);
PushAssetLandmark(B, Asset, PageType, TRUE);
AppendStringToBuffer(B, Wrap0("\""));
if(Defer)
{
AppendStringToBuffer(B, Wrap0(" defer"));
}
AppendStringToBuffer(B, Wrap0(">"));
CloseNode(B, IndentationLevel, NODE_SCRIPT);
}
void
GenerateNavigationRecursively(project *Current, project *Target, navigation_buffer *NavBuffer, uint32_t *IndentationLevel)
{
buffer *B = &NavBuffer->Buffer;
if(Target == Current)
{
OpenNodeNewLine(B, IndentationLevel, NODE_LI, 0);
AppendStringToBuffer(B, Wrap0(" class=\"current\">"));
}
else
{
OpenNodeCNewLine(B, IndentationLevel, NODE_LI, 0);
}
OpenNode(B, IndentationLevel, NODE_A, 0);
AppendStringToBuffer(B, Wrap0(" href=\""));
buffer URL;
ClaimBuffer(&URL, BID_URL_SEARCH, MAX_BASE_URL_LENGTH + 1 + MAX_RELATIVE_PAGE_LOCATION_LENGTH + 1);
ConstructSearchURL(&URL, Current);
AppendBuffer(B, &URL);
DeclaimBuffer(&URL);
AppendStringToBuffer(B, Wrap0("\">"));
AppendStringToBuffer(B, Current->HTMLTitle.Length ? Current->HTMLTitle : Current->Title);
CloseNode(B, IndentationLevel, NODE_A);
if(Current->Child.ItemCount > 0)
{
OpenNodeCNewLine(B, IndentationLevel, NODE_UL, 0);
}
for(int i = 0; i < Current->Child.ItemCount; ++i)
{
GenerateNavigationRecursively(GetPlaceInBook(&Current->Child, i), Target, NavBuffer, IndentationLevel);
}
if(Current->Child.ItemCount > 0)
{
CloseNodeNewLine(B, IndentationLevel, NODE_UL);
CloseNodeNewLine(B, IndentationLevel, NODE_LI);
}
else
{
CloseNode(B, IndentationLevel, NODE_LI);
}
}
void
GenerateNavigation(config *C, project *Target, navigation_buffer *NavBuffer)
{
uint32_t IndentationLevel = 0;
uint32_t DropdownIndent = 0;
buffer *B = &NavBuffer->Buffer;
string Theme = C ? C->GlobalTheme : Target->Theme;
switch(NavBuffer->Spec.Type)
{
case NT_DROPDOWN:
{
//IndentationLevel = 0;
OpenNode(B, &IndentationLevel, NODE_NAV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraNavDropdown "));
AppendStringToBuffer(B, Theme);
AppendStringToBuffer(B, Wrap0("\">"));
OpenNodeNewLine(B, &IndentationLevel, NODE_NAV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraNavTitle\">"));
AppendStringToBuffer(B, Target ? Target->HTMLTitle.Length ? Target->HTMLTitle : Target->Title : Wrap0("Indexed Episode Guide"));
CloseNode(B, &IndentationLevel, NODE_NAV);
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraPositioner\">"));
DropdownIndent = IndentationLevel;
} // NOTE(matt): Intentional fall-through
case NT_HORIZONTAL:
{
//IndentationLevel = 0;
OpenNode(B, &IndentationLevel, NODE_UL, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraNavHorizontal "));
AppendStringToBuffer(B, Theme);
AppendStringToBuffer(B, Wrap0("\">"));
} break;
case NT_PLAIN:
{
//IndentationLevel = 0;
OpenNode(B, &IndentationLevel, NODE_UL, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraNavPlain "));
AppendStringToBuffer(B, Theme);
AppendStringToBuffer(B, Wrap0("\">"));
} break;
case NT_NULL:
case NT_COUNT: break;
};
//IndentationLevel = 1;
if(NavBuffer->Spec.ChildrenOf)
{
project *Parent = NavBuffer->Spec.ChildrenOf;
for(int i = 0; i < Parent->Child.ItemCount; ++i)
{
GenerateNavigationRecursively(GetPlaceInBook(&Parent->Child, i), Target, NavBuffer, &IndentationLevel);
}
}
else
{
for(int i = 0; i < Config->Project.ItemCount; ++i)
{
GenerateNavigationRecursively(GetPlaceInBook(&Config->Project, i), Target, NavBuffer, &IndentationLevel);
}
}
CloseNodeNewLine(B, &IndentationLevel, NODE_UL);
if(NavBuffer->Spec.Type == NT_DROPDOWN)
{
CloseNodeNewLine(B, &DropdownIndent, NODE_DIV);
CloseNodeNewLine(B, &DropdownIndent, NODE_NAV);
}
}
void
SortLandmarks(memory_book *A)
{
for(int AssetIndex = 0; AssetIndex < A->ItemCount; ++AssetIndex)
{
asset *This = GetPlaceInBook(A, AssetIndex);
for(int SearchLandmarkIndex = 0; SearchLandmarkIndex < This->Search.ItemCount; ++SearchLandmarkIndex)
{
landmark *A = GetPlaceInBook(&This->Search, SearchLandmarkIndex);
for(int TestIndex = SearchLandmarkIndex + 1; TestIndex < This->Search.ItemCount; ++TestIndex)
{
landmark *B = GetPlaceInBook(&This->Search, TestIndex);
if(A->Offset > B->Offset)
{
landmark Temp = *A;
*A = *B;
*B = Temp;
}
}
}
for(int PlayerLandmarkIndex = 0; PlayerLandmarkIndex < This->Player.ItemCount; ++PlayerLandmarkIndex)
{
landmark *A = GetPlaceInBook(&This->Player, PlayerLandmarkIndex);
for(int TestIndex = PlayerLandmarkIndex + 1; TestIndex < This->Player.ItemCount; ++TestIndex)
{
landmark *B = GetPlaceInBook(&This->Player, TestIndex);
if(A->Offset > B->Offset)
{
landmark Temp = *A;
*A = *B;
*B = Temp;
}
}
}
}
}
rc
BuffersToHTML(config *C, project *Project, buffers *CollationBuffers, template *Template, char *OutputPath, page_type PageType, bool GlobalSearch, unsigned int *PlayerOffset)
{
rc Result = RC_SUCCESS;
MEM_TEST_TOP();
#if DEBUG
Print(stdout, "\n\n --- Buffer Collation ---\n"
" %s\n\n\n", OutputPath);
#endif
#if DEBUG_MEM
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
WriteToFile(MemLog, "\nEntered BuffersToHTML(");
if(OutputPath)
{
WriteToFile(MemLog, "%s", OutputPath);
}
else
{
WriteToFile(MemLog, "%.*s", (int)Project->PlayerLocation.Length, Project->PlayerLocation.Base);
}
WriteToFile(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))// || Project->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));
Result = RC_ERROR_MEMORY;
goto End;
}
#if DEBUG_MEM
MemLog = fopen("/home/matt/cinera_mem", "a+");
WriteToFile(MemLog, " Allocated Master (%ld)\n", Master.Size);
fclose(MemLog);
Print(stdout, " 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.Tags.ItemCount; ++i)
{
tag_offset *Tag = GetPlaceInBook(&Template->Metadata.Tags, i);
for(int j = 0; Tag->Offset > j; ++j)
{
*Master.Ptr++ = *Template->File.Buffer.Ptr++;
}
// 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(Tag->TagCode)
{
case TAG_PROJECT:
{
if(Project->HTMLTitle.Length > 0) { CopyStringToBufferNoFormat(&Master, Project->HTMLTitle); } // NOTE(matt): Not HTML-safe
else { CopyStringToBufferHTMLSafe(&Master, Project->Title); }
} break;
case TAG_PROJECT_ID: CopyStringToBufferNoFormat(&Master, Project->ID); break; // NOTE(matt): Not HTML-safe
case TAG_PROJECT_LINEAGE: CopyStringToBufferHTMLSafe(&Master, Project->WrittenLineage); break;
case TAG_PROJECT_PLAIN: CopyStringToBufferHTMLSafe(&Master, Project->Title); break;
case TAG_SEARCH_URL: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->URLSearch)); break; // NOTE(matt): Not HTML-safe
case TAG_THEME: CopyStringToBufferNoFormat(&Master, Project->Theme); break; // NOTE(matt): Not HTML-safe
case TAG_TITLE: CopyStringToBufferHTMLSafe(&Master, Wrap0i(CollationBuffers->Title)); break;
case TAG_URL:
if(PageType == PAGE_PLAYER)
{
CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->URLPlayer));
}
else
{
if(GlobalSearch)
{
CopyStringToBufferNoFormat(&Master, Config->GlobalSearchURL);
}
else
{
CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->URLSearch));
}
}
break;
case TAG_VIDEO_ID: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->VideoID)); break;
case TAG_VOD_PLATFORM: CopyStringToBufferNoFormat(&Master, Wrap0(VODPlatformStrings[CollationBuffers->VODPlatform])); break;
case TAG_SEARCH:
{
/* */ MEM_TEST_MID();
/* +MEM */ CopyLandmarkedBuffer(&Master, &CollationBuffers->Search, 0, PageType);
/* */ MEM_TEST_MID();
} 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:
{
/* */ MEM_TEST_MID();
/* +MEM */ CopyLandmarkedBuffer(&Master, &CollationBuffers->Player, PlayerOffset, PageType);
/* */ MEM_TEST_MID();
} break;
case TAG_ASSET:
{
buffer URL;
ConstructResolvedAssetURL(&URL, Tag->Asset, PageType);
CopyStringToBuffer(&Master, "%s", URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&Master, Tag->Asset, PageType, 0);
} break;
case TAG_CSS:
{
buffer URL;
ConstructResolvedAssetURL(&URL, Tag->Asset, PageType);
CopyStringToBuffer(&Master,
"<link rel=\"stylesheet\" type=\"text/css\" href=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&Master, Tag->Asset, PageType, 0);
CopyStringToBuffer(&Master, "\">");
} break;
case TAG_IMAGE:
{
buffer URL;
ConstructResolvedAssetURL(&URL, Tag->Asset, PageType);
CopyStringToBuffer(&Master, "%s", URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&Master, Tag->Asset, PageType, 0);
} break;
case TAG_JS:
{
buffer URL;
ConstructResolvedAssetURL(&URL, Tag->Asset, PageType);
CopyStringToBuffer(&Master,
"<script type=\"text/javascript\" src=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&Master, Tag->Asset, PageType, 0);
CopyStringToBuffer(&Master, "\"></script>");
} break;
case TAG_NAV:
case TAG_GLOBAL_NAV:
{
if(!Tag->Nav->Buffer.Location)
{
GenerateNavigation(C, Project, Tag->Nav);
}
// TODO(matt): If we add project icons to the nav - and, frankly, we will want project art at some
// point - we'll need to correctly offset its landmarks here
CopyLandmarkedBuffer(&Master, &Tag->Nav->Buffer, 0, PageType);
} break;
case TAG_CUSTOM0: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom0)); break;
case TAG_CUSTOM1: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom1)); break;
case TAG_CUSTOM2: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom2)); break;
case TAG_CUSTOM3: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom3)); break;
case TAG_CUSTOM4: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom4)); break;
case TAG_CUSTOM5: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom5)); break;
case TAG_CUSTOM6: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom6)); break;
case TAG_CUSTOM7: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom7)); break;
case TAG_CUSTOM8: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom8)); break;
case TAG_CUSTOM9: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom9)); break;
case TAG_CUSTOM10: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom10)); break;
case TAG_CUSTOM11: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom11)); break;
case TAG_CUSTOM12: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom12)); break;
case TAG_CUSTOM13: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom13)); break;
case TAG_CUSTOM14: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom14)); break;
case TAG_CUSTOM15: CopyStringToBufferNoFormat(&Master, Wrap0i(CollationBuffers->Custom15)); break;
case TEMPLATE_TAG_COUNT: 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+");
WriteToFile(MemLog, " Freed Master\n");
fclose(MemLog);
Print(stdout, " Freed Master\n");
#endif
Result = RC_ERROR_FILE;
goto End;
}
fwrite(Master.Location, Master.Ptr - Master.Location, 1, OutFile);
fclose(OutFile);
/* */ MEM_TEST_MID();
/* +MEM */ FreeBuffer(&Master);
/* */ MEM_TEST_MID();
#if DEBUG_MEM
MemLog = fopen("/home/matt/cinera_mem", "a+");
WriteToFile(MemLog, " Freed Master\n");
fclose(MemLog);
Print(stdout, " Freed Master\n");
#endif
}
else
{
Result = RC_INVALID_TEMPLATE;
}
#endif // AFE
}
else
{
buffer Master = {};
Master.Size = Kilobytes(512);
Master.ID = BID_MASTER;
if(!(Master.Location = malloc(Master.Size)))
{
LogError(LOG_ERROR, "BuffersToHTML(): %s",
strerror(errno));
Result = RC_ERROR_MEMORY;
goto End;
}
Master.Ptr = Master.Location;
CopyStringToBuffer(&Master,
"<html>\n"
" <head>\n"
" ");
CopyLandmarkedBuffer(&Master, PageType == PAGE_PLAYER ? &CollationBuffers->IncludesPlayer : &CollationBuffers->IncludesSearch, 0, PageType);
CopyStringToBuffer(&Master, "\n");
CopyStringToBuffer(&Master,
" </head>\n"
" <body>\n"
" ");
if(PageType == PAGE_PLAYER)
{
/* */ MEM_TEST_MID();
/* +MEM */ CopyLandmarkedBuffer(&Master, &CollationBuffers->Player, PlayerOffset, PageType);
/* */ MEM_TEST_MID();
CopyStringToBuffer(&Master, "\n");
}
else
{
/* */ MEM_TEST_MID();
/* +MEM */ CopyLandmarkedBuffer(&Master, &CollationBuffers->Search, 0, PageType);
/* */ MEM_TEST_MID();
}
CopyStringToBuffer(&Master,
" </body>\n"
"</html>\n");
FILE *OutFile;
if(!(OutFile = fopen(OutputPath, "w")))
{
LogError(LOG_ERROR, "Unable to open output file %s: %s", OutputPath, strerror(errno));
DeclaimBuffer(&Master);
Result = RC_ERROR_FILE;
goto End;
}
fwrite(Master.Location, Master.Ptr - Master.Location, 1, OutFile);
fclose(OutFile);
OutFile = 0;
/* */ MEM_TEST_MID();
/* +MEM */ FreeBuffer(&Master);
/* */ MEM_TEST_MID();
Result = RC_SUCCESS;
}
End:
SortLandmarks(&Assets);
MEM_TEST_END();
return Result;
}
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)) < 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))) { *Entry = LowerEntry; return Lower; }
if(!StringsDiffer(SearchTerm, Wrap0i(PivotEntry->HMMLBaseFilename))) { *Entry = PivotEntry; return Pivot; }
if(!StringsDiffer(SearchTerm, Wrap0i(UpperEntry->HMMLBaseFilename))) { *Entry = UpperEntry; return Upper; }
if(StringsDiffer(SearchTerm, Wrap0i(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.Path = ConstructIndexFilePath(&P->BaseDir, &P->SearchLocation, P->ID);
ReadFileIntoBuffer(&DB.File); // NOTE(matt): Could we actually catch errors (permissions?) here and bail?
if(!DB.File.Buffer.Location)
{
string IndexFileDir = StripComponentFromPath(Wrap0(DB.File.Path));
char *IndexFileDir0 = MakeString0("l", &IndexFileDir);
DIR *OutputDirectoryHandle = opendir(IndexFileDir0);
if(!OutputDirectoryHandle)
{
if(!MakeDir(IndexFileDir))
{
LogError(LOG_ERROR, "Unable to create directory %.*s: %s", (int)P->BaseDir.Length, P->BaseDir.Base, strerror(errno));
Print(stderr, "Unable to create directory %.*s: %s\n", (int)P->BaseDir.Length, P->BaseDir.Base, strerror(errno));
Free(IndexFileDir0);
return;
};
}
Free(IndexFileDir0);
closedir(OutputDirectoryHandle);
DB.File.Handle = fopen(DB.File.Path, "w");
WriteToFile(DB.File.Handle, "---\n");
CloseFile(&DB.File, NA);
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 += sizeof("---\n")-1;
}
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
Print(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);
Print(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)))
{
string StoredID = Wrap0i(N->Project->ID);
Colourise(CS_ERROR);
Print(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);
Print(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)
{
Print(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)
{
Print(stdout, "\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)
{
Print(stderr, "(Pre-deletion index %d) ", N->PreDeletionThisIndex);
PrintEntry(N->This, N->ThisIndex, NM_THIS);
}
if(N->Next)
{
PrintEntry(N->Next, N->NextIndex, NM_NEXT);
}
Print(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)
{
db_entry *FirstEntry = LocateFirstEntry(N->Project);
N->PreDeletionThisIndex = N->ThisIndex;
db_entry *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;
}
void
RenumberEntries(neighbourhood *N, db_header_project *P, int64_t IndexOfFirstEntryToRenumber)
{
db_entry *Entry = LocateEntryOfProject(P, IndexOfFirstEntryToRenumber);
int EntryOffset = AccumulateDBEntryInsertionOffset(P, IndexOfFirstEntryToRenumber);
char *Cursor = DB.File.Buffer.Location;
DB.File.Buffer.Ptr = DB.File.Buffer.Location + EntryOffset;
if(!(DB.File.Handle = fopen(DB.File.Path, "w"))) { return; }
Cursor += fwrite(Cursor, 1, EntryOffset, DB.File.Handle);
for(int i = IndexOfFirstEntryToRenumber; i < P->EntryCount; ++i)
{
char *Start = Cursor;
int OldSize = Entry->Size;
SeekBufferForString(&DB.File.Buffer, "\nnumber: \"", C_SEEK_FORWARDS, C_SEEK_AFTER);
Cursor += fwrite(Cursor, 1, DB.File.Buffer.Ptr - Cursor, DB.File.Handle);
SeekBufferForString(&DB.File.Buffer, "\"\n", C_SEEK_FORWARDS, C_SEEK_START);
int NumberLength = DB.File.Buffer.Ptr - Cursor;
Cursor = DB.File.Buffer.Ptr;
string Blank = {};
numbering_info NumberingInfo = GetEntryNumbering(CurrentProject, Blank, Blank, i, P->EntryCount);
int NewNumberLength = WriteToFile(DB.File.Handle, "%0*li", NumberingInfo.DigitsInHighestNumber, NumberingInfo.Value.int64_t);
Entry->Size += NewNumberLength - NumberLength;
Cursor += fwrite(Cursor, 1, OldSize - (Cursor - Start), DB.File.Handle);
++Entry;
}
Cursor += fwrite(Cursor, 1, DB.File.Buffer.Size - (Cursor - DB.File.Buffer.Location), DB.File.Handle);
CycleFile(&DB.File);
WriteEntireDatabase(N);
}
db_entry *
InsertIntoDB(neighbourhood *N, clash_resolver *ClashResolver, buffers *CollationBuffers, template *BespokeTemplate, string BaseFilename, bool RecheckingPrivacy, bool *Reinserting)
{
MEM_TEST_TOP();
db_entry *Result = 0;
ResetNeighbourhood(N);
edit_type_id EditType = EDIT_APPEND;
int EntryInsertionStart = sizeof("---\n")-1;
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)) < 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();
/* +MEM */ rc HMMLToBuffersReturn = HMMLToBuffers(CollationBuffers, BespokeTemplate, BaseFilename, N, ClashResolver, N->Project->EntryCount + (EditType != EDIT_REINSERTION));
/* */ MEM_TEST_MID();
if(HMMLToBuffersReturn == RC_SUCCESS || HMMLToBuffersReturn == RC_PRIVATE_VIDEO)
{
if(HMMLToBuffersReturn == RC_PRIVATE_VIDEO)
{
VideoIsPrivate = TRUE;
}
ClearCopyStringNoFormatOrTerminate(N->WorkingThis.HMMLBaseFilename, sizeof(N->WorkingThis.HMMLBaseFilename), BaseFilename);
if(!VideoIsPrivate) { ClearCopyStringNoFormatOrTerminate(N->WorkingThis.Title, sizeof(N->WorkingThis.Title), Wrap0i(CollationBuffers->Title)); }
if(!DB.File.Buffer.Location)
{
/* */ MEM_TEST_MID();
/* +MEM */ InitIndexFile(CurrentProject);
/* */ MEM_TEST_MID();
}
// TODO(matt): CollationBuffers->SearchEntry may, I believe, now contain data. We must not write it to file by mistake
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);
CloseFile(&DB.File, NA);
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(!(OpenFileForWriting(&DB.Metadata.File))) { 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);
DB.Metadata.Signposts.This.Ptr = DB.Metadata.File.Buffer.Location + BytesIntoFile;
N->This = DB.Metadata.Signposts.This.Ptr;
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(CurrentProject->Numbering.Method == NM_AUTO)
{
RenumberEntries(N, N->Project, N->ThisIndex + 1);
}
}
if(!VideoIsPrivate || !RecheckingPrivacy)
{
string EntryTitle = Wrap0(CollationBuffers->Title);
LogEdit(EditType, CurrentProject->Lineage, BaseFilename, &EntryTitle, VideoIsPrivate);
PrintEdit(EditType, CurrentProject->Lineage, BaseFilename, &EntryTitle, VideoIsPrivate);
}
// TODO(matt): Remove VideoIsPrivate in favour of generating a player page in a random location
if(HMMLToBuffersReturn == RC_SUCCESS)
{
Result = N->This;
}
}
MEM_TEST_END();
return Result;
}
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);
OpenFileForWriting(&DB.Metadata.File);
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->Index.Entry >= 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->Index.Entry; break;
case EDIT_ADDITION: ++Landmark->Index.Entry; 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)
{
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 SkipHeaderedSectionRecursively(Project, GetDBStructureHeaderedSection(B_PROJ));
}
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)
{
MEM_TEST_TOP();
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 = SkipHeaderedSectionRecursively(Asset, GetDBStructureHeaderedSection(B_ASET));
}
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);
}
MEM_TEST_END();
}
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->Index.Project) > 0))
{
break;
}
}
for(; Result.First + Result.Length < A->LandmarkCount; ++Landmark, ++Result.Length)
{
if(ProjectIndicesDiffer(I, Landmark->Index.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.ItemCount; ++i)
{
asset *Asset = GetPlaceInBook(&Assets, 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.ItemCount; ++i)
{
asset *AssetInMemory = GetPlaceInBook(&Assets, 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), Wrap0i(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->Player.ItemCount;
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->Player.ItemCount; ++j)
{
landmark *ThisLandmark = GetPlaceInBook(&AssetInMemory->Player, j);
db_landmark Landmark = {};
Landmark.Index.Project = P->Index;
Landmark.Index.Entry = N->ThisIndex;
Landmark.Position = ThisLandmark->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.ItemCount; ++i)
{
asset *This = GetPlaceInBook(&Assets, i);
if(!This->Known && This->Player.ItemCount > 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)
{
PrintLinedFunctionName(LineNumber, "VerifyLandmarks()");
db_block_assets *Block = LocateBlock(B_ASET);
db_asset *Asset = LocateFirstAsset(Block);
bool Malformed = FALSE;
for(uint16_t i = 0; i < Block->Count; ++i)
{
bool Titled = FALSE;
db_landmark *Landmark = LocateFirstLandmark(Asset);
for(uint16_t 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->Entry < Landmark->Entry) ||
(ProjectIndicesMatch(Next->Project, Landmark->Project) && Next->Entry == Landmark->Entry && Next->Position < Landmark->Position))
{
if(!Titled)
{
Print(stderr, "\nOut-of-order landmarks\n");
PrintAsset(Asset, &i);
Titled = TRUE;
}
//PrintAssetsBlock(0);
PrintLandmark(Landmark, &j);
PrintC(CS_YELLOW, " vs ");
PrintLandmark(Next, 0);
Print(stderr, "\n");
//PrintNeighbourhood(N);
//_exit(1);
Malformed = TRUE;
}
}
}
Asset = SkipAsset(Asset);
}
if(Malformed)
{
_exit(1);
}
#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)
{
Print(stdout, "Failure!!!\n");
Print(stdout, " %s\n", HTML.Path);
Print(stdout, " %.*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)
{
Print(stdout, "%sFailure ↓%s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END]);
Print(stdout, " %s\n", HTML.Path);
Print(stdout, " %.*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
{
Print(stdout, "%sFailed to open%s %s\n", ColourStrings[CS_ERROR], ColourStrings[CS_END], HTML.Path);
}
}
Assert(UniversalSuccess);
Print(stdout, "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.ItemCount; ++i)
{
asset *This = GetPlaceInBook(&Assets, 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.ItemCount; ++i)
{
asset *This = GetPlaceInBook(&Assets, 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->Search.ItemCount - 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->Search.ItemCount; ++j)
{
landmark *ThisLandmark = GetPlaceInBook(&This->Search, j);
db_landmark Landmark = {};
Landmark.Index.Project = ProjectIndex;
Landmark.Index.Entry = SP_SEARCH;
Landmark.Position = ThisLandmark->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->Search.ItemCount - Target.Length));
ToWrite -= Target.Length;
fwrite(TrailingLandmark, sizeof(db_landmark), ToWrite, DB.Metadata.File.Handle);
break;
}
}
Asset = SkipHeaderedSectionRecursively(Asset, GetDBStructureHeaderedSection(B_ASET));
}
CycleSignpostedFile(&DB.Metadata);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
VerifyLandmarks(N);
bool NewAsset = FALSE;
for(int InternalAssetIndex = 0; InternalAssetIndex < Assets.ItemCount; ++InternalAssetIndex)
{
asset *This = GetPlaceInBook(&Assets, InternalAssetIndex);
if(!This->Known && This->Search.ItemCount > 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;
rc
InsertNeighbourLink(db_header_project *P, db_entry *From, db_entry *To, enum8(link_directions) LinkDirection, bool FromHasOneNeighbour)
{
rc Result = RC_SUCCESS;
MEM_TEST_TOP(); file HTML = {}; ReadPlayerPageIntoBuffer(&HTML, Wrap0i(P->BaseDir), Wrap0i(P->PlayerLocation), Wrap0i(From->OutputLocation));
if(HTML.Buffer.Location)
{
if(!(HTML.Handle = fopen(HTML.Path, "w"))) { Result = RC_ERROR_FILE; };
if(Result == RC_SUCCESS)
{
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));
}
string ProjectTitle = BestTitleForHTML(CurrentProject);
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?
CopyProjectWelcomeStringToBuffer(&Link, ProjectTitle);
}
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);
CopyProjectEndStringToBuffer(&Link, ProjectTitle);
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);
CopyProjectWelcomeStringToBuffer(&Link, ProjectTitle);
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
{
CopyProjectEndStringToBuffer(&Link, ProjectTitle);
}
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);
CloseFile(&HTML, NA);
HTML.Handle = 0;
}
}
else
{
Result = RC_ERROR_FILE;
}
FreeFile(&HTML, NA);
MEM_TEST_END();
return Result;
}
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(&HTML, Wrap0i(N->Project->BaseDir), Wrap0i(N->Project->PlayerLocation), Wrap0i(Entry->OutputLocation));
if(HTML.Buffer.Location)
{
if(!(HTML.Handle = fopen(HTML.Path, "w"))) { FreeFile(&HTML, NA); 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);
CloseFile(&HTML, NA);
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, NA);
return RC_SUCCESS;
}
void
LinkToNewEntry(neighbourhood *N)
{
MEM_TEST_TOP();
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;
InsertNeighbourLink(N->Project, N->Prev, N->This, LINK_FORWARDS, N->FormerIsFirst);
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;
InsertNeighbourLink(N->Project, N->Next, N->This, LINK_BACKWARDS, N->LatterIsFinal);
N->NextOffsetModifier = N->Next->LinkOffsets.PrevEnd
+ N->Next->LinkOffsets.NextStart
+ N->Next->LinkOffsets.NextEnd
- N->PreLinkNextOffsetTotal;
}
MEM_TEST_END();
}
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(&HTML, Wrap0i(N->Project->BaseDir), Wrap0i(N->Project->PlayerLocation), Wrap0i(N->Next->OutputLocation));
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);
CopyProjectWelcomeStringToBuffer(&Link, BestTitleForHTML(CurrentProject));
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);
CloseFile(&HTML, NA);
}
else
{
link_insertion_offsets Blank = {};
N->Next->LinkOffsets = Blank;
}
FreeFile(&HTML, NA);
}
void
MarkPrevAsFinal(neighbourhood *N)
{
file HTML = {};
ReadPlayerPageIntoBuffer(&HTML, Wrap0i(N->Project->BaseDir), Wrap0i(N->Project->PlayerLocation), Wrap0i(N->Prev->OutputLocation));
if(HTML.Buffer.Location)
{
HTML.Handle = fopen(HTML.Path, "w");
buffer Link;
ClaimBuffer(&Link, BID_LINK, Kilobytes(4));
fwrite(HTML.Buffer.Location, N->Prev->LinkOffsets.PrevStart + N->Prev->LinkOffsets.PrevEnd + N->Prev->LinkOffsets.NextStart, 1, HTML.Handle);
CopyProjectEndStringToBuffer(&Link, BestTitleForHTML(CurrentProject));
fwrite(Link.Location, (Link.Ptr - Link.Location), 1, HTML.Handle);
fwrite(HTML.Buffer.Location + N->Prev->LinkOffsets.PrevStart + N->Prev->LinkOffsets.PrevEnd + N->Prev->LinkOffsets.NextStart + N->Prev->LinkOffsets.NextEnd,
HTML.Buffer.Size - (N->Prev->LinkOffsets.PrevStart + N->Prev->LinkOffsets.PrevEnd + N->Prev->LinkOffsets.NextStart + N->Prev->LinkOffsets.NextEnd),
1,
HTML.Handle);
N->Prev->LinkOffsets.NextEnd = Link.Ptr - Link.Location;
DeclaimBuffer(&Link);
CloseFile(&HTML, NA);
}
else
{
link_insertion_offsets Blank = {};
N->Prev->LinkOffsets = Blank;
}
FreeFile(&HTML, NA);
}
void
LinkOverDeletedEntry(neighbourhood *N)
{
if(N->Project->EntryCount > 0)
{
if(N->Project->EntryCount == 1)
{
DeleteNeighbourLinks(N);
}
else
{
if(N->DeletedEntryWasFirst)
{
if(N->Next) // NOTE(matt): Should be impossible to fail this test
{
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;
}
else
{
PrintC(CS_ERROR, "Error: Malformed neighbourhood");
PrintNeighbourhood(N);
}
return;
}
else if(N->DeletedEntryWasFinal)
{
if(N->Prev) // NOTE(matt): Should be impossible to fail this test
{
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;
}
else
{
PrintC(CS_ERROR, "Error: Malformed neighbourhood");
PrintNeighbourhood(N);
}
return;
}
else
{
// Assert(N->PrevIndex >= 0 && N->NextIndex >= 0)
if(N->Prev)
{
N->PreLinkPrevOffsetTotal = N->Prev->LinkOffsets.PrevEnd
+ N->Prev->LinkOffsets.NextStart
+ N->Prev->LinkOffsets.NextEnd;
}
else
{
PrintC(CS_ERROR, "Error: Malformed neighbourhood");
PrintNeighbourhood(N);
}
if(N->Next)
{
N->PreLinkNextOffsetTotal = N->Next->LinkOffsets.PrevEnd
+ N->Next->LinkOffsets.NextStart
+ N->Next->LinkOffsets.NextEnd;
}
else
{
PrintC(CS_ERROR, "Error: Malformed neighbourhood");
PrintNeighbourhood(N);
}
}
if(N->Prev && N->Next)
{
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;
}
else
{
PrintC(CS_ERROR, "Error: Malformed neighbourhood");
PrintNeighbourhood(N);
}
}
}
}
void
LinkNeighbours(neighbourhood *N, enum8(link_types) LinkType)
{
if(LinkType == LINK_INCLUDE)
{
LinkToNewEntry(N);
}
else
{
LinkOverDeletedEntry(N);
}
}
rc
DeleteFromDB(neighbourhood *N, string BaseFilename, db_entry *Deceased)
{
ResetNeighbourhood(N);
// TODO(matt): LogError()
db_entry *Entry = 0;
int EntryIndex = BinarySearchForMetadataEntry(N->Project, &Entry, BaseFilename);
if(!DB.File.Buffer.Location)
{
InitIndexFile(CurrentProject);
}
if(Entry)
{
*Deceased = *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(Wrap0i(N->Project->BaseDir), Wrap0i(N->Project->SearchLocation), Wrap0i(N->Project->ID));
DeleteLandmarksForSearch(CurrentProject->Index);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
}
N->Project->EntryCount = NewEntryCount;
if(!(OpenFileForWriting(&DB.Metadata.File))) { 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);
if(CurrentProject->Numbering.Method == NM_AUTO)
{
RenumberEntries(N, N->Project, N->PreDeletionThisIndex);
}
}
LogError(LOG_INFORMATIONAL, "Deleted %.*s/%.*s", (int)CurrentProject->Lineage.Length, CurrentProject->Lineage.Base, (int)BaseFilename.Length, BaseFilename.Base);
PrintEdit(EDIT_DELETION, CurrentProject->Lineage, BaseFilename, 0, FALSE);
}
return Entry ? RC_SUCCESS : RC_NOOP;
}
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=\"cineraMenuItem cineraFilterProject\" data-baseURL=\""));
AppendStringToBuffer(Filter, P->BaseURL);
AppendStringToBuffer(Filter, Wrap0("\" data-searchLocation=\""));
AppendStringToBuffer(Filter, P->SearchLocation);
AppendStringToBuffer(Filter, Wrap0("\" data-playerLocation=\""));
AppendStringToBuffer(Filter, P->PlayerLocation);
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 = GetPlaceInBook(&P->Child, ChildIndex);
GenerateFilterOfProjectAndChildren(Filter, StoredChild, Child, SearchRequired, FALSE, RequiresCineraJS);
StoredChild = SkipHeaderedSectionRecursively(StoredChild, GetDBStructureHeaderedSection(B_PROJ));
}
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-searchLocation=\""));
AppendStringToBuffer(Index, P->SearchLocation);
AppendStringToBuffer(Index, Wrap0("\" data-playerLocation=\""));
AppendStringToBuffer(Index, P->PlayerLocation);
AppendStringToBuffer(Index, Wrap0("\" data-unit=\""));
AppendStringToBuffer(Index, P->Numbering.Unit);
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));
AppendBuffer(Index, &PlayerURL);
AppendStringToBuffer(Index, Wrap0("\">"));
if(*StoredP->Unit)
{
AppendStringToBuffer(Index, P->Numbering.Unit);
AppendStringToBuffer(Index, Wrap0(" "));
numbering_info Number = GetEntryNumbering(P, Wrap0i(Entry->HMMLBaseFilename), Wrap0i(Entry->Number), i, StoredP->EntryCount);
switch(Number.Value.Type)
{
case PT_STRING:
{
AppendStringToBuffer(Index, Number.Value.String);
} break;
case PT_INT64:
{
for(int i = 0; i < Number.ZeroPadding; ++i)
{
AppendInt32ToBuffer(Index, 0);
}
AppendInt64ToBuffer(Index, Number.Value.int64_t);
} break;
default: break;
}
AppendStringToBuffer(Index, Wrap0(": "));
}
AppendStringToBufferHTMLSafe(Index, Wrap0i(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 = GetPlaceInBook(&P->Child, ChildIndex);
GenerateIndexOfProjectAndChildren(Index, StoredChild, Child, SearchRequired, FALSE, RequiresCineraJS);
StoredChild = SkipHeaderedSectionRecursively(StoredChild, GetDBStructureHeaderedSection(B_PROJ));
}
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 = SkipHeaderedSectionRecursively(Child, GetDBStructureHeaderedSection(B_PROJ));
}
}
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 = SkipHeaderedSectionRecursively(Child, GetDBStructureHeaderedSection(B_PROJ));
}
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 = SkipHeaderedSectionRecursively(Child, GetDBStructureHeaderedSection(B_PROJ));
}
return FALSE;
}
void
PushUniqueStringsRecursively(memory_book *UniqueThemes, project *P)
{
bool FoundMatch = FALSE;
for(int i = 0; i < UniqueThemes->ItemCount; ++i)
{
string **This = GetPlaceInBook(UniqueThemes, i);
if(StringsMatch(P->Theme, **This))
{
FoundMatch = TRUE;
}
}
if(!FoundMatch && P->Theme.Length > 0)
{
string **This = MakeSpaceInBook(UniqueThemes);
*This = &P->Theme;
}
for(int i = 0; i < P->Child.ItemCount; ++i)
{
project *Child = GetPlaceInBook(&P->Child, i);
PushUniqueStringsRecursively(UniqueThemes, Child);
}
}
void
PushUniqueString(memory_book *UniqueThemes, string *GlobalTheme)
{
bool FoundMatch = FALSE;
for(int i = 0; i < UniqueThemes->ItemCount; ++i)
{
string **This = GetPlaceInBook(UniqueThemes, i);
if(StringsMatch(*GlobalTheme, **This))
{
FoundMatch = TRUE;
}
}
if(!FoundMatch && GlobalTheme->Length > 0)
{
string **This = MakeSpaceInBook(UniqueThemes);
*This = GlobalTheme;
}
}
void
GenerateThemeLinks(buffer *IncludesSearch, project *P)
{
memory_book UniqueThemes = InitBookOfPointers(4);
if(P)
{
PushUniqueStringsRecursively(&UniqueThemes, P);
}
else
{
PushUniqueString(&UniqueThemes, &Config->GlobalTheme);
for(int i = 0; i < Config->Project.ItemCount; ++i)
{
P = GetPlaceInBook(&Config->Project, i);
PushUniqueStringsRecursively(&UniqueThemes, P);
}
}
for(int i = 0; i < UniqueThemes.ItemCount; ++i)
{
string **This = GetPlaceInBook(&UniqueThemes, i);
string ThemeFilename = MakeString("sls", "cinera__", *This, ".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,
"\">");
}
FreeBook(&UniqueThemes);
}
void
AppendHelpKeyToBuffer(buffer *B, uint32_t *IndentationLevel, string Key, bool Available)
{
OpenNodeNewLine(B, IndentationLevel, NODE_SPAN, 0);
AppendStringToBuffer(B, Wrap0(" class=\"help_key"));
if(!Available)
{
AppendStringToBuffer(B, Wrap0(" unavailable"));
}
AppendStringToBuffer(B, Wrap0("\">"));
AppendStringToBuffer(B, Key);
CloseNode(B, IndentationLevel, NODE_SPAN);
}
void
AppendHelpKeyToBufferNewLine(buffer *B, uint32_t *IndentationLevel, string Key, bool Available)
{
AppendHelpKeyToBuffer(B, IndentationLevel, Key, Available);
OpenNodeC(B, IndentationLevel, NODE_BR, 0);
}
void
AppendHelpKeyAndTextToBuffer(buffer *B, uint32_t *IndentationLevel, string Key, string Text, bool Available)
{
AppendHelpKeyToBuffer(B, IndentationLevel, Key, Available);
AppendStringToBuffer(B, Wrap0(" "));
OpenNodeNewLine(B, IndentationLevel, NODE_SPAN, 0);
AppendStringToBuffer(B, Wrap0(" class=\"help_text"));
if(!Available)
{
AppendStringToBuffer(B, Wrap0(" unavailable"));
}
AppendStringToBuffer(B, Wrap0("\">"));
AppendStringToBuffer(B, Text);
CloseNode(B, IndentationLevel, NODE_SPAN);
AppendStringToBuffer(B, Wrap0("\n"));
}
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"
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\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, P);
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,
"\">");
uint32_t IndentationLevel = 1;
++IndentationLevel;
bool SearchRequired = FALSE;
//IndentBuffer(&CollationBuffers->Search, ++IndentationLevel);
buffer *B = &CollationBuffers->Search;
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, "cineraIndex");
AppendStringToBuffer(B, Wrap0(" class=\""));
AppendStringToBuffer(B, Theme);
AppendStringToBuffer(B, Wrap0("\">"));
OpenNodeC(B, &IndentationLevel, NODE_DIV, "cineraIndexControl");
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraMenu cineraMenuTitle ViewSettings\">"));
OpenNodeCNewLine(B, &IndentationLevel, NODE_SPAN, 0);
AppendStringToBuffer(B, Wrap0("Settings"));
CloseNodeNewLine(B, &IndentationLevel, NODE_SPAN);
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraMenuContainer\">"));
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraMenuItem sort\">"));
AppendStringToBuffer(B, Wrap0("Sort: Old to New &#9206;"));
CloseNode(B, &IndentationLevel, NODE_DIV);
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraMenuItem view\">"));
AppendStringToBuffer(B, Wrap0("View: List"));
CloseNode(B, &IndentationLevel, NODE_DIV);
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraMenuItem anim\">"));
AppendStringToBuffer(B, Wrap0("Animations: ✔"));
CloseNode(B, &IndentationLevel, NODE_DIV);
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraMenuItem save\">"));
AppendStringToBuffer(B, Wrap0("Save Settings: ✔"));
CloseNode(B, &IndentationLevel, NODE_DIV);
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraMenuContainer
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraMenu ViewSettings
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=\"cineraMenu cineraMenuTitle IndexFilter\">"));
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=\"cineraMenuContainer\">"));
}
// 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 = GetPlaceInBook(&Config->Project, i);
if(FilterRequired)
{
GenerateFilterOfProjectAndChildren(&Filter, StoredP, Child, &SearchRequired, FALSE, &RequiresCineraJS);
}
GenerateIndexOfProjectAndChildren(&Index, StoredP, Child, &SearchRequired, FALSE, &RequiresCineraJS);
StoredP = SkipHeaderedSectionRecursively(StoredP, GetDBStructureHeaderedSection(B_PROJ));
}
}
if(FilterRequired)
{
AppendLandmarkedBuffer(B, &Filter, PAGE_SEARCH);
FreeBuffer(&Filter);
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraMenuContainer
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraMenu IndexFilter
}
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>");
}
asset *JSSearchPre = GetAsset(Wrap0(BuiltinAssets[ASSET_JS_SEARCH_PRE].Filename), ASSET_JS);
ConstructResolvedAssetURL(&URL, JSSearchPre, PAGE_SEARCH);
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
"\n <script type=\"text/javascript\" src=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesSearch, JSSearchPre, PAGE_SEARCH, 0);
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
"\"></script>");
asset *JSSearchPost = GetAsset(Wrap0(BuiltinAssets[ASSET_JS_SEARCH_POST].Filename), ASSET_JS);
ConstructResolvedAssetURL(&URL, JSSearchPost, PAGE_SEARCH);
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
"\n <script type=\"text/javascript\" src=\"%s",
URL.Location);
DeclaimBuffer(&URL);
PushAssetLandmark(&CollationBuffers->IncludesSearch, JSSearchPost, PAGE_SEARCH, 0);
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
"\" defer></script>");
if(P)
{
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
"\n"
"\n <meta property=\"cinera:type\" content=\"project\">");
CopyStringToBufferHTMLSafeSurrounded(&CollationBuffers->IncludesSearch,
Wrap0("\n <meta property=\"cinera:title\" content=\""), P->Title, Wrap0("\">"));
CopyStringToBufferHTMLSafeSurrounded(&CollationBuffers->IncludesSearch,
Wrap0("\n <meta property=\"cinera:project_lineage\" content=\""), P->WrittenLineage, Wrap0("\">"));
}
else
{
CopyStringToBuffer(&CollationBuffers->IncludesSearch,
"\n"
"\n <meta property=\"cinera:type\" content=\"instance\">");
CopyStringToBufferHTMLSafeSurrounded(&CollationBuffers->IncludesSearch,
Wrap0("\n <meta property=\"cinera:title\" content=\""), Config->InstanceTitle, Wrap0("\">"));
}
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); // spinner
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // inputContainer
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraQueryContainer
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraHelp\">"));
OpenNodeCNewLine(B, &IndentationLevel, NODE_SPAN, 0);
AppendStringToBuffer(B, Wrap0("?"));
CloseNode(B, &IndentationLevel, NODE_SPAN);
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"help_container\">"));
AppendHelpKeyToBuffer(B, &IndentationLevel, Wrap0("?"), TRUE);
OpenNodeC(B, &IndentationLevel, NODE_H1, 0);
AppendStringToBuffer(B, Wrap0("Keyboard Navigation"));
CloseNode(B, &IndentationLevel, NODE_H1);
OpenNodeC(B, &IndentationLevel, NODE_H2, 0);
AppendStringToBuffer(B, Wrap0("Grid Traversal"));
CloseNode(B, &IndentationLevel, NODE_H2);
AppendHelpKeyAndTextToBuffer(B, &IndentationLevel, Wrap0("h"), Wrap0("Shift leftwards"), TRUE);
AppendHelpKeyAndTextToBuffer(B, &IndentationLevel, Wrap0("k"), Wrap0("Ascend (zoom out)"), TRUE);
AppendHelpKeyAndTextToBuffer(B, &IndentationLevel, Wrap0("l"), Wrap0("Shift rightwards"), TRUE);
OpenNodeCNewLine(B, &IndentationLevel, NODE_BR, 0);
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"help_paragraph\">"));
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"key_block\">"));
OpenNodeCNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("1"), TRUE);
AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("q"), TRUE);
AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("a"), TRUE);
AppendHelpKeyToBuffer(B, &IndentationLevel, Wrap0("z"), TRUE);
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV);
OpenNodeCNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("2"), TRUE);
AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("w"), TRUE);
AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("s"), TRUE);
AppendHelpKeyToBuffer(B, &IndentationLevel, Wrap0("x"), TRUE);
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV);
OpenNodeCNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("3"), TRUE);
AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("e"), TRUE);
AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("d"), TRUE);
AppendHelpKeyToBuffer(B, &IndentationLevel, Wrap0("c"), TRUE);
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV);
OpenNodeCNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("4"), TRUE);
AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("r"), TRUE);
AppendHelpKeyToBufferNewLine(B, &IndentationLevel, Wrap0("f"), TRUE);
AppendHelpKeyToBuffer(B, &IndentationLevel, Wrap0("v"), TRUE);
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV);
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // key_block
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // help_paragraph
OpenNodeC(B, &IndentationLevel, NODE_H2, 0);
AppendStringToBuffer(B, Wrap0("Settings"));
CloseNode(B, &IndentationLevel, NODE_H2);
AppendHelpKeyAndTextToBuffer(B, &IndentationLevel, Wrap0("t"), Wrap0("Toggle sort order"), TRUE);
AppendHelpKeyAndTextToBuffer(B, &IndentationLevel, Wrap0("y"), Wrap0("Toggle view style"), TRUE);
AppendHelpKeyAndTextToBuffer(B, &IndentationLevel, Wrap0("m"), Wrap0("Toggle animations"), TRUE);
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // help_container
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraHelp
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraIndexControl
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, "cineraIndexList");
asset *JSClear = GetAsset(Wrap0(BuiltinAssets[ASSET_JS_CLEAR].Filename), ASSET_JS);
AppendScriptNode(B, &URL, &IndentationLevel, JSClear, FALSE, PAGE_SEARCH);
AppendLandmarkedBuffer(B, &Index, PAGE_SEARCH);
FreeBuffer(&Index);
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraIndexList
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraIndexGridContainer\">"));
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraTraversalContainer\">"));
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraTraversal\">"));
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraButton prev\">"));
OpenNodeC(B, &IndentationLevel, NODE_P, 0);
AppendStringToBuffer(B, Wrap0(""));
CloseNode(B, &IndentationLevel, NODE_P);
CloseNode(B, &IndentationLevel, NODE_DIV);
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraButton ascension\">"));
OpenNodeC(B, &IndentationLevel, NODE_P, 0);
AppendStringToBuffer(B, Wrap0(""));
CloseNode(B, &IndentationLevel, NODE_P);
CloseNode(B, &IndentationLevel, NODE_DIV);
OpenNodeNewLine(B, &IndentationLevel, NODE_DIV, 0);
AppendStringToBuffer(B, Wrap0(" class=\"cineraButton next\">"));
OpenNodeC(B, &IndentationLevel, NODE_P, 0);
AppendStringToBuffer(B, Wrap0(""));
CloseNode(B, &IndentationLevel, NODE_P);
CloseNode(B, &IndentationLevel, NODE_DIV);
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraTraversal
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraTraversalContainer
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraIndexGridContainer
CloseNodeNewLine(B, &IndentationLevel, NODE_DIV); // cineraIndex
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_TOP();
string BaseDir = Wrap0i(N->Project->BaseDir);
string PlayerLocation = Wrap0i(N->Project->PlayerLocation);
char *PlayerPath = ConstructDirectoryPath(&BaseDir, &PlayerLocation, &OutputLocation);
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));
Print(stderr, "Unable to create directory %s: %s\n", PlayerPath, strerror(errno));
return RC_ERROR_DIRECTORY;
};
}
closedir(OutputDirectoryHandle);
ExtendString0(&PlayerPath, Wrap0("/index.html"));
bool SearchInTemplate = FALSE;
for(int TagIndex = 0; TagIndex < PlayerTemplate->Metadata.Tags.ItemCount; ++TagIndex)
{
tag_offset *Tag = GetPlaceInBook(&PlayerTemplate->Metadata.Tags, TagIndex);
if(Tag->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), PlayerTemplate->Metadata.RequiresCineraJS);
break;
}
}
/* */ MEM_TEST_MID();
/* +MEM */ BuffersToHTML(0, CurrentProject, CollationBuffers, PlayerTemplate, PlayerPath, PAGE_PLAYER, FALSE, &N->This->LinkOffsets.PrevStart);
/* */ MEM_TEST_MID();
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);
if(SearchInTemplate)
{
FreeBuffer(&CollationBuffers->Search);
}
ResetAssetLandmarks();
MEM_TEST_END();
return RC_SUCCESS;
}
rc
GenerateSearchPage(neighbourhood *N, buffers *CollationBuffers, db_header_project *StoredP, project *P)
{
MEM_TEST_TOP();
string BaseDir = Wrap0i(N->Project->BaseDir);
string SearchLocation = Wrap0i(N->Project->SearchLocation);
char *SearchPath = ConstructDirectoryPath(&BaseDir, &SearchLocation, 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));
Print(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), P->SearchTemplate.Metadata.RequiresCineraJS))
{
case RC_SUCCESS:
{
BuffersToHTML(0, P, CollationBuffers, &P->SearchTemplate, SearchPath, PAGE_SEARCH, FALSE, 0);
UpdateLandmarksForSearch(N, P->Index);
break;
}
case RC_NOOP:
{
DeleteSearchPageFromFilesystem(BaseDir, SearchLocation, P->ID);
DeleteLandmarksForSearch(P->Index);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
break;
}
}
Free(SearchPath);
FreeBuffer(&CollationBuffers->Search);
ResetAssetLandmarks();
MEM_TEST_END();
return RC_SUCCESS;
}
rc
GenerateGlobalSearchPage(neighbourhood *N, buffers *CollationBuffers)
{
MEM_TEST_TOP();
db_block_projects *ProjectsBlock = DB.Metadata.Signposts.ProjectsBlock.Ptr;
string SearchLocationL = Wrap0i(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));
Print(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:
{
/* */ MEM_TEST_MID();
/* +MEM */ BuffersToHTML(Config, 0, CollationBuffers, &Config->SearchTemplate, SearchPath, PAGE_SEARCH, TRUE, 0);
/* */ MEM_TEST_MID();
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();
MEM_TEST_END();
return RC_SUCCESS;
}
int
GenerateSearchPages(neighbourhood *N, buffers *CollationBuffers)
{
MEM_TEST_TOP();
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);
/* */ MEM_TEST_MID();
/* +MEM */ GenerateGlobalSearchPage(N, CollationBuffers);
/* */ MEM_TEST_MID();
}
//sleep(1);
MEM_TEST_END();
return RC_SUCCESS;
}
rc
DeleteEntry(neighbourhood *N, clash_resolver *ClashResolver, string BaseFilename)
{
rc Result = RC_NOOP;
// TODO(matt): Fix deletion of the final entry in a project
db_entry Deceased = {};
if(DeleteFromDB(N, BaseFilename, &Deceased) == RC_SUCCESS)
{
LinkNeighbours(N, LINK_EXCLUDE);
string BaseDir = Wrap0i(N->Project->BaseDir);
string PlayerLocation = Wrap0i(N->Project->PlayerLocation);
DeletePlayerPageFromFilesystem(BaseDir, PlayerLocation, Wrap0i(Deceased.OutputLocation), FALSE, TRUE);
VacatePotentialClash(ClashResolver->Main, CurrentProject, BaseFilename);
UpdateLandmarksForNeighbourhood(N, EDIT_DELETION);
Result = RC_SUCCESS;
}
else
{
ResolveClash(ClashResolver->Main, CurrentProject, BaseFilename);
}
return Result;
}
rc
InsertEntry(neighbourhood *N, clash_resolver *ClashResolver, buffers *CollationBuffers, template *BespokeTemplate, string BaseFilename, bool RecheckingPrivacy)
{
rc Result = RC_SUCCESS;
MEM_TEST_TOP();
bool Reinserting = FALSE;
/* */ MEM_TEST_MID();
/* +MEM */ db_entry *Entry = InsertIntoDB(N, ClashResolver, CollationBuffers, BespokeTemplate, BaseFilename, RecheckingPrivacy, &Reinserting);
/* */ MEM_TEST_MID();
if(Entry)
{
LinkNeighbours(N, LINK_INCLUDE);
if(BespokeTemplate->File.Buffer.Location)
{
/* */ MEM_TEST_MID();
/* +MEM (inferred) */ GeneratePlayerPage(N, CollationBuffers, BespokeTemplate, Wrap0i(Entry->OutputLocation), Reinserting);
/* */ MEM_TEST_MID();
FreeTemplate(BespokeTemplate);
}
else
{
/* */ MEM_TEST_MID();
/* +MEM */ GeneratePlayerPage(N, CollationBuffers, &CurrentProject->PlayerTemplate, Wrap0i(Entry->OutputLocation), Reinserting);
/* */ MEM_TEST_MID();
}
}
else
{
Result = RC_NOOP;
}
MEM_TEST_END();
return Result;
}
void
SetCurrentProject(project *P, neighbourhood *N)
{
if(CurrentProject != P)
{
FreeFile(&DB.File, NA);
if(CurrentProject)
{
FreeTemplateNavBuffers(&CurrentProject->SearchTemplate);
FreeTemplateNavBuffers(&CurrentProject->PlayerTemplate);
}
CurrentProject = P;
InitNeighbourhood(N);
}
}
// NOTE(matt): Clash Resolution
//
clash_entry *
FindFirstClashNotStarted(memory_book *C)
{
clash_entry *Result = 0;
for(int i = 0; i < C->ItemCount; ++i)
{
clash_entry *This = GetPlaceInBook(C, i);
if(This->Status == RS_NOT_STARTED || This->Vacating)
{
Result = This;
break;
}
}
return Result;
}
clash_entry *
FindFirstInChain(memory_book *C, clash_entry *Clash)
{
clash_entry *Result = Clash;
while(Result
&& (Result->Status == RS_NOT_STARTED || Result->Status == RS_REPORTED)
&& Wrap0i(Result->CurrentOutputValue).Length > 0)
{
Result->Status = RS_SEEN;
// TODO(matt): GetBestClashCandidateOf()
clash_entry *Candidate = GetClashCandidateOf(C, Result);
if(Candidate)
{
Result = Candidate;
}
}
return Result;
}
void
BuildChain(clash_resolver *R, clash_entry *First)
{
related_clash_entry This = { .Entry = First, .Relationship = CR_INCUMBENT };
clash_entry **Final = PushClashPtr(&R->Chain, This.Entry);
This.Entry->Status = RS_PENDING;
while(This.Entry
&& !(*Final)->Vacating
&& First->Project == (*Final)->Project
&& Wrap0i((*Final)->IncumbentBaseFilename).Length > 0)
{
This = GetFellowOrClashIncumbentOf(R->Main, *Final);
if(This.Entry)
{
if(This.Relationship == CR_INCUMBENT)
{
Final = PushClashPtr(&R->Chain, This.Entry);
}
This.Entry->Status = RS_PENDING;
}
}
}
bool
SetResolvability(clash_resolver *R)
{
R->ChainIsResolvable = FALSE;
R->ChainStructure = CS_OPEN_ENDED;
if(R->Chain.ItemCount > 1)
{
clash_entry **First = GetPlaceInBook(&R->Chain, 0);
clash_entry **Final = GetPlaceInBook(&R->Chain, R->Chain.ItemCount - 1);
if(Wrap0i((*Final)->IncumbentBaseFilename).Length == 0)
{
R->ChainStructure = CS_OPEN_ENDED;
if((*Final)->Vacating || StringsDiffer(Wrap0i((*Final)->DesiredOutputValue), Wrap0i((*Final)->CurrentOutputValue)))
{
R->ChainIsResolvable = TRUE;
}
}
else if(StringsMatch(Wrap0i((*Final)->DesiredOutputValue), Wrap0i((*First)->CurrentOutputValue)))
{
R->ChainStructure = CS_CLOSED_LOOP;
R->ChainIsResolvable = TRUE;
}
}
return R->ChainIsResolvable;
}
void
AffirmSuccessors(clash_resolver *R)
{
for(int i = 0; i < R->Chain.ItemCount; ++i)
{
clash_entry **Successor = GetPlaceInBook(&R->Chain, i);
string IncumbentBaseFilename = Wrap0i((*Successor)->IncumbentBaseFilename);
string SuccessorBaseFilename = Wrap0i((*Successor)->CandidateBaseFilename);
for(int j = 0; j < R->Main->ItemCount; ++j)
{
clash_entry *Fellow = GetPlaceInBook(R->Main, j);
if(*Successor != Fellow
&& (*Successor)->Project == Fellow->Project
&& Fellow->Status == RS_PENDING
&& StringsMatch(IncumbentBaseFilename, Wrap0i(Fellow->IncumbentBaseFilename)))
{
(*Successor)->Superseding = TRUE;
ClearCopyStringNoFormatOrTerminate(Fellow->IncumbentBaseFilename, sizeof(Fellow->IncumbentBaseFilename), SuccessorBaseFilename);
}
}
}
}
void
ReportPendingClashes(clash_resolver *R)
{
for(int i = 0; i < R->Main->ItemCount; ++i)
{
clash_entry *This = GetPlaceInBook(R->Main, i);
if(This->Status == RS_PENDING)
{
string IncumbentBaseFilename = Wrap0i(This->IncumbentBaseFilename);
if(IncumbentBaseFilename.Length > 0)
{
string CandidateBaseFilename = Wrap0i(This->CandidateBaseFilename);
int NullTerminationBytes = 1;
char Filepath[This->Project->HMMLDir.Length + sizeof("/")-1 + CandidateBaseFilename.Length + ExtensionStrings[EXT_HMML].Length + NullTerminationBytes];
char *P = Filepath;
P += CopyStringToBarePtr(P, This->Project->HMMLDir);
P += CopyStringToBarePtr(P, Wrap0("/"));
P += CopyStringToBarePtr(P, CandidateBaseFilename);
P += CopyStringToBarePtr(P, ExtensionStrings[EXT_HMML]);
*P = '\0';
string FilepathL = Wrap0(Filepath);
IndexingErrorClash(&FilepathL, 0, S_ERROR, "output", Wrap0i(This->DesiredOutputValue), IncumbentBaseFilename);
clash_entry *Incumbent = GetClash(R->Main, This->Project, IncumbentBaseFilename);
if(Incumbent && Incumbent->Status == RS_RESOLVED)
{
Incumbent->Status = RS_REPORTED;
}
}
This->Status = RS_REPORTED;
}
}
}
bool
ResolveChain(clash_resolver *R,
neighbourhood *N, buffers *CollationBuffers, template *BespokeTemplate, bool RecheckingPrivacy)
{
bool Result = FALSE;
if(R->ChainIsResolvable)
{
AffirmSuccessors(R);
clash_entry **First = GetPlaceInBook(&R->Chain, 0);
SetCurrentProject((*First)->Project, N);
// NOTE(matt): Resolving the chain in reverse (incumbents first) so that each extant Player Page will be deleted while
// generating the new one, before the following candidate generates their own Player Page there, rather than vice versa
for(int i = R->Chain.ItemCount - 1; i >= 0; --i)
{
clash_entry **This = GetPlaceInBook(&R->Chain, i);
if((*This)->Vacating || InsertEntry(N, R, CollationBuffers, BespokeTemplate, Wrap0i((*This)->CandidateBaseFilename), RecheckingPrivacy) == RC_SUCCESS)
{
(*This)->Status = RS_RESOLVED;
(*This)->Vacating = FALSE;
Result = TRUE;
}
if((*This)->Superseding)
{
Clear((*This)->IncumbentBaseFilename, sizeof((*This)->IncumbentBaseFilename));
ClearCopyStringNoFormatOrTerminate((*This)->CurrentOutputValue, sizeof((*This)->CurrentOutputValue), Wrap0i((*This)->DesiredOutputValue));
(*This)->Status = RS_REPORTED;
(*This)->Superseding = FALSE;
}
}
}
ReportPendingClashes(R);
return Result;
}
clash_entry *
CopyClash(memory_book *Dest, clash_entry *Entry)
{
clash_entry *Result = MakeSpaceInBook(Dest);
*Result = *Entry;
Result->Status = RS_REPORTED;
return Result;
}
void
PurgeResolvedClashes(clash_resolver *R)
{
for(int i = 0; i < R->Main->ItemCount; ++i)
{
clash_entry *This = GetPlaceInBook(R->Main, i);
if(This->Status != RS_RESOLVED)
{
CopyClash(R->Holder, This);
}
}
FreeAndReinitialiseBook(R->Main);
SwapPtrs(R->Main, R->Holder);
R->ChainStructure = CS_OPEN_ENDED;
R->Resolving = FALSE;
}
void
ConfirmVacationStatus(clash_resolver *R)
{
for(int i = 0; i < R->Main->ItemCount; ++i)
{
clash_entry *This = GetPlaceInBook(R->Main, i);
if(This->Vacating && !GetClashCandidateOf(R->Main, This))
{
This->Vacating = FALSE;
}
}
}
#define ResolveClashes(R, N, CollationBuffers, BespokeTemplates, RecheckingPrivacy) ResolveClashes_(R, __LINE__, N, CollationBuffers, BespokeTemplates, RecheckingPrivacy)
rc
ResolveClashes_(clash_resolver *R, int LineNumber,
neighbourhood *N, buffers *CollationBuffers, template *BespokeTemplate, bool RecheckingPrivacy)
{
rc Result = RC_NOOP;
#if DEBUG_CLASH_RESOLVER
PrintClashes(R->Main, LineNumber);
#endif
R->Resolving = TRUE;
ConfirmVacationStatus(R);
clash_entry *Cursor = FindFirstClashNotStarted(R->Main);
while(Cursor)
{
Cursor = FindFirstInChain(R->Main, Cursor);
BuildChain(R, Cursor);
#if DEBUG_CLASH_RESOLVER
Print(stderr, " Clash chain:\n");
PrintClashChain(R);
if(SetResolvability(R))
{
Colourise(CS_GREEN);
Print(stderr, " ↑ Chain (%s) is resolvable!\n", ChainStructureStrings[R->ChainStructure]);
}
else
{
Colourise(CS_RED);
Print(stderr, " ↑ Chain (%s) is not resolvable :(\n", ChainStructureStrings[R->ChainStructure]);
}
Colourise(CS_END);
#else
SetResolvability(R);
#endif
if(ResolveChain(R, N, CollationBuffers, BespokeTemplate, RecheckingPrivacy))
{
Result = RC_SUCCESS;
}
FreeAndReinitialiseBook(&R->Chain);
Cursor = FindFirstClashNotStarted(R->Main);
}
PurgeResolvedClashes(R);
return Result;
}
//
// NOTE(matt): Clash Resolution
void
RecheckPrivacyRecursively(project *P, neighbourhood *N, clash_resolver *ClashResolver, 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, ClashResolver, CollationBuffers, BespokeTemplate, Wrap0i(PrivateEntries[i].HMMLBaseFilename), TRUE) == RC_SUCCESS);
}
Inserted |= (ResolveClashes(ClashResolver, N, CollationBuffers, BespokeTemplate, TRUE) == RC_SUCCESS);
if(Inserted)
{
GenerateSearchPages(N, CollationBuffers);
DeleteStaleAssets();
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
}
}
for(int i = 0; i < P->Child.ItemCount; ++i)
{
RecheckPrivacyRecursively(GetPlaceInBook(&P->Child, i), N, ClashResolver, CollationBuffers, BespokeTemplate);
}
}
void
RecheckPrivacy(neighbourhood *N, clash_resolver *ClashResolver, buffers *CollationBuffers, template *BespokeTemplate)
{
for(int i = 0; i < Config->Project.ItemCount; ++i)
{
RecheckPrivacyRecursively(GetPlaceInBook(&Config->Project, i), N, ClashResolver, CollationBuffers, BespokeTemplate);
}
LastPrivacyCheck = time(0);
}
void
UpdateDeferredAssetChecksums(void)
{
for(int i = 0; i < Assets.ItemCount; ++i)
{
asset *This = GetPlaceInBook(&Assets, i);
if(This->DeferredUpdate)
{
UpdateAssetInDB(This);
}
}
}
rc
RemoveDirectory(string D)
{
int Result;
char *Path = 0;
ExtendString0(&Path, D);
if((remove(Path) == -1))
{
LogError(LOG_NOTICE, "Unable to remove directory %s: %s", Path, strerror(errno));
Print(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);
Print(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;
ClearCopyStringNoFormatOrTerminate(DB.AssetsBlock.RootDir, sizeof(DB.AssetsBlock.RootDir), Config->AssetsRootDir);
ClearCopyStringNoFormatOrTerminate(DB.AssetsBlock.RootURL, sizeof(DB.AssetsBlock.RootURL), Config->AssetsRootURL);
ClearCopyStringNoFormatOrTerminate(DB.AssetsBlock.CSSDir, sizeof(DB.AssetsBlock.CSSDir), Config->CSSDir);
ClearCopyStringNoFormatOrTerminate(DB.AssetsBlock.ImagesDir, sizeof(DB.AssetsBlock.ImagesDir), Config->ImagesDir);
ClearCopyStringNoFormatOrTerminate(DB.AssetsBlock.JSDir, sizeof(DB.AssetsBlock.JSDir), Config->JSDir);
++DB.Header.BlockCount;
OpenFileForWriting(&DB.Metadata.File);
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
}
Print(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, clash_resolver *ClashResolver, bool *Modified)
{
// TODO(matt): Additionally remove the BaseDir if it changed
bool HasNewPlayerLocation = FALSE;
bool HasNewSearchLocation = FALSE;
// TODO(matt): Here is where the relocation happens. This could be wrong
if(StringsDiffer(CurrentProject->BaseDir, Wrap0i(N->Project->BaseDir)) ||
StringsDiffer(CurrentProject->PlayerLocation, Wrap0i(N->Project->PlayerLocation)))
{
string OldBaseDir = Wrap0i(N->Project->BaseDir);
string OldPlayerLocation = Wrap0i(N->Project->PlayerLocation);
char *OldPlayerDirectory = ConstructDirectoryPath(&OldBaseDir, &OldPlayerLocation, 0);
db_header_project NewProjectHeader = *N->Project;
ClearCopyStringNoFormatOrTerminate(NewProjectHeader.BaseDir, sizeof(NewProjectHeader.BaseDir), CurrentProject->BaseDir);
string NewBaseDir = Wrap0i(NewProjectHeader.BaseDir);
char *NewPlayerDirectory = ConstructDirectoryPath(&NewBaseDir, &CurrentProject->PlayerLocation, 0);
Print(stdout, "%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;
string BaseDir = Wrap0i(N->Project->BaseDir);
string PlayerLocation = Wrap0i(N->Project->PlayerLocation);
DeletePlayerPageFromFilesystem(BaseDir, PlayerLocation, Wrap0i(This->OutputLocation), TRUE, TRUE);
}
if(OldPlayerLocation.Length > 0)
{
RemoveChildDirectories(Wrap0(OldPlayerDirectory), Wrap0i(N->Project->BaseDir));
}
Free(OldPlayerDirectory);
ClearCopyStringNoFormatOrTerminate(N->Project->PlayerLocation, sizeof(N->Project->PlayerLocation), CurrentProject->PlayerLocation);
HasNewPlayerLocation = TRUE;
}
if(StringsDiffer(CurrentProject->BaseDir, Wrap0i(N->Project->BaseDir)) ||
StringsDiffer(CurrentProject->SearchLocation, Wrap0i(N->Project->SearchLocation)))
{
string OldBaseDir = Wrap0i(N->Project->BaseDir);
string OldSearchLocation = Wrap0i(N->Project->SearchLocation);
char *OldSearchDirectory = ConstructDirectoryPath(&OldBaseDir, &OldSearchLocation, 0);
db_header_project NewProjectHeader = *N->Project;
ClearCopyStringNoFormatOrTerminate(NewProjectHeader.BaseDir, sizeof(NewProjectHeader.BaseDir), CurrentProject->BaseDir);
ClearCopyStringNoFormatOrTerminate(NewProjectHeader.SearchLocation, sizeof(NewProjectHeader.SearchLocation), CurrentProject->SearchLocation);
string NewBaseDir = Wrap0i(NewProjectHeader.BaseDir);
string NewSearchLocation = Wrap0i(NewProjectHeader.SearchLocation);
char *NewSearchDirectory = ConstructDirectoryPath(&NewBaseDir, &NewSearchLocation, 0);
MakeDir(Wrap0(NewSearchDirectory));
Print(stdout, "%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(&OldBaseDir, &OldSearchLocation, Wrap0i(N->Project->ID));
char *NewSearchIndexPath = ConstructIndexFilePath(&NewBaseDir, &NewSearchLocation, Wrap0i(N->Project->ID));
rename(OldSearchIndexPath, NewSearchIndexPath);
Free(OldSearchIndexPath);
Free(NewSearchIndexPath);
Free(NewSearchDirectory);
FreeFile(&DB.File, NA);
DB.File.Path = ConstructIndexFilePath(&CurrentProject->BaseDir, &CurrentProject->SearchLocation, CurrentProject->ID);
ReadFileIntoBuffer(&DB.File); // NOTE(matt): Could we actually catch errors (permissions?) here and bail?
if(OldSearchLocation.Length > 0)
{
RemoveChildDirectories(Wrap0(OldSearchDirectory), Wrap0i(N->Project->BaseDir));
}
remove(OldSearchDirectory);
Free(OldSearchDirectory);
ClearCopyStringNoFormatOrTerminate(N->Project->SearchLocation, sizeof(N->Project->SearchLocation), CurrentProject->SearchLocation);
HasNewSearchLocation = TRUE;
}
if(StringsDiffer(CurrentProject->BaseDir, Wrap0i(N->Project->BaseDir)))
{
ClearCopyStringNoFormatOrTerminate(N->Project->BaseDir, sizeof(N->Project->BaseDir), CurrentProject->BaseDir);
OpenFileForWriting(&DB.Metadata.File);
WriteFromByteToEnd(&DB.Metadata.File, 0);
CycleSignpostedFile(&DB.Metadata);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
*Modified = TRUE;
}
if(StringsDiffer(CurrentProject->BaseURL, Wrap0i(N->Project->BaseURL)))
{
ClearCopyStringNoFormatOrTerminate(N->Project->BaseURL, sizeof(N->Project->BaseURL), CurrentProject->BaseURL);
OpenFileForWriting(&DB.Metadata.File);
WriteFromByteToEnd(&DB.Metadata.File, 0);
CycleSignpostedFile(&DB.Metadata);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
*Modified = TRUE;
}
if(HasNewPlayerLocation || HasNewSearchLocation)
{
if(!(OpenFileForWriting(&DB.Metadata.File))) { FreeBuffer(&DB.Metadata.File.Buffer); return RC_ERROR_FILE; }
ClearCopyStringNoFormatOrTerminate(N->Project->BaseDir, sizeof(N->Project->BaseDir), CurrentProject->BaseDir);
ClearCopyStringNoFormatOrTerminate(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;
ClearCopyStringNoFormatOrTerminate(Entries[EntryIndex].ID, sizeof(Entries[EntryIndex].ID), Wrap0i(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));
Print(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(Wrap0i(Entries[i].ID), GetBaseFilename(Filename, EXT_HMML)))
{
Entries[i].Present = TRUE;
break;
}
}
}
}
closedir(HMMLDirHandle);
bool Deleted = FALSE;
int OldEntryCount = N->Project->EntryCount;
for(int i = 0; i < OldEntryCount; ++i)
{
if(Entries[i].Present == FALSE)
{
Deleted = TRUE;
ResetNeighbourhood(N);
DeleteEntry(N, ClashResolver, Wrap0i(Entries[i].ID));
}
}
return Deleted ? RC_SUCCESS : RC_NOOP;
}
rc
SyncDBWithInput(neighbourhood *N, clash_resolver *ClashResolver, buffers *CollationBuffers, template *BespokeTemplate)
{
rc Result = RC_SUCCESS;
MEM_TEST_TOP();
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)))
{
ClearCopyStringNoFormatOrTerminate(N->Project->Title, sizeof(N->Project->Title), CurrentProject->Title);
OpenFileForWriting(&DB.Metadata.File);
WriteFromByteToEnd(&DB.Metadata.File, 0);
CycleSignpostedFile(&DB.Metadata);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
Modified = TRUE;
}
if(StringsDiffer(CurrentProject->Theme, Wrap0i(N->Project->Theme)))
{
ClearCopyStringNoFormatOrTerminate(N->Project->Theme, sizeof(N->Project->Theme), CurrentProject->Theme);
OpenFileForWriting(&DB.Metadata.File);
WriteFromByteToEnd(&DB.Metadata.File, 0);
CycleSignpostedFile(&DB.Metadata);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
Modified = TRUE;
}
if(StringsDiffer(CurrentProject->Numbering.Unit, Wrap0i(N->Project->Unit)))
{
ClearCopyStringNoFormatOrTerminate(N->Project->Unit, sizeof(N->Project->Unit), CurrentProject->Numbering.Unit);
OpenFileForWriting(&DB.Metadata.File);
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, ClashResolver, &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)
{
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();
/* +MEM */ Inserted |= (InsertEntry(N, ClashResolver, CollationBuffers, BespokeTemplate, GetBaseFilename(Filename, EXT_HMML), FALSE) == RC_SUCCESS);
/* */ MEM_TEST_MID();
VerifyLandmarks(N);
}
}
closedir(HMMLDirHandle);
Inserted |= (ResolveClashes(ClashResolver, N, CollationBuffers, BespokeTemplate, FALSE) == RC_SUCCESS);
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
/* */ MEM_TEST_MID();
/* +MEM */ GenerateSearchPages(N, CollationBuffers);
/* */ MEM_TEST_MID();
DeleteStaleAssets();
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
VerifyLandmarks(N);
}
}
else
{
LogError(LOG_ERROR, "Unable to scan project directory %.*s: %s", (int)CurrentProject->HMMLDir.Length, CurrentProject->HMMLDir.Base, strerror(errno));
Print(stderr, "Unable to scan project directory %.*s: %s\n", (int)CurrentProject->HMMLDir.Length, CurrentProject->HMMLDir.Base, strerror(errno));
Result = RC_ERROR_DIRECTORY;
}
MEM_TEST_END();
return Result;
}
void
PrintVersions()
{
curl_version_info_data *CurlVersion = curl_version_info(CURLVERSION_NOW);
Print(stdout, "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 = SkipHeaderedSectionRecursively(Child, GetDBStructureHeaderedSection(B_PROJ));
}
DecrementCurrentGeneration(A);
}
project_generations
CopyAccumulator(project_generations *G)
{
project_generations Result = {};
Result.EntriesInGeneration = InitBook(sizeof(uint32_t), 4);
for(int i = 0; i < G->EntriesInGeneration.ItemCount; ++i)
{
uint32_t *Src = GetPlaceInBook(&G->EntriesInGeneration, i);
uint32_t *Dest = MakeSpaceInBook(&Result.EntriesInGeneration);
*Dest = *Src;
}
return Result;
}
project_generations
InitAccumulator(project_generations *G)
{
project_generations Result = {};
Result.EntriesInGeneration = InitBook(sizeof(uint32_t), 4);
for(int i = 0; i < G->CurrentGeneration; ++i)
{
MakeSpaceInBook(&Result.EntriesInGeneration);
++Result.CurrentGeneration;
}
return Result;
}
rc
InitDB(void)
{
rc Result = RC_SUCCESS;
// 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, TRUE);
if(ReadFileIntoBuffer(&DB.Metadata.File) == RC_ERROR_FILE_LOCKED) // NOTE(matt): Could we actually catch errors (permissions?) here and bail?
{
ConfigErrorLockedDBLocation(0, 0, &Config->DatabaseLocation);
CloseFile(&DB.Metadata.File, TRUE);
Result = RC_ERROR_FILE_LOCKED;
}
else
{
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)
{
Print(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)
{
Print(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
{
Print(stderr, "%sMalformed database%s: %s\n",
ColourStrings[CS_ERROR], ColourStrings[CS_END], DB.Metadata.File.Path);
}
}
if(OpenFileForWriting(&DB.Metadata.File))
{
fwrite(B->Location, B->Size, 1, DB.Metadata.File.Handle);
SetFileEditPosition(&DB.Metadata);
CycleSignpostedFile(&DB.Metadata);
DB.Ready = TRUE;
}
else
{
// TODO(matt): Handle unopenable database files
PrintC(CS_RED, "Could not open database file: ");
PrintC(CS_MAGENTA, DB.Metadata.File.Path);
Print(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");
ClearCopyStringNoFormatOrTerminate(DB.ProjectsBlock.GlobalSearchDir, sizeof(DB.ProjectsBlock.GlobalSearchDir), Config->GlobalSearchDir);
ClearCopyStringNoFormatOrTerminate(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;
ClearCopyStringNoFormatOrTerminate(DB.AssetsBlock.RootDir, sizeof(DB.AssetsBlock.RootDir), Config->AssetsRootDir);
ClearCopyStringNoFormatOrTerminate(DB.AssetsBlock.RootURL, sizeof(DB.AssetsBlock.RootURL), Config->AssetsRootURL);
ClearCopyStringNoFormatOrTerminate(DB.AssetsBlock.CSSDir, sizeof(DB.AssetsBlock.CSSDir), Config->CSSDir);
ClearCopyStringNoFormatOrTerminate(DB.AssetsBlock.ImagesDir, sizeof(DB.AssetsBlock.ImagesDir), Config->ImagesDir);
ClearCopyStringNoFormatOrTerminate(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));
Print(stderr, "Unable to create directory %.*s: %s\n", (int)Config->DatabaseLocation.Length, Config->DatabaseLocation.Base, strerror(errno));
Free(DatabaseLocation0);
Result = RC_ERROR_DIRECTORY;
};
}
else
{
closedir(OutputDirectoryHandle);
OutputDirectoryHandle = 0;
}
Free(DatabaseLocation0);
if(Result == RC_SUCCESS)
{
if(OpenFileForWriting(&DB.Metadata.File))
{
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);
CloseFile(&DB.Metadata.File, FALSE);
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;
DB.Ready = TRUE;
}
else
{
// TODO(matt): Handle unopenable database files
PrintC(CS_RED, "Could not open database file: ");
PrintC(CS_MAGENTA_BOLD, DB.Metadata.File.Path);
Print(stderr, "\n");
_exit(0);
}
#if 0
DB.File.Handle = fopen(DB.File.Path, "w");
WriteToFile(DB.File.Handle, "---\n");
CloseFile(&DB.File, NA);
ReadFileIntoBuffer(&DB.File);
#endif
}
}
}
return Result;
}
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) - sizeof(".metadata")-1);
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 + sizeof("/")-1 + 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->Numbering.Unit);
Project.ArtIndex = SAI_UNSET;
Project.IconIndex = SAI_UNSET;
Project.EntryCount = 0;
Project.ChildCount = P->Child.ItemCount;
fwrite(&Project, sizeof(Project), 1, DB.Metadata.File.Handle);
AccumulateFileEditSize(&DB.Metadata, sizeof(Project));
IncrementCurrentGeneration(G);
for(int i = 0; i < P->Child.ItemCount; ++i)
{
InitProjectInDBRecursively(G, GetPlaceInBook(&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->Index.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 = 0 ;
if(OldAcc->EntriesInGeneration.ItemCount > 0)
{
OldEntriesInGeneration = *(uint32_t *)GetPlaceInBook(&OldAcc->EntriesInGeneration, NewLandmark.Index.Project.Generation);
}
if(NewLandmark.Index.Project.Index >= OldEntriesInGeneration)
{
NewLandmark.Index.Project.Index +=
(NewLandmark.Index.Project.Generation < NewAcc->EntriesInGeneration.ItemCount ? *(uint32_t *)GetPlaceInBook(&NewAcc->EntriesInGeneration, NewLandmark.Index.Project.Generation) : 0)
-
(NewLandmark.Index.Project.Generation < OldAcc->EntriesInGeneration.ItemCount ? 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 = SkipHeaderedSectionRecursively(Asset, GetDBStructureHeaderedSection(B_ASET));
}
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(project_generations *G, db_block_projects **Block, db_header_project **Parent, db_header_project **Child, project *P)
{
bool GotBlock = Block ? TRUE : FALSE;
bool GotParent = Parent ? TRUE : FALSE;
Assert(GotBlock ^ GotParent);
uint64_t Byte = 0;
OpenFileForWriting(&DB.Metadata.File);
uint64_t PPos;
if(GotBlock)
{
PPos = (char *)*Block - DB.Metadata.File.Buffer.Location;
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);
}
else
{
PPos = (char *)*Parent - DB.Metadata.File.Buffer.Location;
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);
*Child = InitProjectInDBPostamble(&OldG, G);
if(GotBlock)
{
*Block = (db_block_projects *)(DB.Metadata.File.Buffer.Location + PPos);
}
else
{
*Parent = (db_header_project *)(DB.Metadata.File.Buffer.Location + PPos);
}
FreeBook(&OldG.EntriesInGeneration);
}
bool
ConfiguredAndStoredProjectIDsMatch(project *ConfiguredP, db_header_project *StoredP)
{
return StringsMatch(ConfiguredP->ID, Wrap0i(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->EntriesInGeneration.ItemCount; ++i)
{
uint32_t *This = GetPlaceInBook(&LocalAcc->EntriesInGeneration, i);
for(int j = 0; j < *This; ++j)
{
db_project_index Acc = {};
Acc.Generation = i;
Acc.Index = j;
if(i < MainAcc->EntriesInGeneration.ItemCount)
{
Acc.Index += *(uint32_t *)GetPlaceInBook(&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->EntriesInGeneration.ItemCount; ++GenIndex)
{
uint64_t GenLandmarkDeletionCount = 0;
db_landmark *GenLandmark = 0;
//uint64_t LandmarkIndex = 0;
uint32_t *This = GetPlaceInBook(&LocalAcc->EntriesInGeneration, GenIndex);
for(int j = 0; j < *This; ++j)
{
db_project_index Acc = {};
Acc.Generation = GenIndex;
Acc.Index = j;
if(GenIndex < MainAcc->EntriesInGeneration.ItemCount)
{
Acc.Index += *(uint32_t *)GetPlaceInBook(&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->Index.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
uint32_t LocalEntries = *(uint32_t *)GetPlaceInBook(&LocalAcc->EntriesInGeneration, GenIndex);
uint32_t MainEntries = *(uint32_t *)GetPlaceInBook(&MainAcc->EntriesInGeneration, GenIndex);
if((int64_t)NewLandmark.Index.Project.Index - (int64_t)LocalEntries >= (int64_t)MainEntries)
{
NewLandmark.Index.Project.Index -= *(uint32_t *)GetPlaceInBook(&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;
string BaseDir = Wrap0i(Project->BaseDir);
string PlayerLocation = Wrap0i(Project->PlayerLocation);
db_entry *Entry = LocateFirstEntry(Project);
for(int i = 0; i < Project->EntryCount; ++i, ++Entry)
{
DeletePlayerPageFromFilesystem(BaseDir, PlayerLocation, Wrap0i(Entry->OutputLocation), FALSE, FALSE);
}
DeleteSearchPageFromFilesystem(BaseDir, PlayerLocation, Wrap0i(Project->ID));
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);
}
string BaseDir = Wrap0i(Project->BaseDir);
string SearchLocation = Wrap0i(Project->SearchLocation);
DeleteSearchPageFromFilesystem(BaseDir, SearchLocation, Wrap0i(Project->ID));
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 = SkipHeaderedSectionRecursively(Asset, GetDBStructureHeaderedSection(B_ASET));
}
*BytesThroughBuffer += WriteFromByteToEnd(&DB.Metadata.File, *BytesThroughBuffer);
CycleSignpostedFile(&DB.Metadata);
FreeBook(&ChildAccumulator.EntriesInGeneration);
}
void
DeleteProject(db_block_projects **Block, db_header_project **Parent, db_header_project **Child, project_generations *G)
{
PrintFunctionName("DeleteProject()");
bool GotBlock = Block ? TRUE : FALSE;
bool GotParent = Parent ? TRUE : FALSE;
Assert(GotBlock ^ GotParent);
// TODO(matt); Print out something sensible to inform real life users
// TODO(matt):
//
// 0:1
// 1:4
uint64_t PPos;
if(GotBlock)
{
PPos = (char *)*Block - DB.Metadata.File.Buffer.Location;
}
else
{
PPos = (char *)*Parent - DB.Metadata.File.Buffer.Location;
}
uint64_t CPos = (char *)*Child - DB.Metadata.File.Buffer.Location;
//db_project_index Index = GetCurrentProjectIndex(G);
OpenFileForWriting(&DB.Metadata.File);
uint64_t Byte = 0;
if(GotBlock)
{
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);
}
else
{
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);
if(GotBlock)
{
*Block = (db_block_projects *)(DB.Metadata.File.Buffer.Location + PPos);
}
else
{
*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_block_projects **Block, db_header_project **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 < *(uint32_t *)GetPlaceInBook(&ThisAcc->EntriesInGeneration, GenIndex); ++j)
{
db_project_index ThisIndex = {};
ThisIndex.Generation = GenIndex;
ThisIndex.Index = j;
if(MainAcc && GenIndex < MainAcc->EntriesInGeneration.ItemCount) { ThisIndex.Index += *(uint32_t *)GetPlaceInBook(&MainAcc->EntriesInGeneration, GenIndex); }
if(PrevAcc && GenIndex < PrevAcc->EntriesInGeneration.ItemCount) { ThisIndex.Index += *(uint32_t *)GetPlaceInBook(&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_structure_headered_section *DBStructureProjects = GetDBStructureHeaderedSection(B_PROJ);
db_header_project *This = SkipHeaderedSectionRecursively(SChild, DBStructureProjects);
for(++ChildIndex; ChildIndex < ChildCount; ++ChildIndex)
{
if(ConfiguredAndStoredProjectIDsMatch(CChild, This))
{
project_generations ThisAcc = InitAccumulator(G);
AccumulateProjectIndices(&ThisAcc, This);
OpenFileForWriting(&DB.Metadata.File);
uint64_t Byte = 0;
WriteFromByteToPointer(&DB.Metadata.File, &Byte, SChild);
char *Next = SkipHeaderedSectionRecursively(This, DBStructureProjects);
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.EntriesInGeneration.ItemCount; ++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.EntriesInGeneration.ItemCount)
{
NewLandmark.Index.Project.Index -= *(uint32_t*)GetPlaceInBook(&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.Index.Project.Index += *(uint32_t *)GetPlaceInBook(&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->Index.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 = SkipHeaderedSectionRecursively(Asset, GetDBStructureHeaderedSection(B_ASET));
}
CycleSignpostedFile(&DB.Metadata);
FreeBook(&ThisAcc.EntriesInGeneration);
Result = TRUE;
break;
}
else
{
AccumulateProjectIndices(&LocalAcc, This);
}
This = SkipHeaderedSectionRecursively(This, DBStructureProjects);
}
FreeBook(&LocalAcc.EntriesInGeneration);
return Result;
}
bool
ReorganiseProjects(project_generations *G, project *CChild,
db_block_projects **Block, db_header_project **SParent,
uint64_t ChildIndex, db_header_project **SChild)
{
bool GotBlock = Block ? TRUE : FALSE;
bool GotParent = SParent ? TRUE : FALSE;
Assert(GotBlock ^ GotParent);
uint64_t ParentPos;
uint64_t ChildCount;
if(GotBlock)
{
ParentPos = (char *)*Block - DB.Metadata.File.Buffer.Location;
ChildCount = (*Block)->Count;
}
else
{
ParentPos = (char *)*SParent - DB.Metadata.File.Buffer.Location;
ChildCount = (*SParent)->ChildCount;
}
uint64_t SChildPos = (char *)*SChild - DB.Metadata.File.Buffer.Location;
bool Result = ReorganiseProjectsInterior(G, CChild, ChildIndex, ChildCount, *SChild);
if(Result == TRUE)
{
*SChild = (db_header_project *)(DB.Metadata.File.Buffer.Location + SChildPos);
AddEntryToGeneration(G, CChild);
if(GotBlock)
{
*Block = (db_block_projects *)(DB.Metadata.File.Buffer.Location + ParentPos);
}
else
{
*SParent = (db_header_project *)(DB.Metadata.File.Buffer.Location + ParentPos);
}
SyncProjects(G, CChild, Block, SParent, SChild);
}
return Result;
}
void
SyncProjects(project_generations *G, project *C, db_block_projects **Block, db_header_project **SParent, db_header_project **SChild)
{
bool GotBlock = Block ? TRUE : FALSE;
bool GotParent = SParent ? TRUE : FALSE;
Assert(GotBlock ^ GotParent);
uint64_t SChildPos = (char *)*SChild - DB.Metadata.File.Buffer.Location;
uint64_t ParentPos;
if(GotBlock)
{
ParentPos = (char *)*Block - DB.Metadata.File.Buffer.Location;
}
else
{
ParentPos = (char *)*SParent - DB.Metadata.File.Buffer.Location;
}
IncrementCurrentGeneration(G);
db_header_project *SGrandChild = LocateFirstChildProject(*SChild);
for(int ci = 0; ci < C->Child.ItemCount; ++ci)
{
bool Located = FALSE;
project *CChild = GetPlaceInBook(&C->Child, ci);
if(ci < (*SChild)->ChildCount)
{
if(ConfiguredAndStoredProjectIDsMatch(CChild, SGrandChild))
{
Located = TRUE;
AddEntryToGeneration(G, CChild);
SyncProjects(G, CChild, 0, SChild, &SGrandChild);
}
else
{
Located = ReorganiseProjects(G, CChild, 0, SChild, ci, &SGrandChild);
}
}
if(!Located)
{
InsertProjectIntoDB(G, 0, SChild, &SGrandChild, CChild);
}
SGrandChild = SkipHeaderedSectionRecursively(SGrandChild, GetDBStructureHeaderedSection(B_PROJ));
}
uint64_t DeletionCount = (*SChild)->ChildCount - C->Child.ItemCount;
for(int DeletionIndex = 0; DeletionIndex < DeletionCount; ++DeletionIndex)
{
DeleteProject(0, SChild, &SGrandChild, G);
}
if((*SChild)->ChildCount == 0 && (*SChild)->EntryCount == 0)
{
string BaseDir = Wrap0i((*SChild)->BaseDir);
string SearchLocation = Wrap0i((*SChild)->SearchLocation);
string ProjectID = Wrap0i((*SChild)->ID);
DeleteSearchPageFromFilesystem(BaseDir, SearchLocation, ProjectID);
DeleteLandmarksForSearch(C->Index);
}
DecrementCurrentGeneration(G);
if(GotBlock)
{
*Block = (db_block_projects *)(DB.Metadata.File.Buffer.Location + ParentPos);
}
else
{
*SParent = (db_header_project *)(DB.Metadata.File.Buffer.Location + ParentPos);
}
*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 = {};
Accumulator.EntriesInGeneration = InitBook(sizeof(uint32_t), 4);
db_block_projects *SParent = DB.Metadata.Signposts.ProjectsBlock.Ptr;
db_header_project *SChild = LocateFirstChildProjectOfBlock(SParent);
for(uint64_t ci = 0; ci < C->Project.ItemCount; ++ci)
{
bool Located = FALSE;
project *CChild = GetPlaceInBook(&C->Project, ci);
if(ci < SParent->Count)
{
if(ConfiguredAndStoredProjectIDsMatch(CChild, SChild))
{
Located = TRUE;
AddEntryToGeneration(&Accumulator, CChild);
SyncProjects(&Accumulator, CChild, &SParent, 0, &SChild);
}
else
{
Located = ReorganiseProjects(&Accumulator, CChild, &SParent, 0, ci, &SChild);
}
}
if(!Located)
{
InsertProjectIntoDB(&Accumulator, &SParent, 0, &SChild, CChild);
}
SChild = SkipHeaderedSectionRecursively(SChild, GetDBStructureHeaderedSection(B_PROJ));
}
uint64_t DeletionCount = SParent->Count - C->Project.ItemCount;
for(int DeletionIndex = 0; DeletionIndex < DeletionCount; ++DeletionIndex)
{
DeleteProject(&SParent, 0, &SChild, &Accumulator);
}
DeleteStaleAssets();
//PrintAssetsBlock();
//PrintConfig(C);
//PrintGenerations(&Accumulator, FALSE);
FreeBook(&Accumulator.EntriesInGeneration);
//_exit(0);
}
void
SyncGlobalPagesWithInput(neighbourhood *N, buffers *CollationBuffers)
{
MEM_TEST_TOP();
db_block_projects *ProjectsBlock = DB.Metadata.Signposts.ProjectsBlock.Ptr;
string StoredGlobalSearchDir = Wrap0i(ProjectsBlock->GlobalSearchDir);
string StoredGlobalSearchURL = Wrap0i(ProjectsBlock->GlobalSearchURL);
char *StoredGlobalSearchDir0 = MakeString0("l", &StoredGlobalSearchDir);
if(StringsDiffer(StoredGlobalSearchDir, Config->GlobalSearchDir))
{
ClearCopyStringNoFormatOrTerminate(ProjectsBlock->GlobalSearchDir, sizeof(ProjectsBlock->GlobalSearchDir), Config->GlobalSearchDir);
WriteEntireDatabase(N);
}
if(StringsDiffer(StoredGlobalSearchURL, Config->GlobalSearchURL))
{
ClearCopyStringNoFormatOrTerminate(ProjectsBlock->GlobalSearchURL, sizeof(ProjectsBlock->GlobalSearchURL), Config->GlobalSearchURL);
WriteEntireDatabase(N);
}
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);
MEM_TEST_END();
}
void
InitMemoryArena(arena *Arena, int Size)
{
Arena->Size = Size;
if(!(Arena->Location = malloc(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
PackProjectTemplates(project *P, neighbourhood *N, bool *Titled)
{
for(int i = 0; i < P->Child.ItemCount; ++i)
{
PackProjectTemplates(GetPlaceInBook(&P->Child, i), N, Titled);
}
SetCurrentProject(P, N);
// 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(!Titled)
{
Print(stderr, "\n"
"╾─ Packing templates ─╼\n");
*Titled = TRUE;
}
switch(PackTemplate(&CurrentProject->PlayerTemplate, CurrentProject->PlayerTemplatePath, TEMPLATE_PLAYER, CurrentProject))
{
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;
default: break;
}
}
if(CurrentProject->SearchTemplatePath.Length > 0)
{
if(!*Titled)
{
Print(stderr, "\n"
"╾─ Packing templates ─╼\n");
*Titled = TRUE;
}
switch(PackTemplate(&CurrentProject->SearchTemplate, CurrentProject->SearchTemplatePath, TEMPLATE_SEARCH, CurrentProject))
{
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;
default: break;
}
}
}
void
PackTemplates(neighbourhood *N)
{
bool Titled = FALSE;
if(Config->GlobalSearchTemplatePath.Length > 0)
{
if(!Titled)
{
Print(stderr, "\n"
"╾─ Packing templates ─╼\n");
Titled = TRUE;
}
Print(stderr, "\n");
switch(PackTemplate(&Config->SearchTemplate, Config->GlobalSearchTemplatePath, TEMPLATE_GLOBAL_SEARCH, 0))
{
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;
default: break;
}
}
for(int i = 0; i < Config->Project.ItemCount; ++i)
{
PackProjectTemplates(GetPlaceInBook(&Config->Project, i), N, &Titled);
}
}
void
SyncProject(project *P, neighbourhood *N, clash_resolver *ClashResolver, buffers *CollationBuffers, template *BespokeTemplate, bool *Titled)
{
MEM_TEST_TOP();
for(int i = 0; i < P->Child.ItemCount; ++i)
{
/* */ MEM_TEST_MID();
/* +MEM */ SyncProject(GetPlaceInBook(&P->Child, i), N, ClashResolver, CollationBuffers, BespokeTemplate, Titled);
/* */ MEM_TEST_MID();
}
SetCurrentProject(P, N);
if(!*Titled)
{
Print(stderr, "\n"
"┌─ Synchronising with Input Directories ─╼\n"
"└─── ");
*Titled = TRUE;
}
else
{
Print(stderr, "\n"
"╾─── ");
}
PrintLineage(P->Lineage, FALSE);
Print(stderr, " ───╼\n");
/* */ MEM_TEST_MID();
/* +MEM */ SyncDBWithInput(N, ClashResolver, CollationBuffers, BespokeTemplate);
/* */ MEM_TEST_MID();
PushWatchHandle(P->HMMLDir, EXT_HMML, WT_HMML, P, 0);
MEM_TEST_END();
}
char *inotifyEventStrings[] =
{
/* 0x00000001 */ "IN_ACCESS",
/* 0x00000002 */ "IN_MODIFY",
/* 0x00000004 */ "IN_ATTRIB",
/* 0x00000008 */ "IN_CLOSE_WRITE",
/* 0x00000010 */ "IN_CLOSE_NOWRITE",
/* 0x00000020 */ "IN_OPEN",
/* 0x00000040 */ "IN_MOVED_FROM",
/* 0x00000080 */ "IN_MOVED_TO",
/* 0x00000100 */ "IN_CREATE",
/* 0x00000200 */ "IN_DELETE",
/* 0x00000400 */ "IN_DELETE_SELF",
/* 0x00000800 */ "IN_MOVE_SELF",
/* 0x00001000 */ "", // NOTE(matt): Apparently doesn't exist
/* 0x00002000 */ "IN_UNMOUNT",
/* 0x00004000 */ "IN_Q_OVERFLOW",
/* 0x00008000 */ "IN_IGNORED",
/* 0x00010000 */ "",
/* 0x00020000 */ "",
/* 0x00040000 */ "",
/* 0x00080000 */ "",
/* 0x00100000 */ "",
/* 0x00200000 */ "",
/* 0x00400000 */ "",
/* 0x00800000 */ "",
/* 0x01000000 */ "IN_ONLYDIR",
/* 0x02000000 */ "IN_DONT_FOLLOW",
/* 0x04000000 */ "IN_EXCL_UNLINK",
/* 0x08000000 */ "", // NOTE(matt): Apparently doesn't exist
/* 0x10000000 */ "IN_MASK_CREATE",
/* 0x20000000 */ "IN_MASK_ADD",
/* 0x40000000 */ "IN_ISDIR",
/* 0x80000000 */ "IN_ONESHOT",
};
#define DEBUG_EVENTS 0
#if DEBUG_EVENTS
void
PrintEvent(struct inotify_event *Event, int EventIndex, int Indentation)
{
Print(stderr, "\n\n");
Indent(Indentation);
Print(stderr, "Event[%d]\n", EventIndex);
Indent(Indentation);
Print(stderr, " wd: %d\n", Event->wd);
Indent(Indentation);
Print(stderr, " mask: 0x%08X", Event->mask);
for(int i = 0; i < ArrayCount(inotifyEventStrings); ++i)
{
if(Event->mask & (1 << i))
{
Print(stderr, "\n");
Indent(Indentation);
Print(stderr, " %s", inotifyEventStrings[i]);
}
}
Print(stderr, "\n");
Indent(Indentation);
Print(stderr, " cookie: %d", Event->cookie);
Print(stderr, "\n");
Indent(Indentation);
Print(stderr, " len: %d", Event->len);
Print(stderr, "\n");
Indent(Indentation);
Print(stderr, " name: %s", Event->name);
}
#endif
rc
InitAll(neighbourhood *Neighbourhood, clash_resolver *ClashResolver, buffers *CollationBuffers, template *BespokeTemplate)
{
rc Result = RC_SUCCESS;
MEM_TEST_TOP();
RewindCollationBuffers(CollationBuffers);
ResetClashResolver(ClashResolver);
/* */ MEM_TEST_MID();
/* +MEM */ Result = InitDB();
/* */ MEM_TEST_MID();
if(Result == RC_SUCCESS)
{
SyncDB(Config);
Print(stdout, "\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();
//
////
PackTemplates(Neighbourhood);
bool TitledSync = FALSE;
for(int i = 0; i < Config->Project.ItemCount; ++i)
{
/* */ MEM_TEST_MID();
/* +MEM */ SyncProject(GetPlaceInBook(&Config->Project, i), Neighbourhood, ClashResolver, CollationBuffers, BespokeTemplate, &TitledSync);
/* */ MEM_TEST_MID();
}
SyncGlobalPagesWithInput(Neighbourhood, CollationBuffers);
for(int i = 0; i < Assets.ItemCount; ++i)
{
UpdateAssetInDB(GetPlaceInBook(&Assets, i));
}
DeleteStaleAssets();
//PrintAssetsBlock(0);
MEM_TEST_END();
if(GlobalRunning && inotifyInstance != -1)
{
Print(stderr,
"\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]);
}
}
return Result;
}
void
RemoveAndFreeWatchHandles(watch_handles *W)
{
for(int i = 0; i < W->Handles.ItemCount; ++i)
{
watch_handle *This = GetPlaceInBook(&W->Handles, i);
inotify_rm_watch(inotifyInstance, This->Descriptor);
for(int j = 0; j < This->Files.ItemCount; ++j)
{
watch_file *File = GetPlaceInBook(&This->Files, j);
if(File->Handle)
{
fclose(File->Handle);
File->Handle = 0;
}
}
FreeBook(&This->Files);
}
FreeAndReinitialiseBook(&W->Handles);
FreeAndReinitialiseBook(&W->Paths);
}
void
DiscardAllAndFreeConfig(void)
{
FreeSignpostedFile(&DB.Metadata, TRUE); // NOTE(matt): This seems fine
FreeFile(&DB.File, NA); // NOTE(matt): This seems fine
DB.Ready = FALSE;
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.Handles.ItemCount; ++HandleIndex)
{
watch_handle *ThisWatch = GetPlaceInBook(&WatchHandles.Handles, HandleIndex);
if(Event->wd == ThisWatch->Descriptor)
{
if(StringsMatch(ThisWatch->TargetPath, ThisWatch->WatchedPath))
{
if(Event->mask & IN_DELETE_SELF)
{
Update = TRUE;
}
else
{
for(int FileIndex = 0; FileIndex < ThisWatch->Files.ItemCount; ++FileIndex)
{
watch_file *ThisFile = GetPlaceInBook(&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;
}
}
}
else
{
string WatchablePath = GetNearestExistingPath(ThisWatch->TargetPath);
if(StringsDiffer(WatchablePath, ThisWatch->WatchedPath))
{
Update = TRUE;
}
}
}
}
if(Update)
{
UpdateWatchHandles(Event->wd);
Result = GetWatchFileForEvent(Event);
}
return Result;
}
void
ParseAndEitherPrintConfigOrInitAll(string ConfigPath, memory_book *TokensList, neighbourhood *N, clash_resolver *ClashResolver, buffers *CollationBuffers, template *BespokeTemplate)
{
Config = ParseConfig(ConfigPath, TokensList);
if(Config)
{
if(Mode & MODE_DRYRUN)
{
PrintConfig(Config, TRUE);
}
else
{
InitAll(N, ClashResolver, CollationBuffers, BespokeTemplate);
}
}
}
#if DEBUG_EVENTS
void
SquashEventsD(buffer *Events, int BytesRead, struct inotify_event **Event, int *DebugEventIndex)
{
char *PeekPtr = Events->Ptr + sizeof(struct inotify_event) + (*Event)->len;
struct inotify_event *Peek = (struct inotify_event *)PeekPtr;
bool Squashed = FALSE;
while(PeekPtr - Events->Location < BytesRead && (*Event)->wd == Peek->wd && StringsMatch(Wrap0((*Event)->name), Wrap0(Peek->name)))
{
if(!Squashed)
{
Print(stderr, "\n\n"
" Squashing events:");
}
PrintEvent(Peek, ++*DebugEventIndex, 1);
Squashed = TRUE;
*Event = Peek;
Events->Ptr = PeekPtr;
PeekPtr = Events->Ptr + sizeof(struct inotify_event) + (*Event)->len;
Peek = (struct inotify_event *)PeekPtr;
}
if(Squashed)
{
Print(stderr, "\n\n"
" Finished squashing\n");
}
}
#endif
void
SquashEvents(buffer *Events, int BytesRead, struct inotify_event **Event)
{
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(Wrap0((*Event)->name), Wrap0(Peek->name)))
{
*Event = Peek;
Events->Ptr = PeekPtr;
PeekPtr = Events->Ptr + sizeof(struct inotify_event) + (*Event)->len;
Peek = (struct inotify_event *)PeekPtr;
}
}
int
MonitorFilesystem(neighbourhood *N, clash_resolver *ClashResolver, buffers *CollationBuffers, template *BespokeTemplate, string ConfigPath, memory_book *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 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, 0);
//PrintWatchHandles();
#endif
watch_file *WatchFile = GetWatchFileForEvent(Event);
if(WatchFile)
{
#if DEBUG_EVENTS
SquashEventsD(&Events, BytesRead, &Event, &DebugEventIndex);
#else
SquashEvents(&Events, BytesRead, &Event);
#endif
bool WouldHaveDeleted = (Event->mask & (IN_DELETE | IN_MOVED_FROM)) ? TRUE : FALSE;
if(WouldHaveDeleted && (Events.Ptr + sizeof(struct inotify_event) + Event->len) - Events.Location == BytesRead)
{
sleep(1); // NOTE(matt): Give any remaining events time to occur
struct inotify_event EventN = *Event;
char EventNName[Event->len];
ClearCopyStringNoFormatOrTerminate(EventNName, sizeof(EventNName), Wrap0(Event->name));
char *EventSlot1 = Events.Location + sizeof(struct inotify_event) + Event->len;
int NewBytesRead = read(inotifyInstance, EventSlot1, Events.Size - (EventSlot1 - Events.Location)); // TODO(matt): Handle error EINVAL
if(NewBytesRead >= 0)
{
BytesRead = sizeof(struct inotify_event) + EventN.len + NewBytesRead;
struct inotify_event *Event0 = (struct inotify_event *)Events.Location;
*Event0 = EventN;
ClearCopyStringNoFormatOrTerminate(Event0->name, Event0->len, Wrap0i(EventNName));
Event = Event0;
Events.Ptr = Events.Location;
#if DEBUG_EVENTS
SquashEventsD(&Events, BytesRead, &Event, &DebugEventIndex);
#else
SquashEvents(&Events, BytesRead, &Event);
#endif
}
}
switch(WatchFile->Type)
{
case WT_HMML:
{
if(DB.Ready)
{
SetCurrentProject(WatchFile->Project, N);
string BaseFilename = GetBaseFilename(Wrap0(Event->name), WatchFile->Extension);
if(Event->mask & (IN_DELETE | IN_MOVED_FROM))
{
Deleted |= (DeleteEntry(N, ClashResolver, BaseFilename) == RC_SUCCESS);
}
else if(Event->mask & (IN_CLOSE_WRITE | IN_MOVED_TO))
{
Inserted |= (InsertEntry(N, ClashResolver, CollationBuffers, BespokeTemplate, BaseFilename, FALSE) == RC_SUCCESS);
}
}
} break;
case WT_ASSET:
{
if(DB.Ready)
{
UpdateAsset(WatchFile->Asset, FALSE);
UpdateNeighbourhoodPointers(N, &DB.Metadata.Signposts);
#if DEBUG_LANDMARKS
UpdatedAsset = TRUE;
#endif
}
} break;
case WT_CONFIG:
{
DiscardAllAndFreeConfig();
PushWatchHandle(ConfigPath, EXT_NULL, WT_CONFIG, 0, 0);
ParseAndEitherPrintConfigOrInitAll(ConfigPath, TokensList, N, ClashResolver, CollationBuffers, BespokeTemplate);
} break;
}
}
}
Inserted |= (ResolveClashes(ClashResolver, N, CollationBuffers, BespokeTemplate, FALSE) == RC_SUCCESS);
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);
if(!GlobalRunning)
{
PrintC(CS_SUCCESS, "\nExiting cleanly. Thank you for indexing with Cinera\n");
}
_exit(RC_SUCCESS);
}
void
InitWatchHandles(uint32_t DefaultEventsMask, uint64_t DesiredPageSize)
{
WatchHandles.DefaultEventsMask = DefaultEventsMask;
WatchHandles.Handles = InitBook(sizeof(watch_handle), 16);
WatchHandles.Paths = InitBookOfStrings(DesiredPageSize);
}
void
Coda(int Sig)
{
// NOTE(matt): I reckon this suffices because we are single-threaded. A multi-threaded system may require a volatile
// sig_atomic_t Boolean
GlobalRunning = FALSE;
}
void
InitInterruptHandler(void)
{
GlobalRunning = TRUE;
struct sigaction CleanExit = {};
CleanExit.sa_handler = &Coda;
CleanExit.sa_flags = 0;
sigemptyset(&CleanExit.sa_mask);
sigaction(SIGINT, &CleanExit, 0);
}
void
InitMessageControl(void)
{
MESSAGE_CONTROL.RepetitionCount = 1;
MESSAGE_CONTROL.Message[0] = InitBookOfStrings(512);
MESSAGE_CONTROL.Message[1] = InitBookOfStrings(512);
MESSAGE_CONTROL.DesiredMessage = &MESSAGE_CONTROL.Message[0];
MESSAGE_CONTROL.LastMessage = &MESSAGE_CONTROL.Message[1];
}
int
main(int ArgC, char **Args)
{
InitDBStructures();
#if 0
PrintDBStructures(stderr);
_exit(1);
#endif
MEM_TEST_TOP();
InitInterruptHandler();
InitMessageControl();
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);
case 'h':
default:
PrintHelp();
_exit(RC_SUCCESS);
}
}
// NOTE(matt): Init MemoryArenas (they are global)
MEM_TEST_MID();
InitMemoryArena(&MemoryArena, Megabytes(4));
MEM_TEST_MID();
ConfigPath = ExpandPath(Wrap0(ConfigPath), 0);
if(ConfigPath)
{
PrintVersions();
#if DEBUG_MEM
FILE *MemLog = fopen("/home/matt/cinera_mem", "a+");
WriteToFile(MemLog, " Allocated MemoryArena (%d)\n", MemoryArena.Size);
fclose(MemLog);
Print(stdout, " Allocated MemoryArena (%d)\n", MemoryArena.Size);
#endif
#if DEBUG
Print(stdout, "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(4)) == 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()
memory_book TokensList = InitBook(sizeof(tokens), 8);
clash_resolver ClashResolver = {};
InitClashResolver(&ClashResolver);
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);
int inotifyError = errno;
string ConfigPathL = Wrap0(ConfigPath);
PushWatchHandle(ConfigPathL, EXT_NULL, WT_CONFIG, 0, 0);
Config = ParseConfig(ConfigPathL, &TokensList);
if(Config)
{
if(Mode & MODE_EXAMINE)
{
DB.Metadata.File.Buffer.ID = BID_DATABASE;
DB.Metadata.File = InitFile(0, &Config->DatabaseLocation, EXT_NULL, TRUE);
ReadFileIntoBuffer(&DB.Metadata.File); // NOTE(matt): Could we actually catch errors (permissions?) here and bail?
ExamineDB(&DB.Metadata.File);
FreeFile(&DB.Metadata.File, TRUE);
_exit(RC_SUCCESS);
}
if(Mode & MODE_DRYRUN)
{
PrintConfig(Config, FALSE);
}
else
{
/* */ MEM_TEST_MID();
/* +MEM */ InitAll(&Neighbourhood, &ClashResolver, &CollationBuffers, &BespokeTemplate);
/* */ MEM_TEST_MID();
}
}
else if(SeekConfirmation("Print config help?", TRUE))
{
if(ArgC < 2)
{
PrintHelp();
}
else
{
PrintHelpConfig();
}
}
if(inotifyInstance != -1)
{
while(GlobalRunning && MonitorFilesystem(&Neighbourhood, &ClashResolver, &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) && DB.Ready && Config && Config->RespectingPrivacy && time(0) - LastPrivacyCheck > Config->PrivacyCheckInterval)
{
RecheckPrivacy(&Neighbourhood, &ClashResolver, &CollationBuffers, &BespokeTemplate);
}
sleep(GLOBAL_UPDATE_INTERVAL);
}
}
else if(GlobalRunning)
{
string inotifyErrorL = Wrap0(strerror(inotifyError));
SystemError(0, 0, S_ERROR, "inotify initialisation failed with message: ", &inotifyErrorL);
}
DiscardAllAndFreeConfig();
MEM_TEST_END();
}
else
{
SystemError(0, 0, S_ERROR, "Config file path cannot be empty, aborting", 0);
}
Exit();
}