18408 lines
654 KiB
C
18408 lines
654 KiB
C
#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 = ""; // all
|
||
icon_type = "textual"; // or "graphical"
|
||
icon_variants = "all"; // for "graphical" icons, same as art_variants
|
||
|
||
icon_normal = ""; // fills the whole normal row
|
||
icon_focused = ""; // fills the whole focused row
|
||
icon_disabled = ""; // 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, void *OutPtr)
|
||
{
|
||
size_t Length = CharLength * Chars;
|
||
CopyStringToBufferNoFormat(OutPtr, Wrap0i_(InPtr, Length));
|
||
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);
|
||
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 = \"🧪\";\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(64)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; };
|
||
if(ClaimBuffer(&PlayerBuffers->Main, BID_PLAYER_BUFFERS_MAIN, Megabytes(1)) == 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 ▼</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, "<");
|
||
break;
|
||
case '>':
|
||
CopyStringToBuffer(&IndexBuffers->Text, ">");
|
||
break;
|
||
case '&':
|
||
CopyStringToBuffer(&IndexBuffers->Text, "&");
|
||
break;
|
||
case '\"':
|
||
CopyStringToBuffer(&IndexBuffers->Text, """);
|
||
break;
|
||
case '\'':
|
||
CopyStringToBuffer(&IndexBuffers->Text, "'");
|
||
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 ▼</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\">—%.*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, "“");
|
||
CopyStringToBufferHTMLSafe(&IndexBuffers->Text, QuoteInfo.Text);
|
||
CopyStringToBuffer(&IndexBuffers->Text, "”");
|
||
}
|
||
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>•</div><div>Welcome to <cite>%.*s</cite></div><div>•</div></div>\n", (int)ProjectTitle.Length, ProjectTitle.Base);
|
||
}
|
||
|
||
void
|
||
CopyProjectEndStringToBuffer(buffer *B, string ProjectTitle)
|
||
{
|
||
CopyStringToBuffer(B,
|
||
" <div class=\"episodeMarker last\"><div>•</div><div>You have arrived at the (current) end of <cite>%.*s</cite></div><div>•</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>⏫</div><div>Previous: '%s'</div><div>⏫</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\">🟉" : "");
|
||
}
|
||
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\">🎭</div>\n"
|
||
" <div class=\"views_container\">\n"
|
||
" <div class=\"view\" data-id=\"super\" title=\"SUPERtheatre mode\">🏟</div>\n"
|
||
" </div>\n"
|
||
" </div>\n"
|
||
" <div class=\"menu link\">\n"
|
||
" <span>🔗</span>\n"
|
||
" <div class=\"link_container\">\n"
|
||
" <div id=\"cineraLinkMode\">Link to: current 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\"><</span> / <span class=\"help_key\">]</span>, <span class=\"help_key\">></span> <span class=\"help_text\">Jump to previous / next episode</span><br>\n"
|
||
" <span class=\"help_key\">W</span>, <span class=\"help_key\">K</span>, <span class=\"help_key\">P</span> / <span class=\"help_key\">S</span>, <span class=\"help_key\">J</span>, <span class=\"help_key\">N</span> <span class=\"help_text\">Jump to previous / next 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>⏬</div><div>Next: '%s'</div><div>⏬</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>⏫</div><div>Previous: '%s'</div><div>⏫</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>⏬</div><div>Next: '%s'</div><div>⏬</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 ⏶"));
|
||
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();
|
||
}
|