#if 0 ctime -begin ${0%.*}.ctm gcc -g -Wall -fsanitize=address -std=c99 $0 -o ${0%.*} #clang -g -fsanitize=address -std=c99 $0 -o ${0%.*} ctime -end ${0%.*}.ctm exit #endif // config // #include #include #include #include #include #define ArgCountPointer(...) (sizeof((void *[]){__VA_ARGS__})/sizeof(void *)) #define DigitsInInt(I) DigitsInInt_(I, sizeof(*I)) uint8_t DigitsInInt_(void *Int, uint8_t WidthInBytes) { uint8_t Result = 0; switch(WidthInBytes) { case 1: { int8_t This = *(int8_t *)Int; if(This <= 0) { This = ~This; ++Result; if(This == 0) { ++Result; return Result; } } for(; This > 0; This /= 10) { ++Result; } } break; case 2: { int16_t This = *(int16_t *)Int; if(This <= 0) { This = ~This; ++Result; if(This == 0) { ++Result; return Result; } } for(; This > 0; This /= 10) { ++Result; } } break; case 4: { int32_t This = *(int32_t *)Int; if(This <= 0) { This = ~This; ++Result; if(This == 0) { ++Result; return Result; } } for(; This > 0; This /= 10) { ++Result; } } break; case 8: { int64_t This = *(int64_t *)Int; if(This <= 0) { This = ~This; ++Result; if(This == 0) { ++Result; return Result; } } for(; This > 0; This /= 10) { ++Result; } } break; default: { Assert(0); } } return Result; } #define DigitsInUint(I) DigitsInUint_(I, sizeof(*I)) uint8_t DigitsInUint_(void *Uint, uint8_t WidthInBytes) { uint32_t Result = 0; switch(WidthInBytes) { case 1: { uint8_t This = *(uint8_t *)Uint; if(This == 0) { ++Result; return Result; } for(; This > 0; This /= 10) { ++Result; } } break; case 2: { uint16_t This = *(uint16_t *)Uint; if(This == 0) { ++Result; return Result; } for(; This > 0; This /= 10) { ++Result; } } break; case 4: { uint32_t This = *(uint32_t *)Uint; if(This == 0) { ++Result; return Result; } for(; This > 0; This /= 10) { ++Result; } } break; case 8: { uint64_t This = *(uint64_t *)Uint; if(This == 0) { ++Result; return Result; } for(; This > 0; This /= 10) { ++Result; } } break; default: { Assert(0); } } return Result; } #define MakeString(Format, ...) MakeString_(__FILE__, __LINE__, Format, ArgCountPointer(__VA_ARGS__), __VA_ARGS__) string MakeString_(char *Filename, int LineNumber, char *Format, int ArgCount, ...) { // NOTE(matt): Be sure to free built strings when you're finished with them // // Usage: // // Similar to printf and friends, MakeString() takes a format string as its first argument. // While printf has many format specifiers - e.g. %s, %d, %c - MakeString() has two: s, l // s: Null-terminated string, i.e. char * // l: Length-string, i.e. string * // We allocate memory, null-terminate and return the char * // // Examples: // 1. char *BuiltString = MakeString("ss", "First", "Second"); // 2. char *Input = "First"; // char *BuiltString = MakeString("ss", Input, "Second"); // 3. string Input = { .Base = "Second", .Length = StringLength("Second") }; // char *BuiltString = MakeString("sl", "First", &Input); int FormatLength = StringLength(Format); if(FormatLength != ArgCount) { Colourise(CS_FAILURE); fprintf(stderr, "%s:%d MakeString()", Filename, LineNumber); Colourise(CS_END); fprintf(stderr, "\n" " MakeString() has been passed a format string containing "); Colourise(CS_BLUE_BOLD); fprintf(stderr, "%d specifier%s", FormatLength, FormatLength == 1 ? "" : "s"); Colourise(CS_END); fprintf(stderr, ",\n" " and "); Colourise(CS_BLUE_BOLD); fprintf(stderr, "%d argument%s", ArgCount, ArgCount == 1 ? "" : "s"); Colourise(CS_END); fprintf(stderr, ", but these numbers should be equal.\n"); __asm__("int3"); } // TODO(matt): Type-checking, if possible? va_list Args; va_start(Args, ArgCount); string Result = {}; for(int i = 0; i < FormatLength; ++i) { switch(Format[i]) { case 's': { char *Next = va_arg(Args, char *); int NextLength = StringLength(Next); Result.Base = realloc(Result.Base, Result.Length + NextLength); char *Ptr = Result.Base + Result.Length; while(*Next) { *Ptr++ = *Next++; } Result.Length += NextLength; } break; case 'l': { string *Next = va_arg(Args, string *); Result.Base = realloc(Result.Base, Result.Length + Next->Length); char *Ptr = Result.Base + Result.Length; int j; for(j = 0; j < Next->Length && Next->Base[j]; ++j) { *Ptr++ = Next->Base[j]; } Result.Length += j; } break; default: { Colourise(CS_FAILURE); fprintf(stderr, "%s:%d", Filename, LineNumber); Colourise(CS_END); fprintf(stderr, "\n" " MakeString() has been passed an unsupported format specifier: %c\n", Format[i]); __asm__("int3"); } break; } } va_end(Args); return Result; } #define MakeString0(Format, ...) MakeString0_(__FILE__, __LINE__, Format, ArgCountPointer(__VA_ARGS__), __VA_ARGS__) char * MakeString0_(char *Filename, int LineNumber, char *Format, int ArgCount, ...) { // NOTE(matt): Be sure to free built strings when you're finished with them // // Usage: // // Similar to printf and friends, MakeString() takes a format string as its first argument. // While printf has many format specifiers - e.g. %s, %d, %c - MakeString() has two: s, l // s: Null-terminated string, i.e. char * // l: Length-string, i.e. string * // We allocate memory, null-terminate and return the char * // // Examples: // 1. char *BuiltString = MakeString("ss", "First", "Second"); // 2. char *Input = "First"; // char *BuiltString = MakeString("ss", Input, "Second"); // 3. string Input = { .Base = "Second", .Length = StringLength("Second") }; // char *BuiltString = MakeString("sl", "First", &Input); int FormatLength = StringLength(Format); if(FormatLength != ArgCount) { Colourise(CS_FAILURE); fprintf(stderr, "%s:%d MakeString0()", Filename, LineNumber); Colourise(CS_END); fprintf(stderr, "\n" " MakeString0() has been passed a format string containing "); Colourise(CS_BLUE_BOLD); fprintf(stderr, "%d specifier%s", FormatLength, FormatLength == 1 ? "" : "s"); Colourise(CS_END); fprintf(stderr, ",\n" " and "); Colourise(CS_BLUE_BOLD); fprintf(stderr, "%d argument%s", ArgCount, ArgCount == 1 ? "" : "s"); Colourise(CS_END); fprintf(stderr, ", but these numbers should be equal.\n"); __asm__("int3"); } // TODO(matt): Type-checking, if possible? va_list Args; va_start(Args, ArgCount); char *Result = 0; int RequiredSpace = 0; for(int i = 0; i < FormatLength; ++i) { switch(Format[i]) { case 's': { char *Next = va_arg(Args, char *); int NextLength = StringLength(Next); Result = realloc(Result, RequiredSpace + NextLength); char *Ptr = Result + RequiredSpace; while(*Next) { *Ptr++ = *Next++; } RequiredSpace += NextLength; } break; case 'l': { string *Next = va_arg(Args, string *); Result = realloc(Result, RequiredSpace + Next->Length); char *Ptr = Result + RequiredSpace; int j; for(j = 0; j < Next->Length && Next->Base[j]; ++j) { *Ptr++ = Next->Base[j]; } RequiredSpace += j; } break; case 'd': { int32_t Next = *va_arg(Args, int32_t *); int NextLength = DigitsInInt(&Next); Result = realloc(Result, RequiredSpace + NextLength); char *Ptr = Result + RequiredSpace; sprintf(Ptr, "%d", Next); RequiredSpace += NextLength; } break; case 'u': { uint32_t Next = *va_arg(Args, uint32_t *); int NextLength = DigitsInUint(&Next); Result = realloc(Result, RequiredSpace + NextLength); char *Ptr = Result + RequiredSpace; sprintf(Ptr, "%u", Next); RequiredSpace += NextLength; } break; default: { Colourise(CS_FAILURE); fprintf(stderr, "%s:%d", Filename, LineNumber); Colourise(CS_END); fprintf(stderr, "\n" " MakeString0() has been passed an unsupported format specifier: %c\n", Format[i]); __asm__("int3"); } break; } } va_end(Args); ++RequiredSpace; Result = realloc(Result, RequiredSpace); Result[RequiredSpace - 1] = '\0'; return Result; } void PushToken(tokens *Set, token *Token) { // NOTE(matt): We don't bother to push comments or newlines on to the token list //PrintFunctionName("PushToken()"); if(!(Token->Type == TOKEN_NULL || Token->Type == TOKEN_COMMENT || Token->Type == TOKEN_NEWLINE)) { token *This = MakeSpaceInBook(&Set->Token); This->Type = Token->Type; This->LineNumber = Set->CurrentLine; if(Token->Type == TOKEN_NUMBER) { This->int64_t = StringToInt(Token->Content); } else { This->Content = Token->Content; } } } void PrintToken(token *T, uint64_t Index) { fprintf(stderr, "[%lu, %s] ", Index, TokenTypeStrings[T->Type]); if(T->Type == TOKEN_NUMBER) { Colourise(CS_BLUE_BOLD); fprintf(stderr, "%li", T->int64_t); Colourise(CS_END); } else if(T->Type == TOKEN_COMMENT) { PrintStringC(CS_BLACK_BOLD, T->Content); } else { PrintString(T->Content); } fprintf(stderr, "\n"); } void PrintTokens(tokens *T) { //PrintFunctionName("PrintTokens()"); for(int i = 0; i < T->Token.ItemCount; ++i) { PrintToken(GetPlaceInBook(&T->Token, i), i); } } tokens * PushTokens(memory_book *TokensList) { tokens *This = MakeSpaceInBook(TokensList); This->Token = InitBook(sizeof(token), 16); This->CurrentLine = 1; return This; } void FreeTokensList(memory_book *TokensList) { for(int i = 0; i < TokensList->ItemCount; ++i) { tokens *This = GetPlaceInBook(TokensList, i); This->CurrentIndex = 0; This->CurrentLine = 0; FreeBook(&This->Token); FreeFile(&This->File); } TokensList->ItemCount = 0; } void SkipWhitespace(tokens *T, buffer *B) { while(B->Ptr - B->Location < B->Size && (*B->Ptr == ' ' || //*B->Ptr == '\n' || *B->Ptr == '\t')) { ++B->Ptr; } } void Advance(buffer *B, uint64_t Distance) { if(B->Ptr + Distance - B->Location <= B->Size) { B->Ptr += Distance; } } string GetUTF8Character(char *Ptr, uint64_t BytesRemaining) { string Result = {}; if((uint8_t)(Ptr[0] & 0b11111000) == 0b11110000) { ++Result.Length; if(BytesRemaining >= 3) { if((uint8_t)(Ptr[1] & 0b11000000) == 0b10000000 && (uint8_t)(Ptr[2] & 0b11000000) == 0b10000000 && (uint8_t)(Ptr[3] & 0b11000000) == 0b10000000) { Result.Length = 4; Result.Base = Ptr; return Result; } } } else if((uint8_t)(Ptr[0] & 0b11110000) == 0b11100000) { ++Result.Length; if(BytesRemaining >= 2) { if((uint8_t)(Ptr[1] & 0b11000000) == 0b10000000 && (uint8_t)(Ptr[2] & 0b11000000) == 0b10000000) { Result.Length = 3; Result.Base = Ptr; return Result; } } } else if((uint8_t)(Ptr[0] & 0b11100000) == 0b11000000) { ++Result.Length; if(BytesRemaining >= 1) { if((uint8_t)(Ptr[1] & 0b11000000) == 0b10000000) { Result.Length = 2; Result.Base = Ptr; return Result; } } } else if((uint8_t)(Ptr[0] & 0b10000000) == 0b00000000) { ++Result.Length; Result.Base = Ptr; return Result; } for(; BytesRemaining >= Result.Length; ++Result.Length) { if(((uint8_t)(Ptr[Result.Length] & 0b11111000) == 0b11110000) || ((uint8_t)(Ptr[Result.Length] & 0b11110000) == 0b11100000) || ((uint8_t)(Ptr[Result.Length] & 0b11100000) == 0b11000000) || ((uint8_t)(Ptr[Result.Length] & 0b10000000) == 0b00000000)) { break; } } return Result; } typedef struct { string ID; string Icon; string IconNormal; string IconFocused; icon_type IconType; uint64_t IconVariants; asset *IconAsset; string URL; } support; typedef struct { string ID; string Icon; string IconNormal; string IconDisabled; string IconFocused; icon_type IconType; uint64_t IconVariants; asset *IconAsset; string Name; bool Hidden; } medium; typedef struct { string ID; string Name; // TODO(matt): string SortName; string QuoteUsername; string Homepage; _memory_book(support) Support; } person; typedef struct { string ID; string Name; string Plural; bool NonSpeaking; } role; typedef struct { person *Person; role *Role; } credit; typedef struct project { string ID; string Lineage; string WrittenLineage; string Title; string HTMLTitle; string HMMLDir; string TemplatesDir; string SearchTemplatePath; string PlayerTemplatePath; string BaseDir; string BaseURL; string SearchLocation; string PlayerLocation; string Theme; string Art; uint64_t ArtVariants; asset *ArtAsset; string Icon; string IconNormal; string IconDisabled; string IconFocused; icon_type IconType; uint64_t IconVariants; asset *IconAsset; vod_platform VODPlatform; bool DenyBespokeTemplates; bool SingleBrowserTab; bool IgnorePrivacy; person *Owner; _memory_book(credit *) Credit; _memory_book(medium) Medium; medium *DefaultMedium; _memory_book(project) Child; struct project *Parent; genre Genre; numbering Numbering; db_project_index Index; template PlayerTemplate; template SearchTemplate; } project; typedef struct { string DatabaseLocation; string CacheDir; string GlobalTemplatesDir; string GlobalSearchTemplatePath; string GlobalSearchDir; string GlobalSearchURL; string GlobalTheme; template SearchTemplate; string AssetsRootDir; string AssetsRootURL; string CSSDir; string ImagesDir; string JSDir; string QueryString; bool RespectingPrivacy; bool SuppressingPrompts; time_t PrivacyCheckInterval; uint8_t LogLevel; _memory_book(person) Person; _memory_book(role) Role; _memory_book(project) Project; memory_book ResolvedVariables; } config; char *ExpandPath(string Path, string *RelativeToFile); // NOTE(matt): Forward declared. Consider reorganising the code? void PushWatchHandle(string Path, extension_id Extension, watch_type Type, project *Project, asset *Asset); // NOTE(matt): Forward declared. Consider reorganising the code? tokens * Tokenise(memory_book *TokensList, string Path) { tokens *Result = 0; char *Path0 = MakeString0("l", &Path); FILE *Handle = 0; PushWatchHandle(Path, EXT_NULL, WT_CONFIG, 0, 0); if((Handle = fopen(Path0, "r"))) { fclose(Handle); Result = PushTokens(TokensList); Result->File = InitFile(0, &Path, EXT_NULL); ReadFileIntoBuffer(&Result->File); buffer *B = &Result->File.Buffer; SkipWhitespace(Result, B); while(B->Ptr - B->Location < B->Size) { token T = {}; uint64_t Advancement = 0; if(!StringsDifferS(TokenStrings[TOKEN_COMMENT_SINGLE], B)) { T.Type = TOKEN_COMMENT; B->Ptr += StringLength(TokenStrings[TOKEN_COMMENT_SINGLE]); SkipWhitespace(Result, B); T.Content.Base = B->Ptr; while(B->Ptr && *B->Ptr != '\n') { ++T.Content.Length; ++B->Ptr; } if(*B->Ptr == '\n') { ++Result->CurrentLine; } Advancement = 1; } else if(!StringsDifferS(TokenStrings[TOKEN_COMMENT_MULTI_OPEN], B)) { uint64_t CommentDepth = 1; T.Type = TOKEN_COMMENT; B->Ptr += StringLength(TokenStrings[TOKEN_COMMENT_MULTI_OPEN]); SkipWhitespace(Result, B); T.Content.Base = B->Ptr; while(B->Ptr - B->Location < B->Size && CommentDepth) { if(!StringsDifferS(TokenStrings[TOKEN_COMMENT_MULTI_CLOSE], B)) { --CommentDepth; B->Ptr += StringLength(TokenStrings[TOKEN_COMMENT_MULTI_CLOSE]); } else if(B->Ptr - B->Location < B->Size && *B->Ptr == '\n') { ++Result->CurrentLine; ++B->Ptr; } else if(!StringsDifferS(TokenStrings[TOKEN_COMMENT_MULTI_OPEN], B)) { ++CommentDepth; B->Ptr += StringLength(TokenStrings[TOKEN_COMMENT_MULTI_OPEN]); } else { ++B->Ptr; } } T.Content.Length = B->Ptr - T.Content.Base; Advancement = 0;//StringLength(TokenStrings[TOKEN_COMMENT_MULTI_CLOSE]); } else if(!StringsDifferS(TokenStrings[TOKEN_COMMENT_MULTI_CLOSE], B)) { Advancement = 2; T.Type = TOKEN_NULL; string Char = { .Base = B->Ptr, .Length = 2 }; string Filepath = Wrap0(Result->File.Path); ConfigError(&Filepath, Result->CurrentLine, S_WARNING, "Mismatched closing multiline comment marker: ", &Char); } else if(!StringsDifferS(TokenStrings[TOKEN_DOUBLEQUOTE], B)) { T.Type = TOKEN_STRING; ++B->Ptr; T.Content.Base = B->Ptr; while(B->Ptr - B->Location < B->Size && *B->Ptr != '"') { if(*B->Ptr == '\\') { ++T.Content.Length; ++B->Ptr; } ++T.Content.Length; ++B->Ptr; } Advancement = 1; } else if(!StringsDifferS(TokenStrings[TOKEN_MINUS], B)) { T.Type = TOKEN_MINUS; T.Content.Base = B->Ptr; ++T.Content.Length; Advancement = 1; } else if(!StringsDifferS(TokenStrings[TOKEN_ASSIGN], B)) { T.Type = TOKEN_ASSIGN; T.Content.Base = B->Ptr; ++T.Content.Length; Advancement = 1; } else if(!StringsDifferS(TokenStrings[TOKEN_OPEN_BRACE], B)) { T.Type = TOKEN_OPEN_BRACE; T.Content.Base = B->Ptr; ++T.Content.Length; Advancement = 1; } else if(!StringsDifferS(TokenStrings[TOKEN_CLOSE_BRACE], B)) { T.Type = TOKEN_CLOSE_BRACE; T.Content.Base = B->Ptr; ++T.Content.Length; Advancement = 1; } else if(!StringsDifferS(TokenStrings[TOKEN_SEMICOLON], B)) { T.Type = TOKEN_SEMICOLON; T.Content.Base = B->Ptr; ++T.Content.Length; Advancement = 1; } else if(IsValidIdentifierCharacter(*B->Ptr)) { T.Type = TOKEN_IDENTIFIER; T.Content.Base = B->Ptr; while(IsValidIdentifierCharacter(*B->Ptr)) { ++T.Content.Length; ++B->Ptr; } Advancement = 0; } else if(IsNumber(*B->Ptr)) { T.Type = TOKEN_NUMBER; T.Content.Base = B->Ptr; while(IsNumber(*B->Ptr)) { ++T.Content.Length; ++B->Ptr; } Advancement = 0; } else if(*B->Ptr == '\n') { T.Type = TOKEN_NEWLINE; T.Content.Base = B->Ptr; T.Content.Length = 1; ++Result->CurrentLine; Advancement = 1; } else { T.Type = TOKEN_NULL; string Char = GetUTF8Character(B->Ptr, B->Size - (B->Ptr - B->Location)); Advancement = Char.Length; string Filepath = Wrap0(Result->File.Path); if(Char.Base) { ConfigError(&Filepath, Result->CurrentLine, S_WARNING, "Unhandled character (ignored): ", &Char); } else { ConfigErrorInt(&Filepath, Result->CurrentLine, S_WARNING, "Malformed UTF-8 bytes encountered (skipped): ", Char.Length); } } PushToken(Result, &T); Advance(B, Advancement); SkipWhitespace(Result, B); } } else { ConfigError(0, 0, S_WARNING, "Unable to open config file: ", &Path); } Free(Path0); return Result; } bool TokenEquals(tokens *T, char *String) { int i = 0; token *This = GetPlaceInBook(&T->Token, T->CurrentIndex); for(; i < This->Content.Length && *String && This->Content.Base[i] == String[i]; ++i) { } if(i == This->Content.Length && !String[i]) { return TRUE; } return FALSE; } bool TokenIs(tokens *T, token_type Type) { token *This = GetPlaceInBook(&T->Token, T->CurrentIndex); return(Type == This->Type); } bool ExpectToken(tokens *T, token_type Type, token *Dest) { bool Result = FALSE; token *This = GetPlaceInBook(&T->Token, T->CurrentIndex); if(Type == This->Type) { Result = TRUE; if(Dest) { *Dest = *This; } } else { Dest = 0; ConfigErrorExpectation(T, Type, 0); } ++T->CurrentIndex; return Result; } char *FieldTypeNames[] = { "bare", "boolean", "number", "scope", "string", }; typedef enum { FT_BARE, FT_BOOLEAN, FT_NUMBER, FT_SCOPE, FT_STRING, } config_field_type; typedef struct { config_identifier_id ID; config_field_type Type; bool Singleton; bool IsSetLocally; } config_type_field; typedef struct { config_identifier_id ID; bool Permeable; _memory_book(config_type_field) Field; } config_type_spec; config_type_spec * PushTypeSpec(memory_book *TypeSpecs, config_identifier_id ID, bool IsPermeable) { //PrintFunctionName("PushTypeSpec()"); config_type_spec *Result = MakeSpaceInBook(TypeSpecs); Result->ID = ID; Result->Field = InitBook(sizeof(config_type_field), 4); Result->Permeable = IsPermeable; return Result; } void PushTypeSpecField(config_type_spec *Spec, config_field_type Type, config_identifier_id ID, bool Singleton) { //PrintFunctionName("PushTypeSpecField()"); config_type_field *This = MakeSpaceInBook(&Spec->Field); This->Type = Type; This->ID = ID; This->Singleton = Singleton; This->IsSetLocally = FALSE; } _memory_book(config_type_specs) InitTypeSpecs(void) { //PrintFunctionName("InitTypeSpecs()"); memory_book Result = InitBook(sizeof(config_type_spec), 16); config_type_spec *Root = PushTypeSpec(&Result, IDENT_NULL, FALSE); PushTypeSpecField(Root, FT_STRING, IDENT_ASSETS_ROOT_DIR, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_ASSETS_ROOT_URL, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_BASE_DIR, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_BASE_URL, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_CACHE_DIR, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_COHOST, FALSE); PushTypeSpecField(Root, FT_STRING, IDENT_CSS_PATH, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_DB_LOCATION, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_DEFAULT_MEDIUM, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_GENRE, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_GLOBAL_SEARCH_DIR, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_GLOBAL_SEARCH_TEMPLATE, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_GLOBAL_SEARCH_URL, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_GLOBAL_TEMPLATES_DIR, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_GLOBAL_THEME, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_GUEST, FALSE); PushTypeSpecField(Root, FT_STRING, IDENT_HMML_DIR, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_IMAGES_PATH, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_INDEXER, FALSE); PushTypeSpecField(Root, FT_STRING, IDENT_JS_PATH, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_LOG_LEVEL, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_OWNER, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_PLAYER_LOCATION, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_PLAYER_TEMPLATE, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_QUERY_STRING, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_SEARCH_LOCATION, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_SEARCH_TEMPLATE, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_TEMPLATES_DIR, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_VOD_PLATFORM, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_THEME, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_TITLE, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_HTML_TITLE, TRUE); // TODO(matt): Sort out this title list stuff // PushTypeSpecField(Root, FT_STRING, IDENT_TITLE_LIST_DELIMITER, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_TITLE_LIST_PREFIX, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_TITLE_LIST_SUFFIX, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_TITLE_SUFFIX, TRUE); // //// // NOTE(matt): Numbering PushTypeSpecField(Root, FT_STRING, IDENT_NUMBERING_FILENAME_PREFIX, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_NUMBERING_METHOD, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_NUMBERING_SCHEME, TRUE); PushTypeSpecField(Root, FT_NUMBER, IDENT_NUMBERING_START, TRUE); PushTypeSpecField(Root, FT_STRING, IDENT_NUMBERING_UNIT, TRUE); PushTypeSpecField(Root, FT_BOOLEAN, IDENT_NUMBERING_ZERO_PAD, TRUE); // PushTypeSpecField(Root, FT_BARE, IDENT_TITLE_LIST_END, TRUE); PushTypeSpecField(Root, FT_BOOLEAN, IDENT_DENY_BESPOKE_TEMPLATES, TRUE); PushTypeSpecField(Root, FT_BOOLEAN, IDENT_IGNORE_PRIVACY, TRUE); PushTypeSpecField(Root, FT_BOOLEAN, IDENT_SINGLE_BROWSER_TAB, TRUE); PushTypeSpecField(Root, FT_BOOLEAN, IDENT_SUPPRESS_PROMPTS, TRUE); PushTypeSpecField(Root, FT_NUMBER, IDENT_PRIVACY_CHECK_INTERVAL, TRUE); PushTypeSpecField(Root, FT_SCOPE, IDENT_CREDIT, FALSE); PushTypeSpecField(Root, FT_SCOPE, IDENT_INCLUDE, FALSE); PushTypeSpecField(Root, FT_SCOPE, IDENT_MEDIUM, FALSE); PushTypeSpecField(Root, FT_SCOPE, IDENT_PERSON, FALSE); PushTypeSpecField(Root, FT_SCOPE, IDENT_ROLE, FALSE); PushTypeSpecField(Root, FT_SCOPE, IDENT_PROJECT, FALSE); PushTypeSpecField(Root, FT_SCOPE, IDENT_SUPPORT, FALSE); config_type_spec *Support = PushTypeSpec(&Result, IDENT_SUPPORT, FALSE); PushTypeSpecField(Support, FT_STRING, IDENT_ICON, TRUE); PushTypeSpecField(Support, FT_STRING, IDENT_ICON_NORMAL, TRUE); PushTypeSpecField(Support, FT_STRING, IDENT_ICON_FOCUSED, TRUE); PushTypeSpecField(Support, FT_STRING, IDENT_ICON_TYPE, TRUE); PushTypeSpecField(Support, FT_STRING, IDENT_ICON_VARIANTS, TRUE); PushTypeSpecField(Support, FT_STRING, IDENT_URL, TRUE); config_type_spec *Include = PushTypeSpec(&Result, IDENT_INCLUDE, FALSE); PushTypeSpecField(Include, FT_STRING, IDENT_ALLOW, FALSE); PushTypeSpecField(Include, FT_STRING, IDENT_DENY, FALSE); config_type_spec *Person = PushTypeSpec(&Result, IDENT_PERSON, FALSE); PushTypeSpecField(Person, FT_STRING, IDENT_NAME, TRUE); PushTypeSpecField(Person, FT_STRING, IDENT_HOMEPAGE, TRUE); PushTypeSpecField(Person, FT_STRING, IDENT_QUOTE_USERNAME, TRUE); PushTypeSpecField(Person, FT_SCOPE, IDENT_SUPPORT, FALSE); config_type_spec *Role = PushTypeSpec(&Result, IDENT_ROLE, FALSE); PushTypeSpecField(Role, FT_STRING, IDENT_NAME, TRUE); PushTypeSpecField(Role, FT_STRING, IDENT_PLURAL, TRUE); PushTypeSpecField(Role, FT_NUMBER, IDENT_POSITION, TRUE); PushTypeSpecField(Role, FT_BOOLEAN, IDENT_NON_SPEAKING, TRUE); config_type_spec *Credit = PushTypeSpec(&Result, IDENT_CREDIT, FALSE); PushTypeSpecField(Credit, FT_STRING, IDENT_ROLE, FALSE); config_type_spec *Medium = PushTypeSpec(&Result, IDENT_MEDIUM, FALSE); PushTypeSpecField(Medium, FT_STRING, IDENT_ICON, TRUE); PushTypeSpecField(Medium, FT_STRING, IDENT_ICON_NORMAL, TRUE); PushTypeSpecField(Medium, FT_STRING, IDENT_ICON_FOCUSED, TRUE); PushTypeSpecField(Medium, FT_STRING, IDENT_ICON_DISABLED, TRUE); PushTypeSpecField(Medium, FT_STRING, IDENT_ICON_TYPE, TRUE); PushTypeSpecField(Medium, FT_STRING, IDENT_ICON_VARIANTS, TRUE); PushTypeSpecField(Medium, FT_STRING, IDENT_NAME, TRUE); PushTypeSpecField(Medium, FT_BOOLEAN, IDENT_HIDDEN, TRUE); config_type_spec *Project = PushTypeSpec(&Result, IDENT_PROJECT, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_BASE_DIR, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_BASE_URL, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_DEFAULT_MEDIUM, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_GENRE, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_HMML_DIR, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_OWNER, TRUE); // NOTE(matt): Do not remove, because ResolveLocalVariable() recognises it PushTypeSpecField(Project, FT_STRING, IDENT_PLAYER_LOCATION, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_PLAYER_TEMPLATE, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_QUERY_STRING, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_SEARCH_LOCATION, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_SEARCH_TEMPLATE, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_TEMPLATES_DIR, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_VOD_PLATFORM, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_THEME, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_TITLE, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_HTML_TITLE, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_ART, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_ART_VARIANTS, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_ICON, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_ICON_NORMAL, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_ICON_FOCUSED, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_ICON_DISABLED, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_ICON_TYPE, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_ICON_VARIANTS, TRUE); // TODO(matt): Sort out this title list stuff // PushTypeSpecField(Project, FT_STRING, IDENT_TITLE_LIST_DELIMITER, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_TITLE_LIST_PREFIX, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_TITLE_LIST_SUFFIX, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_TITLE_SUFFIX, TRUE); // //// // NOTE(matt): Numbering PushTypeSpecField(Project, FT_STRING, IDENT_NUMBERING_FILENAME_PREFIX, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_NUMBERING_METHOD, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_NUMBERING_SCHEME, TRUE); PushTypeSpecField(Project, FT_NUMBER, IDENT_NUMBERING_START, TRUE); PushTypeSpecField(Project, FT_STRING, IDENT_NUMBERING_UNIT, TRUE); PushTypeSpecField(Project, FT_BOOLEAN, IDENT_NUMBERING_ZERO_PAD, TRUE); // PushTypeSpecField(Project, FT_BARE, IDENT_TITLE_LIST_END, TRUE); // NOTE(matt): Modes PushTypeSpecField(Project, FT_BOOLEAN, IDENT_DENY_BESPOKE_TEMPLATES, TRUE); PushTypeSpecField(Project, FT_BOOLEAN, IDENT_IGNORE_PRIVACY, TRUE); PushTypeSpecField(Project, FT_BOOLEAN, IDENT_SINGLE_BROWSER_TAB, TRUE); // PushTypeSpecField(Project, FT_SCOPE, IDENT_CREDIT, FALSE); PushTypeSpecField(Project, FT_SCOPE, IDENT_INCLUDE, FALSE); PushTypeSpecField(Project, FT_SCOPE, IDENT_MEDIUM, FALSE); PushTypeSpecField(Project, FT_SCOPE, IDENT_PROJECT, FALSE); return Result; } void PrintTypeField(config_type_field *F, config_identifier_id ParentScopeID, int IndentationLevel) { IndentedCarriageReturn(IndentationLevel); PrintC(CS_YELLOW_BOLD, ConfigIdentifiers[F->ID].String); PrintC(CS_BLACK_BOLD, " ["); if(F->Type == FT_NUMBER) { PrintC(CS_BLUE_BOLD, FieldTypeNames[F->Type]); } else if(F->Type == FT_BARE) { PrintC(CS_BLUE, FieldTypeNames[F->Type]); } else{ Colourise(CS_GREEN_BOLD); fprintf(stderr, "\"%s\"", FieldTypeNames[F->Type]); Colourise(CS_END); } PrintC(CS_BLACK_BOLD, " • "); if(F->Singleton) { PrintC(CS_CYAN, "single"); } else { PrintC(CS_MAGENTA, "multi"); } PrintC(CS_BLACK_BOLD, "]"); // TODO(matt): Consider preventing the description from being written if it had already been // written for a higher-level scope if(ConfigIdentifiers[F->ID].IdentifierDescription) { if((F->ID == IDENT_ICON || F->ID == IDENT_NAME) && ParentScopeID == IDENT_MEDIUM) { if(!ConfigIdentifiers[F->ID].IdentifierDescription_MediumDisplayed) { ++IndentationLevel; IndentedCarriageReturn(IndentationLevel); TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0(ConfigIdentifiers[F->ID].IdentifierDescription_Medium)); ConfigIdentifiers[F->ID].IdentifierDescription_MediumDisplayed = TRUE; --IndentationLevel; } } else if((F->ID == IDENT_NAME) && ParentScopeID == IDENT_ROLE) { if(!ConfigIdentifiers[F->ID].IdentifierDescription_RoleDisplayed) { ++IndentationLevel; IndentedCarriageReturn(IndentationLevel); TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0(ConfigIdentifiers[F->ID].IdentifierDescription_Role)); ConfigIdentifiers[F->ID].IdentifierDescription_RoleDisplayed = TRUE; --IndentationLevel; } } else if((F->ID == IDENT_ROLE) && ParentScopeID == IDENT_CREDIT) { if(!ConfigIdentifiers[F->ID].IdentifierDescription_CreditDisplayed) { ++IndentationLevel; IndentedCarriageReturn(IndentationLevel); TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0(ConfigIdentifiers[F->ID].IdentifierDescription_Credit)); ConfigIdentifiers[F->ID].IdentifierDescription_CreditDisplayed = TRUE; --IndentationLevel; } } else { if(!ConfigIdentifiers[F->ID].IdentifierDescriptionDisplayed) { ++IndentationLevel; IndentedCarriageReturn(IndentationLevel); TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0(ConfigIdentifiers[F->ID].IdentifierDescription)); ConfigIdentifiers[F->ID].IdentifierDescriptionDisplayed = TRUE; --IndentationLevel; } } } } void PrintTypeSpec(config_type_spec *S, int IndentationLevel) { if(S->ID != IDENT_NULL) { IndentedCarriageReturn(IndentationLevel); ++IndentationLevel; PrintStringC(CS_CYAN, Wrap0(ConfigIdentifiers[S->ID].String)); } if(ConfigIdentifiers[S->ID].IdentifierDescription && !ConfigIdentifiers[S->ID].IdentifierDescriptionDisplayed) { IndentedCarriageReturn(IndentationLevel); TypesetString(INDENT_WIDTH * IndentationLevel, Wrap0(ConfigIdentifiers[S->ID].IdentifierDescription)); ConfigIdentifiers[S->ID].IdentifierDescriptionDisplayed = TRUE; } for(int i = 0; i < S->Field.ItemCount; ++i) { config_type_field *This = GetPlaceInBook(&S->Field, i); if(!(S->ID == IDENT_NULL && This->Type == FT_SCOPE)) { PrintTypeField(This, S->ID, IndentationLevel); } } if(S->ID != IDENT_NULL) { --IndentationLevel; } } void PrintTypeSpecs(memory_book *TypeSpecs, int IndentationLevel) { fprintf(stderr, "\n"); // TODO(matt): Document the fact that scopes within scopes are fully configurable, even though // we only print out the "id" of scopes, omitting their containing fields for(int i = 0; i < TypeSpecs->ItemCount; ++i) { PrintTypeSpec(GetPlaceInBook(TypeSpecs, i), IndentationLevel); } } void FreeTypeSpecs(memory_book *TypeSpecs) { for(int i = 0; i < TypeSpecs->ItemCount; ++i) { config_type_spec *This = GetPlaceInBook(TypeSpecs, i); FreeBook(&This->Field); } FreeBook(TypeSpecs); } typedef struct { char *Filename; uint64_t LineNumber; } token_position; typedef enum { PT_BOOL, PT_INT64, PT_STRING, } pair_type; typedef struct { config_identifier_id Key; union { string String; int64_t int64_t; bool bool; }; token_position Position; pair_type Type; } config_pair; typedef struct scope_tree { config_pair ID; _memory_book(scope_tree) Trees; _memory_book(config_pair) Pairs; struct scope_tree *Parent; config_type_spec TypeSpec; //token_position Position; // NOTE(matt): We chose not to make ScopeTokens() set this value } scope_tree; config_type_spec * GetTypeSpec(memory_book *TypeSpecs, config_identifier_id ID) { config_type_spec *Result = 0; for(int i = 0; i < TypeSpecs->ItemCount; ++i) { config_type_spec *This = GetPlaceInBook(TypeSpecs, i); if(This->ID == ID) { Result = This; break; } } return Result; } void SetTypeSpec(scope_tree *Type, memory_book *TypeSpecs) { config_type_spec *Spec = GetTypeSpec(TypeSpecs, Type->ID.Key); //PrintTypeSpec(Spec); Type->TypeSpec.ID = Spec->ID; Type->TypeSpec.Permeable = Spec->Permeable; Type->TypeSpec.Field = InitBook(sizeof(config_type_field), 4); for(int i = 0; i < Spec->Field.ItemCount; ++i) { config_type_field *Src = GetPlaceInBook(&Spec->Field, i); config_type_field *Dest = MakeSpaceInBook(&Type->TypeSpec.Field); Dest->Type = Src->Type; Dest->ID = Src->ID; Dest->Singleton = Src->Singleton; Dest->IsSetLocally = Src->IsSetLocally; } } config_type_field * CurrentFieldIsInSpec(config_type_spec *Spec, tokens *T) { config_type_field *Result = 0; if(Spec) { token *Token = GetPlaceInBook(&T->Token, T->CurrentIndex); for(int i = 0; i < Spec->Field.ItemCount; ++i) { config_type_field *Field = GetPlaceInBook(&Spec->Field, i); if(!StringsDifferLv0(Token->Content, ConfigIdentifiers[Field->ID].String)) { Result = Field; break; } } } return Result; } typedef struct { string Filename; memory_book Allow; memory_book Deny; } config_include; bool ParseMultiStringAssignment(tokens *T, memory_book *Dest, config_type_field *Field) { ++T->CurrentIndex; token Token = {}; if(!ExpectToken(T, TOKEN_ASSIGN, &Token)) { return FALSE; } if(!ExpectToken(T, TOKEN_STRING, &Token)) { return FALSE; } string *This = MakeSpaceInBook(Dest); *This = Token.Content; Field->IsSetLocally = TRUE; if(!ExpectToken(T, TOKEN_SEMICOLON, &Token)) { return FALSE; } return TRUE; } void PrintCurrentToken(colour_code C, tokens *T) { Colourise(C); PrintToken(GetPlaceInBook(&T->Token, T->CurrentIndex), T->CurrentIndex); Colourise(CS_END); } bool DepartAssignment(tokens *T) { uint64_t ScopeLevel = 0; do { if(TokenIs(T, TOKEN_IDENTIFIER)) { ++T->CurrentIndex; if(!ExpectToken(T, TOKEN_ASSIGN, 0)) { return FALSE; } if(TokenIs(T, TOKEN_STRING) || TokenIs(T, TOKEN_NUMBER)) { ++T->CurrentIndex; } else { ConfigErrorExpectation(T, TOKEN_STRING, TOKEN_NUMBER); return FALSE; } if(TokenIs(T, TOKEN_SEMICOLON)) { ++T->CurrentIndex; } else if(TokenIs(T, TOKEN_OPEN_BRACE)) { ++ScopeLevel; ++T->CurrentIndex; } else { ConfigErrorExpectation(T, TOKEN_SEMICOLON, TOKEN_OPEN_BRACE); return FALSE; } } else if(TokenIs(T, TOKEN_CLOSE_BRACE)) { --ScopeLevel; ++T->CurrentIndex; } else { ConfigErrorExpectation(T, TOKEN_IDENTIFIER, TOKEN_CLOSE_BRACE); return FALSE; } } while(ScopeLevel > 0); return TRUE; } bool DepartIncludeAssignment(tokens *T, uint64_t IncludeIdentifierTokenIndex) { T->CurrentIndex = IncludeIdentifierTokenIndex; return(DepartAssignment(T)); } void FreeString(string *S) { Free(S->Base); S->Length = 0; } void FreeInclude(config_include *I) { FreeBook(&I->Allow); FreeBook(&I->Deny); free(I); I = 0; } bool IsStringEmpty(string *S) { return S ? S->Length == 0 : TRUE; } void FreeScopeTreeRecursively(scope_tree *T) { FreeBook(&T->Pairs); FreeBook(&T->TypeSpec.Field); //Free(T->Position.Filename); for(int i = 0; i < T->Trees.ItemCount; ++i) { FreeScopeTreeRecursively(GetPlaceInBook(&T->Trees, i)); } FreeBook(&T->Trees); } void FreeScopeTree(scope_tree *T) { FreeBook(&T->Pairs); FreeBook(&T->TypeSpec.Field); //Free(T->Position.Filename); for(int i = 0; i < T->Trees.ItemCount; ++i) { FreeScopeTreeRecursively(GetPlaceInBook(&T->Trees, i)); } FreeBook(&T->Trees); Free(T); } void PrintPair(config_pair *P, char *Delimiter, bool FillSyntax, uint64_t Indentation, bool PrependNewline, bool AppendNewline) { if(PrependNewline) { fprintf(stderr, "\n"); } Indent(Indentation); PrintC(CS_YELLOW_BOLD, ConfigIdentifiers[P->Key].String); fprintf(stderr, "%s", Delimiter); if(P->Type == PT_BOOL) { if(P->bool == TRUE) { Colourise(CS_GREEN); if(FillSyntax) { fprintf(stderr, "\""); } fprintf(stderr, "true"); if(FillSyntax) { fprintf(stderr, "\""); } } else { Colourise(CS_RED); if(FillSyntax) { fprintf(stderr, "\""); } fprintf(stderr, "false"); if(FillSyntax) { fprintf(stderr, "\""); } } Colourise(CS_END); if(FillSyntax) { fprintf(stderr, ";"); } } else if(P->Type == PT_INT64) { switch(P->Key) { case IDENT_GENRE: { Colourise(CS_GREEN_BOLD); if(FillSyntax) { fprintf(stderr, "\""); } fprintf(stderr, "%s", GenreStrings[P->int64_t]); if(FillSyntax) { fprintf(stderr, "\""); } } break; case IDENT_LOG_LEVEL: { Colourise(CS_GREEN_BOLD); if(FillSyntax) { fprintf(stderr, "\""); } fprintf(stderr, "%s", LogLevelStrings[P->int64_t]); if(FillSyntax) { fprintf(stderr, "\""); } } break; case IDENT_NUMBERING_SCHEME: { Colourise(CS_GREEN_BOLD); if(FillSyntax) { fprintf(stderr, "\""); } fprintf(stderr, "%s", NumberingSchemeStrings[P->int64_t]); if(FillSyntax) { fprintf(stderr, "\""); } } break; default: { Colourise(CS_BLUE_BOLD); fprintf(stderr, "%li", P->int64_t); } break; } Colourise(CS_END); if(FillSyntax) { fprintf(stderr, ";"); } } else if(P->Type == PT_STRING) { if(P->String.Length) { Colourise(CS_GREEN_BOLD); if(FillSyntax) { fprintf(stderr, "\""); } PrintString(P->String); if(FillSyntax) { fprintf(stderr, "\""); } Colourise(CS_END); if(FillSyntax) { fprintf(stderr, ";"); } } else { PrintC(CS_YELLOW, "[unset]"); } } if(AppendNewline) { fprintf(stderr, "\n"); } } void PrintScopeID(config_pair *P, char *Delimiter, uint64_t Indentation) { fprintf(stderr, "\n"); Indent(Indentation); PrintC(CS_YELLOW_BOLD, ConfigIdentifiers[P->Key].String); fprintf(stderr, "%s", Delimiter); if(P->String.Length) { Colourise(CS_GREEN_BOLD); fprintf(stderr, "\""); PrintString(P->String); fprintf(stderr, "\""); Colourise(CS_END); IndentedCarriageReturn(Indentation); fprintf(stderr, "{"); } else { PrintC(CS_YELLOW, "[unset]"); } } void PrintBool(char *Title, bool Bool, char *Delimiter, bool FillSyntax, uint64_t Indentation, bool PrependNewline, bool AppendNewline) { if(PrependNewline) { fprintf(stderr, "\n"); } Indent(Indentation); PrintC(CS_YELLOW_BOLD, Title); fprintf(stderr, "%s", Delimiter); if(Bool == TRUE) { Colourise(CS_GREEN); if(FillSyntax) { fprintf(stderr, "\""); } fprintf(stderr, "true"); if(FillSyntax) { fprintf(stderr, "\""); } } else { Colourise(CS_RED); if(FillSyntax) { fprintf(stderr, "\""); } fprintf(stderr, "false"); if(FillSyntax) { fprintf(stderr, "\""); } } Colourise(CS_END); if(AppendNewline) { fprintf(stderr, "\n"); } } void PrintScopeTree(scope_tree *T, int IndentationLevel); void PrintScopeTree(scope_tree *T, int IndentationLevel) { if(T) { if(T->ID.Key != IDENT_NULL) { PrintScopeID(&T->ID, " = ", IndentationLevel); ++IndentationLevel; } bool ShouldPrependNewline = TRUE; bool ShouldFillSyntax = TRUE; for(int i = 0; i < T->Pairs.ItemCount; ++i) { PrintPair(GetPlaceInBook(&T->Pairs, i), " = ", ShouldFillSyntax, IndentationLevel, ShouldPrependNewline, FALSE); } for(int i = 0; i < T->Trees.ItemCount; ++i) { PrintScopeTree(GetPlaceInBook(&T->Trees, i), IndentationLevel); } if(T->ID.Key != IDENT_NULL) { --IndentationLevel; IndentedCarriageReturn(IndentationLevel); fprintf(stderr, "}"); } } } void PushPair(scope_tree *Parent, config_pair *P) { config_pair *New = MakeSpaceInBook(&Parent->Pairs); *New = *P; } config_pair * GetPair(scope_tree *Parent, config_identifier_id FieldID) { config_pair *Result = 0; for(int i = 0; i < Parent->Pairs.ItemCount; ++i) { config_pair *This = GetPlaceInBook(&Parent->Pairs, i); if(This->Key == FieldID) { Result = This; break; } } return Result; } void InitScopeBooks(scope_tree *Scope) { Scope->Trees = InitBook(sizeof(scope_tree), 4); Scope->Pairs = InitBook(sizeof(config_pair), 4); } scope_tree * InitRootScopeTree(void) { scope_tree *Result = calloc(1, sizeof(scope_tree)); InitScopeBooks(Result); return Result; } scope_tree * PushScope(memory_book *TypeSpecs, scope_tree *Parent, config_pair *ID) { scope_tree *This = MakeSpaceInBook(&Parent->Trees); InitScopeBooks(This); This->ID = *ID; SetTypeSpec(This, TypeSpecs); This->Parent = Parent; return(This); } config_identifier_id GetConfigIdentifierFromToken(token *T) { for(config_identifier_id i = 0; i < IDENT_COUNT; ++i) { if(!StringsDifferLv0(T->Content, ConfigIdentifiers[i].String)) { return i; } } return IDENT_NULL; } config_identifier_id GetConfigIdentifierFromString(string S) { for(config_identifier_id i = 0; i < IDENT_COUNT; ++i) { if(!StringsDifferLv0(S, ConfigIdentifiers[i].String)) { return i; } } return IDENT_NULL; } config_type_field * FieldIsInSpec(config_type_spec *Spec, config_identifier_id ID) { config_type_field *Result = 0; for(int i = 0; i < Spec->Field.ItemCount; ++i) { config_type_field *This = GetPlaceInBook(&Spec->Field, i); if(ID == This->ID) { Result = This; break; } } return Result; } bool PairsMatch(config_pair *A, config_pair *B) { bool Result = FALSE; if(A->Type == B->Type && A->Key == B->Key) { switch(A->Type) { case PT_BOOL: { Result = A->bool == B->bool; } break; case PT_INT64: { Result = A->int64_t == B->int64_t; } break; case PT_STRING: { Result = StringsMatch(A->String, B->String); } break; } } return Result; } scope_tree * GetScope(scope_tree *T, config_pair *ID, bool IsSingleton) { scope_tree *Result = 0; for(int i = 0; i < T->Trees.ItemCount; ++i) { scope_tree *This = GetPlaceInBook(&T->Trees, i); if(IsSingleton) { if(ID->Key == This->ID.Key) { Result = This; break; } } else { if(PairsMatch(ID, &This->ID)) { Result = This; break; } } } return Result; } config_pair * GetAssignment(scope_tree *T, config_pair *Assignment, bool IsSingleton) { config_pair *Result = 0; for(int i = 0; i < T->Pairs.ItemCount; ++i) { config_pair *This = GetPlaceInBook(&T->Pairs, i); if(IsSingleton) { if(Assignment->Type == This->Type && Assignment->Key == This->Key) { Result = This; break; } } else { if(PairsMatch(Assignment, This)) { Result = This; break; } } } return Result; } scope_tree *CopyScope(memory_book *TypeSpecs, scope_tree *Dest, scope_tree *Src); void CopyScopeContents(memory_book *TypeSpecs, scope_tree *Dest, scope_tree *Src) { for(int i = 0; i < Src->Pairs.ItemCount; ++i) { config_pair *Pair = GetPlaceInBook(&Src->Pairs, i); PushPair(Dest, Pair); } for(int i = 0; i < Src->Trees.ItemCount; ++i) { scope_tree *Tree = GetPlaceInBook(&Src->Trees, i); CopyScope(TypeSpecs, Dest, Tree); } } scope_tree * CopyScope(memory_book *TypeSpecs, scope_tree *Dest, scope_tree *Src) { Dest = PushScope(TypeSpecs, Dest, &Src->ID); CopyScopeContents(TypeSpecs, Dest, Src); return Dest; } void MergeScopes(memory_book *TypeSpecs, scope_tree *Dest, scope_tree *Src, bool Intergenerational) { if(!Intergenerational || Dest->TypeSpec.Permeable) { for(int i = 0; i < Src->Pairs.ItemCount; ++i) { config_pair *Pair = GetPlaceInBook(&Src->Pairs, i); config_type_field *Field = FieldIsInSpec(&Dest->TypeSpec, Pair->Key); if(Field) { config_pair *ThisAssignment = GetAssignment(Dest, Pair, Field->Singleton); if(!ThisAssignment) { PushPair(Dest, Pair); } } } for(int i = 0; i < Src->Trees.ItemCount; ++i) { scope_tree *Tree = GetPlaceInBook(&Src->Trees, i); config_type_field *Field = FieldIsInSpec(&Dest->TypeSpec, Tree->ID.Key); if(Field && !(Intergenerational && Tree->ID.Key == Dest->ID.Key)) { scope_tree *ThisAssignment = GetScope(Dest, &Tree->ID, Field->Singleton); if(!ThisAssignment) { CopyScope(TypeSpecs, Dest, Tree); } } } } } typedef enum { CRT_NULL, CRT_ALLOW, CRT_DENY, } config_rule_type; typedef struct { config_rule_type Scheme; _memory_book(config_identifier_id) ID; } config_include_rules; void PushRule(config_include_rules *R, config_identifier_id ID) { for(int i = 0; i < R->ID.ItemCount; ++i) { config_identifier_id *This = GetPlaceInBook(&R->ID, i); if(ID == *This) { return; } } config_identifier_id *This = MakeSpaceInBook(&R->ID); *This = ID; } void ParseRuleString(string *Filename, memory_book *TypeSpecs, config_type_spec *ParentTypeSpec, config_include_rules *Rules, token *RuleString) { int i = SkipWhitespaceS(&RuleString->Content, 0); config_type_field *Field = 0; for(; i < RuleString->Content.Length; i = SkipWhitespaceS(&RuleString->Content, i)) { if(IsValidIdentifierCharacter(RuleString->Content.Base[i])) { string S = {}; S.Base = RuleString->Content.Base + i; for(; i < RuleString->Content.Length && IsValidIdentifierCharacter(RuleString->Content.Base[i]); ++i) { ++S.Length; } config_identifier_id ID; if((ID = GetConfigIdentifierFromString(S)) != IDENT_NULL) { if((Field = FieldIsInSpec(ParentTypeSpec, ID))) { PushRule(Rules, ID); if(i == RuleString->Content.Length && (ParentTypeSpec = GetTypeSpec(TypeSpecs, Field->ID))) { for(int j = 0; j < ParentTypeSpec->Field.ItemCount; ++j) { config_type_field *ParentField = GetPlaceInBook(&ParentTypeSpec->Field, j); PushRule(Rules, ParentField->ID); config_type_spec *ThisTypeSpec = GetTypeSpec(TypeSpecs, ParentField->ID); if(ThisTypeSpec) { for(int k = 0; k < ThisTypeSpec->Field.ItemCount; ++k) { config_type_field *ThisField = GetPlaceInBook(&ThisTypeSpec->Field, k); PushRule(Rules, ThisField->ID); } } } } } else { ConfigError(Filename, RuleString->LineNumber, S_WARNING, "Field not in spec: ", &S); PrintTypeSpec(ParentTypeSpec, 0); return; } } else { ConfigError(Filename, RuleString->LineNumber, S_WARNING, "Unknown identifier: ", &S); return; } } else if(RuleString->Content.Base[i] == '.') { if(Field) { ParentTypeSpec = GetTypeSpec(TypeSpecs, Field->ID); ++i; if(!ParentTypeSpec) { string This = Wrap0(ConfigIdentifiers[Field->ID].String); ConfigError(Filename, RuleString->LineNumber, S_WARNING, "Invalid permission, type has no members: ", &This); } } else { ConfigError(Filename, RuleString->LineNumber, S_WARNING, "Malformed rule string. \".\" must be preceded by a valid type", 0); return; } } else { string Char = GetUTF8Character(RuleString->Content.Base + i, RuleString->Content.Length - i); ConfigError(Filename, RuleString->LineNumber, S_WARNING, "Invalid character in include rule: ", &Char); return; } } } rc ParseIncludeRules(memory_book *TypeSpecs, config_type_spec *ParentTypeSpec, tokens *T, config_include_rules *Rules) { uint64_t IncludeIdentifierTokenIndex = T->CurrentIndex - 3; if(TokenIs(T, TOKEN_SEMICOLON)) { ++T->CurrentIndex; return RC_SUCCESS; } else if(TokenIs(T, TOKEN_OPEN_BRACE)) { ++T->CurrentIndex; for(; T->CurrentIndex < T->Token.ItemCount;) { string Filepath = Wrap0(T->File.Path); if(TokenIs(T, TOKEN_IDENTIFIER)) { if(TokenEquals(T, "allow")) { if(Rules->Scheme == CRT_DENY) { FreeBook(&Rules->ID); token *This = GetPlaceInBook(&T->Token, T->CurrentIndex); ConfigError(&Filepath, This->LineNumber, S_WARNING, "Mixture of allow and deny identifiers in \"include\" scope", 0); return DepartIncludeAssignment(T, IncludeIdentifierTokenIndex) ? RC_SCHEME_MIXTURE : RC_SYNTAX_ERROR; } Rules->Scheme = CRT_ALLOW; } else if(TokenEquals(T, "deny")) { if(Rules->Scheme == CRT_ALLOW) { FreeBook(&Rules->ID); token *This = GetPlaceInBook(&T->Token, T->CurrentIndex); ConfigError(&Filepath, This->LineNumber, S_WARNING, "Mixture of allow and deny identifiers in \"include\" scope", 0); return DepartIncludeAssignment(T, IncludeIdentifierTokenIndex) ? RC_SCHEME_MIXTURE : RC_SYNTAX_ERROR; } Rules->Scheme = CRT_DENY; } else { token *This = GetPlaceInBook(&T->Token, T->CurrentIndex); ConfigError(&Filepath, This->LineNumber, S_WARNING, "Invalid identifier in \"include\" scope: ", &This->Content); fprintf(stderr, " Valid identifiers:\n" " allow\n" " deny\n"); FreeBook(&Rules->ID); return DepartIncludeAssignment(T, IncludeIdentifierTokenIndex) ? RC_INVALID_IDENTIFIER : RC_SYNTAX_ERROR; } ++T->CurrentIndex; if(!ExpectToken(T, TOKEN_ASSIGN, 0)) { FreeBook(&Rules->ID); return RC_SYNTAX_ERROR; } token RuleString = {}; if(!ExpectToken(T, TOKEN_STRING, &RuleString)) { FreeBook(&Rules->ID); return RC_SYNTAX_ERROR; } ParseRuleString(&Filepath, TypeSpecs, ParentTypeSpec, Rules, &RuleString); if(!ExpectToken(T, TOKEN_SEMICOLON, 0)) { FreeBook(&Rules->ID); return RC_SYNTAX_ERROR; } } else if(TokenIs(T, TOKEN_CLOSE_BRACE)) { ++T->CurrentIndex; return RC_SUCCESS; } else { FreeBook(&Rules->ID); return RC_SYNTAX_ERROR; } } } else { FreeBook(&Rules->ID); ConfigErrorExpectation(T, TOKEN_SEMICOLON, TOKEN_OPEN_BRACE); return RC_SYNTAX_ERROR; } return RC_SUCCESS; } bool FieldIsPermitted(config_include_rules *Rules, config_type_field *Field) { for(int i = 0; i < Rules->ID.ItemCount; ++i) { config_identifier_id *This = GetPlaceInBook(&Rules->ID, i); if(Field->ID == *This) { return Rules->Scheme == CRT_ALLOW ? TRUE : FALSE; } } return Rules->Scheme == CRT_ALLOW ? FALSE : TRUE; } void ResetFields(config_type_spec *S) { for(int i = 0; i < S->Field.ItemCount; ++i) { config_type_field *This = GetPlaceInBook(&S->Field, i); This->IsSetLocally = FALSE; } } scope_tree * PushDefaultScope(memory_book *TypeSpecs, scope_tree *Parent, config_identifier_id Key, string Value) { config_pair ID = { .Key = Key, .String = Value, .Type = PT_STRING }; return PushScope(TypeSpecs, Parent, &ID); } void PushDefaultPair(scope_tree *Parent, config_identifier_id Key, string Value) { config_pair Pair = { .Key = Key, .String = Value, .Type = PT_STRING }; return PushPair(Parent, &Pair); } void PushDefaultIntPair(scope_tree *Parent, config_identifier_id Key, uint64_t Value) { config_pair IntPair = { .Key = Key, .int64_t = Value, .Type = PT_INT64 }; return PushPair(Parent, &IntPair); } void PushDefaultBoolPair(scope_tree *Parent, config_identifier_id Key, bool Value) { config_pair BoolPair = { .Key = Key, .bool = Value, .Type = PT_BOOL }; return PushPair(Parent, &BoolPair); } #define DEFAULT_PRIVACY_CHECK_INTERVAL 60 * 4 void SetDefaults(scope_tree *Root, memory_book *TypeSpecs) { scope_tree *SupportPatreon = PushDefaultScope(TypeSpecs, Root, IDENT_SUPPORT, Wrap0("patreon")); PushDefaultPair(SupportPatreon, IDENT_ICON, Wrap0("cinera_sprite_patreon.png")); PushDefaultIntPair(SupportPatreon, IDENT_ICON_TYPE, IT_GRAPHICAL); PushDefaultIntPair(SupportPatreon, IDENT_ICON_VARIANTS, GetArtVariantFromString(Wrap0("normal"))); PushDefaultPair(SupportPatreon, IDENT_URL, Wrap0("https://patreon.com/$person")); scope_tree *SupportSendOwl = PushDefaultScope(TypeSpecs, Root, IDENT_SUPPORT, Wrap0("sendowl")); PushDefaultPair(SupportSendOwl, IDENT_ICON, Wrap0("cinera_sprite_sendowl.png")); scope_tree *MediumAuthored = PushDefaultScope(TypeSpecs, Root, IDENT_MEDIUM, Wrap0("authored")); PushDefaultPair(MediumAuthored, IDENT_ICON, Wrap0("🗪")); PushDefaultPair(MediumAuthored, IDENT_NAME, Wrap0("Chat Comment")); scope_tree *RoleIndexer = PushDefaultScope(TypeSpecs, Root, IDENT_ROLE, Wrap0("indexer")); PushDefaultPair(RoleIndexer, IDENT_NAME, Wrap0("Indexer")); PushDefaultBoolPair(RoleIndexer, IDENT_NON_SPEAKING, TRUE); PushDefaultIntPair(RoleIndexer, IDENT_POSITION, -1); PushDefaultPair(Root, IDENT_QUERY_STRING, Wrap0("r")); PushDefaultPair(Root, IDENT_THEME, Wrap0("$origin")); PushDefaultPair(Root, IDENT_CACHE_DIR, Wrap0("$XDG_CACHE_HOME/cinera")); PushDefaultPair(Root, IDENT_DB_LOCATION, Wrap0("$XDG_CONFIG_HOME/cinera/cinera.db")); // NOTE(matt): Numbering PushDefaultPair(Root, IDENT_NUMBERING_FILENAME_PREFIX, Wrap0("$project")); PushDefaultIntPair(Root, IDENT_NUMBERING_METHOD, NM_FILENAME_DERIVED); PushDefaultIntPair(Root, IDENT_NUMBERING_SCHEME, NS_LINEAR); PushDefaultIntPair(Root, IDENT_NUMBERING_START, 1); PushDefaultBoolPair(Root, IDENT_NUMBERING_ZERO_PAD, FALSE); // // TODO(matt): Consider where the genre setting should apply, project vs entry level PushDefaultIntPair(Root, IDENT_GENRE, GENRE_VIDEO); PushDefaultIntPair(Root, IDENT_PRIVACY_CHECK_INTERVAL, DEFAULT_PRIVACY_CHECK_INTERVAL); PushDefaultIntPair(Root, IDENT_LOG_LEVEL, LOG_ERROR); } void AssignOrPushEnumValue(scope_tree *Parent, config_pair *Pair, bool IsSingleton, int Value) { Pair->int64_t = Value; Pair->Type = PT_INT64; config_pair *Assignment = 0; if((Assignment = GetAssignment(Parent, Pair, IsSingleton))) { Assignment->int64_t = Pair->int64_t; } else { PushPair(Parent, Pair); } } scope_tree * ScopeTokens(scope_tree *Tree, memory_book *TokensList, tokens *T, memory_book *TypeSpecs, config_include_rules *Rules) { scope_tree *Parent = Tree; for(T->CurrentIndex = 0; T->CurrentIndex < T->Token.ItemCount;) { string Filepath = Wrap0(T->File.Path); if(TokenIs(T, TOKEN_IDENTIFIER)) { config_type_field *Field = CurrentFieldIsInSpec(&Parent->TypeSpec, T); if(Field) { if(!Rules || FieldIsPermitted(Rules, Field)) { if(Field->IsSetLocally && Field->Singleton) { token *This = GetPlaceInBook(&T->Token, T->CurrentIndex); ConfigError(&Filepath, This->LineNumber, S_WARNING, "Field already set: ", &This->Content); } // NOTE(matt): If we get rid of FT_BARE, then the ++T->CurrentIndex that all cases perform could happen // right here config_pair Pair = {}; Pair.Key = Field->ID; Pair.Position.Filename = T->File.Path; switch(Field->Type) { case FT_BARE: { token *This = GetPlaceInBook(&T->Token, T->CurrentIndex); token Value = *This; Pair.Position.LineNumber = Value.LineNumber; Pair.Type = PT_STRING; ++T->CurrentIndex; if(!ExpectToken(T, TOKEN_SEMICOLON, 0)) { FreeScopeTree(Tree); return 0; } config_pair *Assignment = 0; if(!(Assignment = GetAssignment(Parent, &Pair, Field->Singleton))) { PushPair(Parent, &Pair); } } break; case FT_BOOLEAN: { ++T->CurrentIndex; if(!ExpectToken(T, TOKEN_ASSIGN, 0)) { FreeScopeTree(Tree); return 0; } token Value = {}; if(!ExpectToken(T, TOKEN_STRING, &Value)) { FreeScopeTree(Tree); return 0; } Pair.Position.LineNumber = Value.LineNumber; Pair.Type = PT_BOOL; if(!ExpectToken(T, TOKEN_SEMICOLON, 0)) { FreeScopeTree(Tree); return 0; } bool Bool = GetBoolFromString(&Filepath, &Value); if(Bool != -1) { Pair.bool = Bool; config_pair *Assignment = 0; if((Assignment = GetAssignment(Parent, &Pair, Field->Singleton))) { Assignment->bool = Pair.bool; } else { PushPair(Parent, &Pair); } } else { FreeScopeTree(Tree); return 0; } } break; case FT_STRING: { ++T->CurrentIndex; if(!ExpectToken(T, TOKEN_ASSIGN, 0)) { FreeScopeTree(Tree); return 0; } token Value = {}; if(!ExpectToken(T, TOKEN_STRING, &Value)) { FreeScopeTree(Tree); return 0; } Pair.Position.LineNumber = Value.LineNumber; if(!ExpectToken(T, TOKEN_SEMICOLON, 0)) { FreeScopeTree(Tree); return 0; } if(Field->ID == IDENT_NUMBERING_METHOD) { numbering_method NumberingMethod = GetNumberingMethodFromString(&Filepath, &Value); if(NumberingMethod != NM_COUNT) { AssignOrPushEnumValue(Parent, &Pair, Field->Singleton, NumberingMethod); } else { FreeScopeTree(Tree); return 0; } } else if(Field->ID == IDENT_NUMBERING_SCHEME) { numbering_scheme NumberingScheme = GetNumberingSchemeFromString(&Filepath, &Value); if(NumberingScheme != NS_COUNT) { AssignOrPushEnumValue(Parent, &Pair, Field->Singleton, NumberingScheme); } else { FreeScopeTree(Tree); return 0; } } else if(Field->ID == IDENT_LOG_LEVEL) { log_level LogLevel = GetLogLevelFromString(&Filepath, &Value); if(LogLevel != LOG_COUNT) { AssignOrPushEnumValue(Parent, &Pair, Field->Singleton, LogLevel); } else { FreeScopeTree(Tree); return 0; } } else if(Field->ID == IDENT_GENRE) { genre Genre = GetGenreFromString(&Filepath, &Value); if(Genre != GENRE_COUNT) { AssignOrPushEnumValue(Parent, &Pair, Field->Singleton, Genre); } else { FreeScopeTree(Tree); return 0; } } else if(Field->ID == IDENT_ICON_TYPE) { icon_type IconType = GetIconTypeFromString(&Filepath, &Value); if(IconType != IT_COUNT) { AssignOrPushEnumValue(Parent, &Pair, Field->Singleton, IconType); } else { FreeScopeTree(Tree); return 0; } } else if(Field->ID == IDENT_VOD_PLATFORM) { vod_platform VODPlatform = GetVODPlatformFromString(&Filepath, &Value); if(VODPlatform != VP_COUNT) { AssignOrPushEnumValue(Parent, &Pair, Field->Singleton, VODPlatform); } else { FreeScopeTree(Tree); return 0; } } else if(Field->ID == IDENT_ART_VARIANTS || Field->ID == IDENT_ICON_VARIANTS) { int64_t ArtVariants = ParseArtVariantsString(&Filepath, &Value); if(ArtVariants != -1) { Pair.int64_t = ArtVariants; Pair.Type = PT_INT64; config_pair *Assignment = 0; if((Assignment = GetAssignment(Parent, &Pair, Field->Singleton))) { Assignment->int64_t = Pair.int64_t; } else { PushPair(Parent, &Pair); } } else { FreeScopeTree(Tree); return 0; } } else { Pair.String = Value.Content; Pair.Type = PT_STRING; config_pair *Assignment = 0; if((Assignment = GetAssignment(Parent, &Pair, Field->Singleton))) { Assignment->String = Pair.String; Assignment->Position.LineNumber = Pair.Position.LineNumber; Assignment->Position.Filename = Pair.Position.Filename; } else { PushPair(Parent, &Pair); } } } break; case FT_NUMBER: { ++T->CurrentIndex; if(!ExpectToken(T, TOKEN_ASSIGN, 0)) { FreeScopeTree(Tree); return 0; } token Value = {}; bool IsNegative = FALSE; if(TokenIs(T, TOKEN_MINUS)) { IsNegative = TRUE; ++T->CurrentIndex; } if(!ExpectToken(T, TOKEN_NUMBER, &Value)) { FreeScopeTree(Tree); return 0; } Pair.int64_t = IsNegative ? 0 - Value.int64_t : Value.int64_t; Pair.Type = PT_INT64; if(!ExpectToken(T, TOKEN_SEMICOLON, 0)) { FreeScopeTree(Tree); return 0; } config_pair *Assignment = 0; if((Assignment = GetAssignment(Parent, &Pair, Field->Singleton))) { Assignment->int64_t = Pair.int64_t; } else { PushPair(Parent, &Pair); } } break; case FT_SCOPE: { ++T->CurrentIndex; if(!ExpectToken(T, TOKEN_ASSIGN, 0)) { FreeScopeTree(Tree); return 0; } token Value = {}; if(!ExpectToken(T, TOKEN_STRING, &Value)) { FreeScopeTree(Tree); return 0; } Pair.Type = PT_STRING; Pair.String = Value.Content; Pair.Position.LineNumber = Value.LineNumber; if(Field->ID == IDENT_INCLUDE) { int IncludePathTokenIndex = T->CurrentIndex - 1; config_include_rules Rules = {}; Rules.ID = InitBook(sizeof(config_identifier_id), 4); switch(ParseIncludeRules(TypeSpecs, &Parent->TypeSpec, T, &Rules)) { case RC_SUCCESS: { string IncluderFilePath = Wrap0(T->File.Path); char *IncludePath = ExpandPath(Pair.String, &IncluderFilePath); if(IncludePath) { string IncludePathL = Wrap0(IncludePath); tokens *I = 0; for(int i = 0; i < TokensList->ItemCount; ++i) { tokens *This = GetPlaceInBook(TokensList, i); if(!StringsDifferLv0(Value.Content, This->File.Path)) { I = This; I->CurrentIndex = 0; I->CurrentLine = 0; } } if(!I) { I = Tokenise(TokensList, IncludePathL); } if(I) { Parent = ScopeTokens(Parent, TokensList, I, TypeSpecs, &Rules); FreeBook(&Rules.ID); if(!Parent) { return 0; } } else { token *This = GetPlaceInBook(&T->Token, IncludePathTokenIndex); ConfigFileIncludeError(&Filepath, This->LineNumber, Wrap0(IncludePath)); } Free(IncludePath); } else { ConfigError(&IncluderFilePath, Pair.Position.LineNumber, S_WARNING, "Unable to include file: ", &Pair.String); } } break; case RC_SYNTAX_ERROR: { FreeScopeTree(Tree); return 0; } break; case RC_SCHEME_MIXTURE: case RC_INVALID_IDENTIFIER: default: break; } } else { // TODO(matt): Here is probably where we must fix deduplication of support scopes scope_tree *Search = 0; bool SelfContained = FALSE; if(Field->ID == IDENT_SUPPORT) { scope_tree *ParentP = Parent; Search = GetScope(ParentP, &Pair, Field->Singleton); if(Search) { SelfContained = TRUE; } while(!Search && ParentP->Parent) { ParentP = ParentP->Parent; Search = GetScope(ParentP, &Pair, Field->Singleton); } } else { Search = GetScope(Parent, &Pair, Field->Singleton); } if(Search) { if(Field->ID == IDENT_SUPPORT && !SelfContained) { Parent = CopyScope(TypeSpecs, Parent, Search); } else { Parent = Search; MergeScopes(TypeSpecs, Parent, Parent->Parent, TRUE); } } else { Parent = PushScope(TypeSpecs, Parent, &Pair); if(Parent->ID.Key == IDENT_MEDIUM) { config_pair Hidden = {}; Hidden.Key = IDENT_HIDDEN; Hidden.bool = FALSE; Hidden.Type = PT_BOOL; PushPair(Parent, &Hidden); } MergeScopes(TypeSpecs, Parent, Parent->Parent, TRUE); } ResetFields(&Parent->TypeSpec); if(TokenIs(T, TOKEN_SEMICOLON)) { Parent = Parent->Parent; } else if(TokenIs(T, TOKEN_OPEN_BRACE)) { } else { ConfigErrorExpectation(T, TOKEN_SEMICOLON, TOKEN_OPEN_BRACE); FreeScopeTree(Tree); return 0; } ++T->CurrentIndex; } } break; } // NOTE(matt): This may not be appropriate for the IDENT_INCLUDE... Field->IsSetLocally = TRUE; } else { token *This = GetPlaceInBook(&T->Token, T->CurrentIndex); ConfigError(&Filepath, This->LineNumber, S_WARNING, "Field not allowed: ", &This->Content); DepartAssignment(T); } } else { token *This = GetPlaceInBook(&T->Token, T->CurrentIndex); ConfigError(&Filepath, This->LineNumber, S_WARNING, "Invalid field: ", &This->Content); if(!DepartAssignment(T)) { FreeScopeTree(Tree); return 0; } } } else if(TokenIs(T, TOKEN_CLOSE_BRACE)) { if(!Parent->Parent) { token *This = GetPlaceInBook(&T->Token, T->CurrentIndex); ConfigError(&Filepath, This->LineNumber, S_ERROR, "Syntax error: Unpaired closing brace", 0); FreeScopeTree(Tree); return 0; } Parent = Parent->Parent; ++T->CurrentIndex; } else { ConfigErrorExpectation(T, TOKEN_IDENTIFIER, TOKEN_CLOSE_BRACE); FreeScopeTree(Tree); return 0; } } return Tree; } // TODO(matt): Fully nail down the title list stuff. Perhaps, make the title_list_end; identifier somehow indicate to child // scopes that they should not absorb any title_list stuff... typedef struct { token_position Position; config_identifier_id Variable; } resolution_error; typedef struct { _memory_book(resolution_error) Errors; _memory_book(resolution_error) Warnings; } resolution_errors; void PushError(resolution_errors *E, severity Severity, token_position *Position, config_identifier_id Variable) { if(Severity == S_WARNING) { resolution_error *This = MakeSpaceInBook(&E->Warnings); if(Position) { This->Position = *Position; } This->Variable = Variable; } else { resolution_error *This = MakeSpaceInBook(&E->Errors); if(Position) { This->Position = *Position; } This->Variable = Variable; } } resolution_error * GetError(resolution_errors *E, severity Severity, token_position *Position, config_identifier_id Variable) { resolution_error *Result = 0; if(Severity == S_WARNING) { for(int i = 0; i < E->Warnings.ItemCount; ++i) { resolution_error *This = GetPlaceInBook(&E->Warnings, i); if(Position->Filename == This->Position.Filename && Position->LineNumber == This->Position.LineNumber && Variable == This->Variable) { Result = This; break; } } } else { for(int i = 0; i < E->Errors.ItemCount; ++i) { resolution_error *This = GetPlaceInBook(&E->Errors, i); if(Position->Filename == This->Position.Filename && Position->LineNumber == This->Position.LineNumber && Variable == This->Variable) { Result = This; break; } } } return Result; } void FreeErrors(resolution_errors *E) { FreeAndReinitialiseBook(&E->Errors); FreeAndReinitialiseBook(&E->Warnings); } string ResolveEnvironmentVariable(memory_book *M, string Variable) { string Result = {}; int DollarCharacterBytes = 1; int NullTerminationBytes = 1; // NOTE(matt): Stack-string char Variable0[DollarCharacterBytes + Variable.Length + NullTerminationBytes]; char *Ptr = Variable0; if(!StringsMatch(Variable, Wrap0("~"))) { *Ptr++ = '$'; } for(int i = 0; i < Variable.Length; ++i) { *Ptr++ = Variable.Base[i]; } *Ptr = '\0'; int Flags = WRDE_NOCMD | WRDE_UNDEF | WRDE_APPEND; wordexp_t Expansions = {}; wordexp(Variable0, &Expansions, Flags); if(Expansions.we_wordc > 0) { Result = ExtendStringInBook(M, Wrap0(Expansions.we_wordv[0])); } wordfree(&Expansions); return Result; } role * GetRole(config *C, resolution_errors *E, config_pair *RoleID) { role *Result = 0; for(int i = 0; i < C->Role.ItemCount; ++i) { role *Role = GetPlaceInBook(&C->Role, i); if(!StringsDifferCaseInsensitive(Role->ID, RoleID->String)) { Result = Role; break; } } if(!Result && !GetError(E, S_WARNING, &RoleID->Position, IDENT_ROLE)) { string Filepath = Wrap0(RoleID->Position.Filename); ConfigError(&Filepath, RoleID->Position.LineNumber, S_WARNING, "Could not find role: ", &RoleID->String); PushError(E, S_WARNING, &RoleID->Position, IDENT_ROLE); WaitForInput(); } return Result; } role * GetRoleByID(config *C, string ID) { role *Result = 0; for(int i = 0; i < C->Role.ItemCount; ++i) { role *Role = GetPlaceInBook(&C->Role, i); if(!StringsDifferCaseInsensitive(Role->ID, ID)) { Result = Role; break; } } if(!Result) { ConfigError(0, 0, S_WARNING, "Could not find role: ", &ID); WaitForInput(); } return Result; } person * GetPerson(config *C, resolution_errors *E, config_pair *PersonID) { person *Result = 0; for(int i = 0; i < C->Person.ItemCount; ++i) { person *Person = GetPlaceInBook(&C->Person, i); if(!StringsDifferCaseInsensitive(Person->ID, PersonID->String)) { Result = Person; break; } } if(!Result && !GetError(E, S_WARNING, &PersonID->Position, IDENT_PERSON)) { string Filepath = Wrap0(PersonID->Position.Filename); ConfigError(&Filepath, PersonID->Position.LineNumber, S_WARNING, "Could not find person: ", &PersonID->String); PushError(E, S_WARNING, &PersonID->Position, IDENT_PERSON); } return Result; } typedef enum { LT_INTERNAL_ID, // e.g. hms/2019/devon LT_EXTERNAL_FULL, // e.g. Handmade Seattle / 2019 / Devon's Journey } lineage_type; string DeriveLineageOfProject(config *C, scope_tree *Project, lineage_type Type) { string Result = {}; string NullTitle = Wrap0("(null)"); memory_book StringList = InitBookOfPointers(4); if(Project) { string **Writer = MakeSpaceInBook(&StringList); switch(Type) { case LT_INTERNAL_ID: { *Writer = &Project->ID.String; } break; case LT_EXTERNAL_FULL: { config_pair *Title = GetPair(Project, IDENT_TITLE); if(Title) { *Writer = &Title->String; } else { *Writer = &NullTitle; } } break; } Project = Project->Parent; } string Divider = Wrap0(Type == LT_INTERNAL_ID ? "/" : " / "); while(Project && Project->ID.Key == IDENT_PROJECT) { string **Writer = MakeSpaceInBook(&StringList); *Writer = &Divider; Writer = MakeSpaceInBook(&StringList); switch(Type) { case LT_INTERNAL_ID: { *Writer = &Project->ID.String; } break; case LT_EXTERNAL_FULL: { config_pair *Title = GetPair(Project, IDENT_TITLE); if(Title) { *Writer = &Title->String; } else { *Writer = &NullTitle; } } break; } Project = Project->Parent; } for(int i = StringList.ItemCount - 1; i >= 0; --i) { string **Reader = GetPlaceInBook(&StringList, i); Result = ExtendStringInBook(&C->ResolvedVariables, **Reader);; } FreeBook(&StringList); return Result; } string EmptyString(void) { string Result = {}; return Result; } string DeriveLineageWithoutOriginOfProject(config *C, scope_tree *Project) { string Result = {}; memory_book StringList = InitBookOfPointers(4); if(Project->Parent && Project->Parent->ID.Key == IDENT_PROJECT) { string **Writer = MakeSpaceInBook(&StringList); *Writer = &Project->ID.String; Project = Project->Parent; } string Slash = Wrap0("/"); while(Project->Parent && Project->Parent->ID.Key == IDENT_PROJECT) { string **Writer = MakeSpaceInBook(&StringList); *Writer = &Slash; Writer = MakeSpaceInBook(&StringList); *Writer = &Project->ID.String; Project = Project->Parent; } if(StringList.ItemCount > 0) { for(int i = StringList.ItemCount - 1; i >= 0; --i) { string **Reader = GetPlaceInBook(&StringList, i); Result = ExtendStringInBook(&C->ResolvedVariables, **Reader); } FreeBook(&StringList); } else { Result = ExtendStringInBook(&C->ResolvedVariables, EmptyString()); } return Result; } string ResolveLocalVariable(config *C, resolution_errors *E, scope_tree *Scope, config_identifier_id Variable, token_position *Position) { string Result = {}; string Filepath = Wrap0(Position->Filename); switch(Variable) { case IDENT_LINEAGE: { if(Scope->ID.Key == IDENT_PROJECT) { Result = DeriveLineageOfProject(C, Scope, LT_INTERNAL_ID); } else { string This = Wrap0(ConfigIdentifiers[Variable].String); if(!GetError(E, S_WARNING, Position, Variable)) { ConfigError(&Filepath, Position->LineNumber, S_WARNING, "Variable could not be resolved: ", &This); PushError(E, S_WARNING, Position, Variable); } } } break; case IDENT_LINEAGE_WITHOUT_ORIGIN: { if(Scope->ID.Key == IDENT_PROJECT) { Result = DeriveLineageWithoutOriginOfProject(C, Scope); } else { string This = Wrap0(ConfigIdentifiers[Variable].String); if(!GetError(E, S_WARNING, Position, Variable)) { ConfigError(&Filepath, Position->LineNumber, S_WARNING, "Variable could not be resolved: ", &This); PushError(E, S_WARNING, Position, Variable); } } } break; case IDENT_ORIGIN: { if(Scope->ID.Key == IDENT_PROJECT) { while(Scope->Parent && Scope->Parent->ID.Key == IDENT_PROJECT) { Scope = Scope->Parent; } Result = ExtendStringInBook(&C->ResolvedVariables, Scope->ID.String); } else { string This = Wrap0(ConfigIdentifiers[Variable].String); if(!GetError(E, S_WARNING, Position, Variable)) { ConfigError(&Filepath, Position->LineNumber, S_WARNING, "Variable could not be resolved: ", &This); PushError(E, S_WARNING, Position, Variable); } } } break; case IDENT_OWNER: { bool Processed = FALSE; while(!Processed && Scope) { for(int i = 0; i < Scope->Pairs.ItemCount; ++i) { config_pair *Pair = GetPlaceInBook(&Scope->Pairs, i); if(Variable == Pair->Key) { if(GetPerson(C, E, Pair)) { Result = ExtendStringInBook(&C->ResolvedVariables, Pair->String); Processed = TRUE; break; } else { if(!GetError(E, S_WARNING, Position, Variable)) { ConfigError(&Filepath, Position->LineNumber, S_WARNING, "Owner set, but the person does not exist: ", &Pair->String); PushError(E, S_WARNING, Position, Variable); } Processed = TRUE; } } } Scope = Scope->Parent; } if(!Processed) { if(!GetError(E, S_WARNING, Position, Variable)) { ConfigError(&Filepath, Position->LineNumber, S_WARNING, "No owner set", 0); PushError(E, S_WARNING, Position, Variable); } } } break; case IDENT_PERSON: { // NOTE(matt): Allow scopes within a person scope, e.g. support, to resolve $person while(Scope && Scope->ID.Key != Variable) { Scope = Scope->Parent; } } // NOTE(matt): Intentional fall-through case IDENT_PROJECT: case IDENT_SUPPORT: { if(Scope && Scope->ID.Key == Variable) { Result = ExtendStringInBook(&C->ResolvedVariables, Scope->ID.String); } else { string This = Wrap0(ConfigIdentifiers[Variable].String); if(!GetError(E, S_WARNING, Position, Variable)) { ConfigError(&Filepath, Position->LineNumber, S_WARNING, "Variable could not be resolved: ", &This); PushError(E, S_WARNING, Position, Variable); } } } break; default: { if(!GetError(E, S_WARNING, Position, Variable)) { string This = Wrap0(ConfigIdentifiers[Variable].String); ConfigError(&Filepath, Position->LineNumber, S_WARNING, "Unhandled local variable: ", &This); PushError(E, S_WARNING, Position, Variable); } } break; }; return Result; } string ResolveString(config *C, resolution_errors *E, scope_tree *Parent, config_pair *S, bool AbsoluteFilePath) { //PrintFunctionName("ResolveString()"); string Result = {}; ResetPen(&C->ResolvedVariables); for(int i = 0; i < S->String.Length;) { switch(S->String.Base[i]) { case '$': { string Test = {}; ++i; if(i < S->String.Length && S->String.Base[i] == '{') { ++i; Test.Base = S->String.Base + i; while(i < S->String.Length && S->String.Base[i] != '}') { ++Test.Length; ++i; } } else { Test.Base = S->String.Base + i; while(i < S->String.Length && IsValidIdentifierCharacter(S->String.Base[i])) { ++Test.Length; ++i; } if(i > 0) { --i; } } config_identifier_id ID = GetConfigIdentifierFromString(Test); if(ID != IDENT_NULL) { Result = ResolveLocalVariable(C, E, Parent, ID, &S->Position); } else { Result = ResolveEnvironmentVariable(&C->ResolvedVariables, Test); } } break; case '~': { string Char = { .Base = S->String.Base + i, .Length = 1 }; if(AbsoluteFilePath && i == 0) { Result = ResolveEnvironmentVariable(&C->ResolvedVariables, Char); } else { Result = ExtendStringInBook(&C->ResolvedVariables, Char); } } break; case '\\': { ++i; }; // NOTE(matt): Intentional fall-through default: { string Char = { .Base = S->String.Base + i, .Length = 1 }; Result = ExtendStringInBook(&C->ResolvedVariables, Char); } break; } ++i; } return Result; } typedef enum { P_ABS, P_REL, } path_relativity; string StripSlashes(string Path, path_relativity Relativity) { string Result = Path; if(Relativity == P_REL) { while(Result.Length > 0 && *Result.Base == '/') { ++Result.Base; --Result.Length; } } while(Result.Length > 0 && Result.Base[Result.Length - 1] == '/') { --Result.Length; } return Result; } bool HasImageFileExtension(string Path) { return ExtensionMatches(Path, EXT_GIF) || ExtensionMatches(Path, EXT_JPEG) || ExtensionMatches(Path, EXT_JPG) || ExtensionMatches(Path, EXT_PNG); } typedef uint64_t variant; typedef struct { string Path; _memory_book(variant) Variants; } variant_string; void PushVariantUniquely(resolution_errors *E, memory_book *VariantStrings, string Path, variant Variants, config_identifier_id Identifier) { if(Path.Length > 0) { bool FoundPath = FALSE; for(int i = 0; i < VariantStrings->ItemCount; ++i) { variant_string *ThisVariantString = GetPlaceInBook(VariantStrings, i); if(StringsMatch(ThisVariantString->Path, Path)) { FoundPath = TRUE; bool FoundVariantsMatch = FALSE; for(int VariantsIndex = 0; VariantsIndex < ThisVariantString->Variants.ItemCount; ++VariantsIndex) { variant Test = *(variant *)GetPlaceInBook(&ThisVariantString->Variants, VariantsIndex); if(Test == Variants) { FoundVariantsMatch = TRUE; } } if(!FoundVariantsMatch) { variant *ThisVariant = MakeSpaceInBook(&ThisVariantString->Variants); *ThisVariant = Variants; PushError(E, S_ERROR, 0, Identifier); } } } if(!FoundPath) { variant_string *NewVariantString = MakeSpaceInBook(VariantStrings); NewVariantString->Path = Path; NewVariantString->Variants = InitBook(sizeof(variant), 4); variant *NewVariants = MakeSpaceInBook(&NewVariantString->Variants); *NewVariants = Variants; } } } typedef struct { string String0; string String1; _memory_book(project *) Projects; } config_string_association; typedef struct { config_identifier_id ID0; config_identifier_id ID1; _memory_book(config_string_association) Associations; } config_string_associations; typedef struct { _memory_book(variant_string) VariantStrings; config_string_associations HMMLDirs; config_string_associations BaseDirAndSearchLocation; config_string_associations BaseDirAndPlayerLocation; } config_verifiers; void PushMedium(config *C, resolution_errors *E, config_verifiers *V, project *P, scope_tree *MediumTree) { // TODO(matt): Do we need to warn about incomplete medium information? medium *Medium = MakeSpaceInBook(&P->Medium); Medium->ID = ResolveString(C, E, MediumTree, &MediumTree->ID, FALSE); for(int i = 0; i < MediumTree->Pairs.ItemCount; ++i) { config_pair *This = GetPlaceInBook(&MediumTree->Pairs, i); switch(This->Key) { case IDENT_ICON: { Medium->Icon = ResolveString(C, E, MediumTree, This, FALSE); } break; case IDENT_ICON_NORMAL: { Medium->IconNormal = ResolveString(C, E, MediumTree, This, FALSE); } break; case IDENT_ICON_FOCUSED: { Medium->IconFocused = ResolveString(C, E, MediumTree, This, FALSE); } break; case IDENT_ICON_DISABLED: { Medium->IconDisabled = ResolveString(C, E, MediumTree, This, FALSE); } break; case IDENT_NAME: { Medium->Name = ResolveString(C, E, MediumTree, This, FALSE); } break; default: break; } } for(int i = 0; i < MediumTree->Pairs.ItemCount; ++i) { config_pair *This = GetPlaceInBook(&MediumTree->Pairs, i); switch(This->Key) { // int64_t case IDENT_ICON_TYPE: { Medium->IconType = This->int64_t; } break; case IDENT_ICON_VARIANTS: { Medium->IconVariants = This->int64_t; } break; // bool case IDENT_HIDDEN: { Medium->Hidden = This->bool; } break; default: break; } } if(Medium->Icon.Length > 0) { if(Medium->IconType == IT_DEFAULT_UNSET) { if(HasImageFileExtension(Medium->Icon)) { Medium->IconType = IT_GRAPHICAL; } else { Medium->IconType = IT_TEXTUAL; } } switch(Medium->IconType) { case IT_GRAPHICAL: { PushVariantUniquely(E, &V->VariantStrings, Medium->Icon, Medium->IconVariants, IDENT_ICON); } break; case IT_TEXTUAL: { if(Medium->IconNormal.Length == 0) { Medium->IconNormal = Medium->Icon; } if(Medium->IconFocused.Length == 0) { Medium->IconFocused = Medium->Icon; } if(Medium->IconDisabled.Length == 0) { Medium->IconDisabled = Medium->Icon; } } break; default: break; } } } medium * GetMediumFromProject(project *P, string ID) { medium *Result = 0; for(int i = 0; i < P->Medium.ItemCount; ++i) { medium *This = GetPlaceInBook(&P->Medium, i); if(StringsMatch(ID, This->ID)) { Result = This; break; } } return Result; } void PushSupport(config *C, resolution_errors *E, config_verifiers *V, person *P, scope_tree *SupportTree) { support *Support = MakeSpaceInBook(&P->Support); Support->ID = ResolveString(C, E, SupportTree, &SupportTree->ID, FALSE); for(int i = 0; i < SupportTree->Pairs.ItemCount; ++i) { config_pair *This = GetPlaceInBook(&SupportTree->Pairs, i); switch(This->Key) { case IDENT_ICON: { Support->Icon = StripSlashes(ResolveString(C, E, SupportTree, This, FALSE), P_REL); } break; case IDENT_ICON_NORMAL: { Support->IconNormal = ResolveString(C, E, SupportTree, This, FALSE); } break; case IDENT_ICON_FOCUSED: { Support->IconFocused = ResolveString(C, E, SupportTree, This, FALSE); } break; case IDENT_URL: { Support->URL = ResolveString(C, E, SupportTree, This, FALSE); } break; default: break; } } for(int i = 0; i < SupportTree->Pairs.ItemCount; ++i) { config_pair *This = GetPlaceInBook(&SupportTree->Pairs, i); switch(This->Key) { case IDENT_ICON_TYPE: { Support->IconType = This->int64_t; } break; case IDENT_ICON_VARIANTS: { Support->IconVariants = This->int64_t; } break; default: break; } } if(Support->Icon.Length > 0) { if(Support->IconType == IT_DEFAULT_UNSET) { if(HasImageFileExtension(Support->Icon)) { Support->IconType = IT_GRAPHICAL; } else { Support->IconType = IT_TEXTUAL; } } switch(Support->IconType) { case IT_GRAPHICAL: { PushVariantUniquely(E, &V->VariantStrings, Support->Icon, Support->IconVariants, IDENT_ICON); } break; case IT_TEXTUAL: { if(Support->IconNormal.Length == 0) { Support->IconNormal = Support->Icon; } if(Support->IconFocused.Length == 0) { Support->IconFocused = Support->Icon; } } break; default: break; } } } void PushPersonOntoConfig(config *C, resolution_errors *E, config_verifiers *V, scope_tree *PersonTree) { //PrintFunctionName("PushPersonOntoConfig()"); //PrintScopeTree(PersonTree); person *This = MakeSpaceInBook(&C->Person); This->ID = ResolveString(C, E, PersonTree, &PersonTree->ID, FALSE); for(int i = 0; i < PersonTree->Pairs.ItemCount; ++i) { config_pair *Pair = GetPlaceInBook(&PersonTree->Pairs, i); if(IDENT_NAME == Pair->Key) { This->Name = ResolveString(C, E, PersonTree, Pair, FALSE); } else if(IDENT_HOMEPAGE == Pair->Key) { This->Homepage = ResolveString(C, E, PersonTree, Pair, FALSE); } else if(IDENT_QUOTE_USERNAME == Pair->Key) { This->QuoteUsername = ResolveString(C, E, PersonTree, Pair, FALSE); } } if(This->QuoteUsername.Length == 0) { This->QuoteUsername = This->ID; } This->Support = InitBook(sizeof(support), 2); for(int i = 0; i < PersonTree->Trees.ItemCount; ++i) { scope_tree *ThisSupport = GetPlaceInBook(&PersonTree->Trees, i); PushSupport(C, E, V, This, ThisSupport); } } void PushRoleOntoConfig(config *C, resolution_errors *E, config_verifiers *V, scope_tree *RoleTree) { role *This = MakeSpaceInBook(&C->Role); This->ID = ResolveString(C, E, RoleTree, &RoleTree->ID, FALSE); for(int i = 0; i < RoleTree->Pairs.ItemCount; ++i) { config_pair *Pair = GetPlaceInBook(&RoleTree->Pairs, i); switch(Pair->Key) { case IDENT_NAME: { This->Name = ResolveString(C, E, RoleTree, Pair, FALSE); } break; case IDENT_PLURAL: { This->Plural = ResolveString(C, E, RoleTree, Pair, FALSE); } break; case IDENT_NON_SPEAKING: { This->NonSpeaking = Pair->bool; } break; default: break; } } } void PushCredit(config *C, resolution_errors *E, project *P, scope_tree *CreditTree) { CreditTree->ID.String = ResolveString(C, E, CreditTree, &CreditTree->ID, FALSE); if(CreditTree->ID.String.Base) { person *Person = GetPerson(C, E, &CreditTree->ID); if(Person) { for(int i = 0; i < CreditTree->Pairs.ItemCount; ++i) { config_pair *This = GetPlaceInBook(&CreditTree->Pairs, i); role *Role = GetRole(C, E, This); if(Role) { credit *Credit = MakeSpaceInBook(&P->Credit); // TODO(matt): Sort the P->Credit book by Person->SortName Credit->Role = Role; Credit->Person = Person; } } } } } void PushProjectOntoProject(config *C, resolution_errors *E, config_verifiers *Verifiers, project *Parent, scope_tree *ProjectTree); void AddProjectToAssociation(config_string_association *A, project *P) { project **This = MakeSpaceInBook(&A->Projects); *This = P; } void PushAssociation(config_string_associations *HMMLDirs, string *S0, string *S1, project *P) { config_string_association *This = MakeSpaceInBook(&HMMLDirs->Associations); This->Projects = InitBookOfPointers(4); if(S0) { This->String0 = *S0; } if(S1) { This->String1 = *S1; } AddProjectToAssociation(This, P); } config_string_association * GetAssociation(config_string_associations *Set, string String0, string String1) { config_string_association *Result = 0; for(int i = 0; i < Set->Associations.ItemCount; ++i) { config_string_association *This = GetPlaceInBook(&Set->Associations, i); if(StringsMatch(String0, This->String0) && StringsMatch(String1, This->String1)) { Result = This; break; } } return Result; } config_string_association * GetGlobalPathAssociation(config_string_associations *Set, string Path) { config_string_association *Result = 0; for(int i = 0; i < Set->Associations.ItemCount; ++i) { config_string_association *This = GetPlaceInBook(&Set->Associations, i); if(StringsMatchSized(Path, This->String0.Length, This->String0)) { Path = TrimString(Path, This->String0.Length, 0); if(Path.Length == 0 && This->String1.Length == 0) { Result = This; break; } else if(StringsMatchSized(Path, 1, Wrap0("/"))) { Path = TrimString(Path, 1, 0); if(Path.Length > 0 && StringsMatchSized(Path, This->String1.Length, This->String1)) { Result = This; break; } } } } return Result; } void SetUniqueHMMLDir(config *C, resolution_errors *E, config_string_associations *HMMLDirs, project *P, scope_tree *ProjectTree, config_pair *HMMLDir) { string Result = StripSlashes(ResolveString(C, E, ProjectTree, HMMLDir, TRUE), P_ABS); bool Clashed = FALSE; for(int i = 0; i < HMMLDirs->Associations.ItemCount; ++i) { config_string_association *This = GetPlaceInBook(&HMMLDirs->Associations, i); if(StringsMatch(Result, This->String0)) { AddProjectToAssociation(This, P); PushError(E, S_ERROR, &HMMLDir->Position, IDENT_HMML_DIR); Clashed = TRUE; break; } } if(!Clashed) { PushAssociation(HMMLDirs, &Result, 0, P); } P->HMMLDir = Result; } void PushProject(config *C, resolution_errors *E, config_verifiers *V, project *P, scope_tree *ProjectTree) { P->Medium = InitBook(sizeof(medium), 8); P->Child = InitBook(sizeof(project), 8); P->Credit = InitBook(sizeof(credit), 4); config_string_associations *HMMLDirs = &V->HMMLDirs; P->ID = ResolveString(C, E, ProjectTree, &ProjectTree->ID, FALSE); if(P->ID.Length > MAX_PROJECT_ID_LENGTH) { string Filepath = Wrap0(ProjectTree->ID.Position.Filename); ConfigErrorSizing(&Filepath, ProjectTree->ID.Position.LineNumber, IDENT_PROJECT, &P->ID, MAX_PROJECT_ID_LENGTH); PushError(E, S_ERROR, 0, IDENT_PROJECT); } ResetPen(&C->ResolvedVariables); P->Lineage = DeriveLineageOfProject(C, ProjectTree, LT_INTERNAL_ID); // NOTE(matt): Initial pass over as-yet unresolvable local variable(s) // for(int i = 0; i < ProjectTree->Pairs.ItemCount; ++i) { config_pair *This = GetPlaceInBook(&ProjectTree->Pairs, i); switch(This->Key) { case IDENT_OWNER: { // TODO(matt): Do we need to fail completely if owner cannot be found? P->Owner = GetPerson(C, E, This); } break; default: break; } } // //// for(int i = 0; i < ProjectTree->Trees.ItemCount; ++i) { scope_tree *This = GetPlaceInBook(&ProjectTree->Trees, i); switch(This->ID.Key) { case IDENT_MEDIUM: { PushMedium(C, E, V, P, This); } break; case IDENT_CREDIT: { PushCredit(C, E, P, This); } break; default: break; } } for(int i = 0; i < ProjectTree->Pairs.ItemCount; ++i) { config_pair *This = GetPlaceInBook(&ProjectTree->Pairs, i); string Filepath = Wrap0(This->Position.Filename); switch(This->Key) { // NOTE(matt): String case IDENT_DEFAULT_MEDIUM: { P->DefaultMedium = GetMediumFromProject(P, This->String); } break; case IDENT_HMML_DIR: { SetUniqueHMMLDir(C, E, HMMLDirs, P, ProjectTree, This); } break; case IDENT_PLAYER_TEMPLATE: { P->PlayerTemplatePath = StripSlashes(ResolveString(C, E, ProjectTree, This, FALSE), P_REL); } break; case IDENT_THEME: { P->Theme = ResolveString(C, E, ProjectTree, This, FALSE); if(P->Theme.Length > MAX_THEME_LENGTH) { ConfigErrorSizing(&Filepath, This->Position.LineNumber, This->Key, &P->Theme, MAX_THEME_LENGTH); PushError(E, S_ERROR, 0, This->Key); } } break; case IDENT_TITLE: { P->Title = ResolveString(C, E, ProjectTree, This, FALSE); if(P->Title.Length > MAX_PROJECT_NAME_LENGTH) { ConfigErrorSizing(&Filepath, This->Position.LineNumber, This->Key, &P->Title, MAX_PROJECT_NAME_LENGTH); PushError(E, S_ERROR, 0, This->Key); } } break; case IDENT_HTML_TITLE: { P->HTMLTitle = ResolveString(C, E, ProjectTree, This, FALSE); } break; // TODO(matt): Sort out this title list stuff //case IDENT_TITLE_LIST_DELIMITER: { C->Project[C->ProjectCount].HMMLDir = ResolveString(This); } break; //case IDENT_TITLE_LIST_PREFIX: { C->Project[C->ProjectCount].HMMLDir = ResolveString(This); } break; //case IDENT_TITLE_LIST_SUFFIX: { C->Project[C->ProjectCount].HMMLDir = ResolveString(This); } break; //case IDENT_TITLE_SUFFIX: { C->Project[C->ProjectCount].HMMLDir = ResolveString(This); } break; case IDENT_NUMBERING_FILENAME_PREFIX: { P->Numbering.FilenameDerived.Prefix = ResolveString(C, E, ProjectTree, This, FALSE); } break; case IDENT_NUMBERING_UNIT: { P->Numbering.Unit = ResolveString(C, E, ProjectTree, This, FALSE); } break; case IDENT_BASE_DIR: { P->BaseDir = StripSlashes(ResolveString(C, E, ProjectTree, This, TRUE), P_ABS); if(P->BaseDir.Length > MAX_BASE_DIR_LENGTH) { ConfigErrorSizing(&Filepath, This->Position.LineNumber, This->Key, &P->BaseDir, MAX_BASE_DIR_LENGTH); PushError(E, S_ERROR, 0, This->Key); } } break; case IDENT_BASE_URL: { P->BaseURL = ResolveString(C, E, ProjectTree, This, FALSE); if(P->BaseURL.Length > MAX_BASE_URL_LENGTH) { ConfigErrorSizing(&Filepath, This->Position.LineNumber, This->Key, &P->BaseURL, MAX_BASE_URL_LENGTH); PushError(E, S_ERROR, 0, This->Key); } } break; case IDENT_PLAYER_LOCATION: { P->PlayerLocation = StripSlashes(ResolveString(C, E, ProjectTree, This, FALSE), P_REL); if(P->PlayerLocation.Length > MAX_RELATIVE_PAGE_LOCATION_LENGTH) { ConfigErrorSizing(&Filepath, This->Position.LineNumber, This->Key, &P->PlayerLocation, MAX_RELATIVE_PAGE_LOCATION_LENGTH); PushError(E, S_ERROR, 0, This->Key); } } break; case IDENT_SEARCH_LOCATION: { P->SearchLocation = StripSlashes(ResolveString(C, E, ProjectTree, This, FALSE), P_REL); if(P->SearchLocation.Length > MAX_RELATIVE_PAGE_LOCATION_LENGTH) { ConfigErrorSizing(&Filepath, This->Position.LineNumber, This->Key, &P->SearchLocation, MAX_RELATIVE_PAGE_LOCATION_LENGTH); PushError(E, S_ERROR, 0, This->Key); } } break; case IDENT_SEARCH_TEMPLATE: { P->SearchTemplatePath = StripSlashes(ResolveString(C, E, ProjectTree, This, FALSE), P_REL); } break; case IDENT_TEMPLATES_DIR: { P->TemplatesDir = StripSlashes(ResolveString(C, E, ProjectTree, This, TRUE), P_ABS); } break; case IDENT_ART: { P->Art = StripSlashes(ResolveString(C, E, ProjectTree, This, FALSE), P_REL); } break; case IDENT_ICON: { P->Icon = StripSlashes(ResolveString(C, E, ProjectTree, This, FALSE), P_REL); } break; case IDENT_ICON_NORMAL: { P->IconNormal = ResolveString(C, E, ProjectTree, This, FALSE); } break; case IDENT_ICON_FOCUSED: { P->IconFocused = ResolveString(C, E, ProjectTree, This, FALSE); } break; case IDENT_ICON_DISABLED: { P->IconDisabled = ResolveString(C, E, ProjectTree, This, FALSE); } break; // NOTE(matt): int64_t case IDENT_GENRE: { P->Genre = This->int64_t; } break; case IDENT_ART_VARIANTS: { P->ArtVariants = This->int64_t; } break; case IDENT_ICON_TYPE: { P->IconType = This->int64_t; } break; case IDENT_ICON_VARIANTS: { P->IconVariants = This->int64_t; } break; case IDENT_NUMBERING_METHOD: { P->Numbering.Method = This->int64_t; } break; case IDENT_NUMBERING_SCHEME: { P->Numbering.Scheme = This->int64_t; } break; case IDENT_NUMBERING_START: { P->Numbering.Auto.StartingNumber = This->int64_t; } break; case IDENT_VOD_PLATFORM: { P->VODPlatform = This->int64_t; } break; // NOTE(matt): bool case IDENT_DENY_BESPOKE_TEMPLATES: { P->DenyBespokeTemplates = This->bool; } break; case IDENT_IGNORE_PRIVACY: { P->IgnorePrivacy = This->bool; } break; case IDENT_NUMBERING_ZERO_PAD: { P->Numbering.Auto.ZeroPadded = This->bool; } break; case IDENT_SINGLE_BROWSER_TAB: { P->SingleBrowserTab = This->bool; } break; default: break; } } ResetPen(&C->ResolvedVariables); P->WrittenLineage = DeriveLineageOfProject(C, ProjectTree, LT_EXTERNAL_FULL); for(int i = 0; i < ProjectTree->Trees.ItemCount; ++i) { scope_tree *This = GetPlaceInBook(&ProjectTree->Trees, i); if(This->ID.Key == IDENT_PROJECT) { PushProjectOntoProject(C, E, V, P, This); } } config_string_association *BaseDirAndSearchLocation = GetAssociation(&V->BaseDirAndSearchLocation, P->BaseDir, P->SearchLocation); if(BaseDirAndSearchLocation) { PushError(E, S_ERROR, 0, IDENT_SEARCH_LOCATION); AddProjectToAssociation(BaseDirAndSearchLocation, P); } else { PushAssociation(&V->BaseDirAndSearchLocation, &P->BaseDir, &P->SearchLocation, P); } config_string_association *BaseDirAndPlayerLocation = GetAssociation(&V->BaseDirAndPlayerLocation, P->BaseDir, P->PlayerLocation); if(BaseDirAndPlayerLocation) { PushError(E, S_ERROR, 0, IDENT_PLAYER_LOCATION); AddProjectToAssociation(BaseDirAndPlayerLocation, P); } else { PushAssociation(&V->BaseDirAndPlayerLocation, &P->BaseDir, &P->PlayerLocation, P); } if(!P->DefaultMedium) { ResetPen(&C->ResolvedVariables); ConfigError(0, 0, S_WARNING, "Unset default_medium for project: ", &P->Lineage); PushError(E, S_WARNING, 0, IDENT_DEFAULT_MEDIUM); } if(P->Art.Length > 0) { PushVariantUniquely(E, &V->VariantStrings, P->Art, P->ArtVariants, IDENT_ART); } if(P->Icon.Length > 0) { if(P->IconType == IT_DEFAULT_UNSET) { if(HasImageFileExtension(P->Icon)) { P->IconType = IT_GRAPHICAL; } else { P->IconType = IT_TEXTUAL; } } switch(P->IconType) { case IT_GRAPHICAL: { PushVariantUniquely(E, &V->VariantStrings, P->Icon, P->IconVariants, IDENT_ICON); } break; case IT_TEXTUAL: { if(P->IconNormal.Length == 0) { P->IconNormal = P->Icon; } if(P->IconFocused.Length == 0) { P->IconFocused = P->Icon; } if(P->IconDisabled.Length == 0) { P->IconDisabled = P->Icon; } } break; default: break; } } C->RespectingPrivacy |= !P->IgnorePrivacy; } void PushProjectOntoProject(config *C, resolution_errors *E, config_verifiers *Verifiers, project *Parent, scope_tree *ProjectTree) { project *P = MakeSpaceInBook(&Parent->Child); PushProject(C, E, Verifiers, P, ProjectTree); P->Parent = Parent; } project * GetProjectFromProject(project *Parent, string ID) { project *Result = 0; if(Parent) { for(int i = 0; i < Parent->Child.ItemCount; ++i) { project *This = GetPlaceInBook(&Parent->Child, i); if(StringsMatch(ID, This->ID)) { Result = This; break; } } } return Result; } void PushProjectOntoConfig(config *C, resolution_errors *E, config_verifiers *Verifiers, scope_tree *ProjectTree) { project *P = MakeSpaceInBook(&C->Project); PushProject(C, E, Verifiers, P, ProjectTree); } project * GetProjectFromConfig(config *C, string ID) { project *Result = 0; if(C) { for(int i = 0; i < C->Project.ItemCount; ++i) { project *This = GetPlaceInBook(&C->Project, i); if(StringsMatch(ID, This->ID)) { Result = This; break; } } } return Result; } void PrintAssociationClashes(config_string_associations *A) { for(int i = 0; i < A->Associations.ItemCount; ++i) { config_string_association *Association = GetPlaceInBook(&A->Associations, i); if(Association->Projects.ItemCount > 1) { ConfigError(0, 0, S_ERROR, "Multiple projects share the same:", 0); config_pair Assoc0 = { .Key = A->ID0, .String = Association->String0, .Type = PT_STRING }; int IndentLevel = 2; PrintPair(&Assoc0, ": ", FALSE, IndentLevel, FALSE, FALSE); if(A->ID1 != IDENT_NULL) { config_pair Assoc1 = { .Key = A->ID1, .String = Association->String1, .Type = PT_STRING }; PrintPair(&Assoc1, ": ", FALSE, IndentLevel, TRUE, FALSE); } fprintf(stderr, "\n"); for(int j = 0; j < Association->Projects.ItemCount; ++j) { project **P = GetPlaceInBook(&Association->Projects, j); fprintf(stderr, " "); if(*P) { PrintStringC(CS_CYAN, (*P)->Lineage); } else { PrintStringC(CS_RED, Wrap0("[global scope: ")); // TODO(matt): Maybe generalise this, next time we're in this situation if(A->ID0 == IDENT_BASE_DIR && A->ID1 == IDENT_SEARCH_LOCATION) { PrintC(CS_YELLOW_BOLD, ConfigIdentifiers[IDENT_GLOBAL_SEARCH_DIR].String); } PrintStringC(CS_RED, Wrap0("]")); } fprintf(stderr, "\n"); } } } } void PrintVariants(uint64_t V, colour_code ColourCode, string Margin) { int TermCols = GetTerminalColumns(); int AvailableColumnsForValue = TermCols - Margin.Length; int RunningAvailableColumns = TermCols - Margin.Length; PrintString(Margin); bool NeedsSpacing = FALSE; if(V) { for(int VariantShifterIndex = 0; VariantShifterIndex < AVS_COUNT; ++VariantShifterIndex) { if(V & (1 << VariantShifterIndex)) { char *String = ArtVariantStrings[VariantShifterIndex]; int Length = StringLength(String); if(RunningAvailableColumns - Length >= 0) { fprintf(stderr, "%s", NeedsSpacing ? " " : ""); } else { fprintf(stderr, "\n"); PrintString(Margin); NeedsSpacing = FALSE; RunningAvailableColumns = AvailableColumnsForValue; } PrintC(ColourCode, String); RunningAvailableColumns -= Length + NeedsSpacing; NeedsSpacing = TRUE; } } } else { PrintC(CS_YELLOW, "[unset]"); } } void PrintVariantInconsistencies(memory_book *VariantStrings) { for(int i = 0; i < VariantStrings->ItemCount; ++i) { variant_string *ThisVariantsString = GetPlaceInBook(VariantStrings, i); if(ThisVariantsString->Variants.ItemCount > 1) { ConfigError(0, 0, S_ERROR, "Sprite has inconsistently set variants ", &ThisVariantsString->Path); for(int j = 0; j < ThisVariantsString->Variants.ItemCount; ++j) { string Margin = Wrap0(" "); variant V = *(variant *)GetPlaceInBook(&ThisVariantsString->Variants, j); PrintVariants(V, j % 2 ? CS_CYAN_BOLD : CS_YELLOW_BOLD, Margin); fprintf(stderr, "\n"); } } } } void FreeAssociations(config_string_associations *A) { for(int i = 0; i < A->Associations.ItemCount; ++i) { config_string_association *This = GetPlaceInBook(&A->Associations, i); FreeBook(&This->Projects); } FreeBook(&A->Associations); } void FreeVariants(memory_book *VariantStrings) { for(int i = 0; i < VariantStrings->ItemCount; ++i) { variant_string *This = GetPlaceInBook(VariantStrings, i); FreeBook(&This->Variants); } FreeBook(VariantStrings); } void FreeVerifiers(config_verifiers *Verifiers) { FreeAssociations(&Verifiers->BaseDirAndSearchLocation); FreeAssociations(&Verifiers->BaseDirAndPlayerLocation); FreeAssociations(&Verifiers->HMMLDirs); FreeVariants(&Verifiers->VariantStrings); } void PrintLogLevel(char *Title, log_level Level, char *Delimiter, uint64_t Indentation, bool AppendNewline) { Indent(Indentation); PrintC(CS_YELLOW_BOLD, ConfigIdentifiers[IDENT_LOG_LEVEL].String); fprintf(stderr, "%s", Delimiter); PrintC(CS_GREEN_BOLD, LogLevelStrings[Level]); if(AppendNewline) { fprintf(stderr, "\n"); } } typedef struct { char *UpperLeftCorner; char *UpperLeft; char *Horizontal; char *UpperRight; char *Vertical; char *LowerLeftCorner; char *LowerLeft; char *Margin; char *Delimiter; char *Separator; } typography; void PrintVerticals(typography *T, int Count) { for(int i = 0; i < Count; ++i) { fprintf(stderr, "%s", T->Vertical); } } void CarriageReturn(typography *T, int VerticalsRequired) { fprintf(stderr, "\n"); PrintVerticals(T, VerticalsRequired); } uint64_t TypesetIconType(typography *T, uint8_t Generation, icon_type Type, bool PrependNewline, bool AppendNewline) { uint64_t Result = 0; // TODO(matt): Implement me, please! //CarriageReturn(T, Generation); //fprintf(stderr, "%s", T.Margin); #if 0 PrintC(CS_YELLOW_BOLD, ConfigIdentifiers[IDENT_ICON_TYPE].String); fprintf(stderr, "%s", T.Delimiter); PrintC(CS_GREEN_BOLD, IconTypeStrings[Type]); fprintf(stderr, "\n"); #endif if(PrependNewline) { CarriageReturn(T, Generation); ++Result; } fprintf(stderr, "%s", T->Margin); config_identifier_id Key = IDENT_ICON_TYPE; PrintC(CS_YELLOW_BOLD, ConfigIdentifiers[Key].String); fprintf(stderr, "%s", T->Delimiter); if(Type == IT_DEFAULT_UNSET) { PrintC(CS_YELLOW, "[unset]"); } else { PrintStringC(CS_GREEN_BOLD, Wrap0(IconTypeStrings[Type])); } if(AppendNewline) { CarriageReturn(T, Generation); ++Result; } return Result; } void TypesetVariants(typography *T, uint8_t Generation, config_identifier_id Key, uint64_t Value, int AvailableColumns, uint8_t *RowsRequired, bool PrependNewline, bool AppendNewline) { // NOTE(matt): RowsRequired is a value passed only by PrintSupport(). if(PrependNewline) { CarriageReturn(T, Generation); if(RowsRequired) { --*RowsRequired; } } if(RowsRequired) { fprintf(stderr, "%s%s ", *RowsRequired == 1 ? T->LowerLeft : T->Vertical, T->Margin); } fprintf(stderr, "%s", T->Margin); int CharactersInKeyPlusDelimiter = StringLength(ConfigIdentifiers[Key].String) + StringLength(T->Delimiter); int AvailableColumnsForValue = AvailableColumns - CharactersInKeyPlusDelimiter; int RunningAvailableColumns = AvailableColumnsForValue; PrintC(CS_YELLOW_BOLD, ConfigIdentifiers[Key].String); fprintf(stderr, "%s", T->Delimiter); bool NeedsSpacing = FALSE; if(Value) { for(int VariantShifterIndex = 0; VariantShifterIndex < AVS_COUNT; ++VariantShifterIndex) { if(Value & (1 << VariantShifterIndex)) { char *String = ArtVariantStrings[VariantShifterIndex]; int Length = StringLength(String); if(RunningAvailableColumns - Length >= 0) { fprintf(stderr, "%s", NeedsSpacing ? " " : ""); } else { CarriageReturn(T, Generation); if(RowsRequired) { --*RowsRequired; fprintf(stderr, "%s%s ", *RowsRequired == 1 ? T->LowerLeft : T->Vertical, T->Margin); } fprintf(stderr, "%s", T->Margin); for(int i = 0; i < CharactersInKeyPlusDelimiter; ++i) { fprintf(stderr, "%s", T->Margin); } NeedsSpacing = FALSE; RunningAvailableColumns = AvailableColumnsForValue; } PrintC(CS_GREEN_BOLD, String); RunningAvailableColumns -= Length + NeedsSpacing; NeedsSpacing = TRUE; } } } else { PrintC(CS_YELLOW, "[unset]"); } if(AppendNewline) { CarriageReturn(T, Generation); } } void PrintMedium(typography *T, medium *M, char *Delimiter, uint64_t Indentation, bool AppendNewline) { PrintStringC(CS_BLUE_BOLD, M->ID); if(M->Hidden) { PrintC(CS_BLACK_BOLD, " [hidden]"); } fprintf(stderr, "%s", Delimiter); PrintStringC(CS_MAGENTA, M->Name); fprintf(stderr, " ("); if(M->Icon.Length > 0) { PrintStringI(M->Icon, Indentation); } if(M->IconNormal.Length > 0) { PrintStringI(M->IconNormal, Indentation); } if(M->IconFocused.Length > 0) { PrintStringI(M->IconFocused, Indentation); } TypesetIconType(T, Indentation, M->IconType, FALSE, AppendNewline); TypesetVariants(T, Indentation, IDENT_ICON_VARIANTS, M->IconVariants, 0, 0, FALSE, AppendNewline); fprintf(stderr, ")"); if(AppendNewline) { fprintf(stderr, "\n"); } } void PrintSupport(support *S, typography *Typography, int IndentationLevel, uint8_t *RowsRequired) { bool ShouldFillSyntax = FALSE; config_pair ID = { .Key = IDENT_SUPPORT, .String = S->ID, .Type = PT_STRING }; fprintf(stderr, "%s%s", *RowsRequired == 1 ? Typography->LowerLeft : Typography->Vertical, Typography->Margin); PrintPair(&ID, Typography->Delimiter, ShouldFillSyntax, IndentationLevel, FALSE, TRUE); --*RowsRequired; if(S->URL.Length > 0) { config_pair URL = { .Key = IDENT_URL, .String = S->URL, .Type = PT_STRING }; fprintf(stderr, "%s%s ", *RowsRequired == 1 ? Typography->LowerLeft : Typography->Vertical, Typography->Margin); PrintPair(&URL, Typography->Delimiter, ShouldFillSyntax, IndentationLevel, FALSE, TRUE); --*RowsRequired; } if(S->Icon.Length > 0) { config_pair Icon = { .Key = IDENT_ICON, .String = S->Icon, .Type = PT_STRING }; fprintf(stderr, "%s%s ", *RowsRequired == 1 ? Typography->LowerLeft : Typography->Vertical, Typography->Margin); PrintPair(&Icon, Typography->Delimiter, ShouldFillSyntax, IndentationLevel, FALSE, TRUE); --*RowsRequired; } if(S->IconNormal.Length > 0) { config_pair IconNormal = { .Key = IDENT_ICON_NORMAL, .String = S->IconNormal, .Type = PT_STRING }; fprintf(stderr, "%s%s ", *RowsRequired == 1 ? Typography->LowerLeft : Typography->Vertical, Typography->Margin); PrintPair(&IconNormal, Typography->Delimiter, ShouldFillSyntax, IndentationLevel, FALSE, TRUE); --*RowsRequired; } if(S->IconFocused.Length > 0) { config_pair IconFocused = { .Key = IDENT_ICON_FOCUSED, .String = S->IconFocused, .Type = PT_STRING }; fprintf(stderr, "%s%s ", *RowsRequired == 1 ? Typography->LowerLeft : Typography->Vertical, Typography->Margin); PrintPair(&IconFocused, Typography->Delimiter, ShouldFillSyntax, IndentationLevel, FALSE, TRUE); --*RowsRequired; } #if 1 // TODO(matt): FIX ME fprintf(stderr, "%s%s ", *RowsRequired == 1 ? Typography->LowerLeft : Typography->Vertical, Typography->Margin); *RowsRequired -= TypesetIconType(Typography, IndentationLevel, S->IconType, FALSE, TRUE); uint8_t StartingColumn = 4; uint64_t AvailableColumns = GetTerminalColumns() - StartingColumn; TypesetVariants(Typography, IndentationLevel, IDENT_ICON_VARIANTS, S->IconVariants, AvailableColumns, RowsRequired, FALSE, TRUE); #endif } uint8_t GetRowsRequiredForVariants(typography *T, config_identifier_id Key, uint64_t Value, int AvailableColumns) { int CharactersInKeyPlusDelimiter = StringLength(ConfigIdentifiers[Key].String) + StringLength(T->Delimiter); int AvailableColumnsForValue = AvailableColumns - CharactersInKeyPlusDelimiter; int RunningAvailableColumns = AvailableColumnsForValue; uint8_t Result = 1; bool NeedsSpacing = FALSE; for(int VariantShifterIndex = 0; VariantShifterIndex < AVS_COUNT; ++VariantShifterIndex) { if(Value & (1 << VariantShifterIndex)) { char *String = ArtVariantStrings[VariantShifterIndex]; int Length = StringLength(String); if(!(RunningAvailableColumns - Length >= 0)) { ++Result; NeedsSpacing = FALSE; RunningAvailableColumns = AvailableColumnsForValue; } RunningAvailableColumns -= Length + NeedsSpacing; NeedsSpacing = TRUE; } } return Result; } uint8_t GetRowsRequiredForPersonInfo(typography *T, person *P) { uint8_t RowsRequired = 0; if(P->Name.Length > 0) { ++RowsRequired; } if(P->Homepage.Length > 0) { ++RowsRequired; } for(int i = 0; i < P->Support.ItemCount; ++i) { ++RowsRequired; support *This = GetPlaceInBook(&P->Support, i); if(This->URL.Length > 0) { ++RowsRequired; } if(This->Icon.Length > 0) { ++RowsRequired; } if(This->IconNormal.Length > 0) { ++RowsRequired; } if(This->IconFocused.Length > 0) { ++RowsRequired; } ++RowsRequired; // NOTE(matt): For TypesetIconType() int StartingColumn = 4; RowsRequired += GetRowsRequiredForVariants(T, IDENT_ICON_VARIANTS, This->IconVariants, GetTerminalColumns() - StartingColumn); } return RowsRequired; } void PrintPerson(person *P, typography *Typography) { bool HaveInfo = P->Name.Length > 0 || P->Homepage.Length > 0 || P->Support.ItemCount > 0; fprintf(stderr, "\n" "%s%s%s%s%s ", HaveInfo ? Typography->UpperLeftCorner : Typography->UpperLeft, Typography->Horizontal, Typography->Horizontal, Typography->Horizontal, Typography->UpperRight); PrintStringC(CS_GREEN_BOLD, P->ID); fprintf(stderr, "\n"); uint8_t RowsRequired = GetRowsRequiredForPersonInfo(Typography, P); int IndentationLevel = 0; bool ShouldFillSyntax = FALSE; if(P->Name.Length > 0) { fprintf(stderr, "%s ", RowsRequired == 1 ? Typography->LowerLeft : Typography->Vertical); config_pair Name = { .Key = IDENT_NAME, .String = P->Name, .Type = PT_STRING }; PrintPair(&Name, Typography->Delimiter, ShouldFillSyntax, IndentationLevel, FALSE, TRUE); --RowsRequired; } if(P->Homepage.Length > 0) { fprintf(stderr, "%s ", RowsRequired == 1 ? Typography->LowerLeft : Typography->Vertical); config_pair Homepage = { .Key = IDENT_HOMEPAGE, .String = P->Homepage, .Type = PT_STRING }; PrintPair(&Homepage, Typography->Delimiter, ShouldFillSyntax, IndentationLevel, FALSE, TRUE); --RowsRequired; } for(int i = 0; i < P->Support.ItemCount; ++i) { PrintSupport(GetPlaceInBook(&P->Support, i), Typography, IndentationLevel, &RowsRequired); } } uint8_t GetRowsRequiredForRoleInfo(role *R) { uint8_t RowsRequired = 0; if(R->Name.Length > 0) { ++RowsRequired; } if(R->Plural.Length > 0) { ++RowsRequired; } int NonSpeakingLines = 1; RowsRequired += NonSpeakingLines; return RowsRequired; } void PrintRole(role *R, typography *Typography) { bool HaveInfo = R->Name.Length > 0 || R->Plural.Length > 0; fprintf(stderr, "\n" "%s%s%s%s%s ", HaveInfo ? Typography->UpperLeftCorner : Typography->UpperLeft, Typography->Horizontal, Typography->Horizontal, Typography->Horizontal, Typography->UpperRight); PrintStringC(CS_GREEN_BOLD, R->ID); fprintf(stderr, "\n"); uint8_t RowsRequired = GetRowsRequiredForRoleInfo(R); int IndentationLevel = 0; bool ShouldFillSyntax = FALSE; if(R->Name.Length > 0) { fprintf(stderr, "%s ", RowsRequired == 1 ? Typography->LowerLeft : Typography->Vertical); config_pair Name = { .Key = IDENT_NAME, .String = R->Name, .Type = PT_STRING }; PrintPair(&Name, Typography->Delimiter, ShouldFillSyntax, IndentationLevel, FALSE, TRUE); --RowsRequired; } if(R->Plural.Length > 0) { fprintf(stderr, "%s ", RowsRequired == 1 ? Typography->LowerLeft : Typography->Vertical); config_pair Plural = { .Key = IDENT_PLURAL, .String = R->Plural, .Type = PT_STRING }; PrintPair(&Plural, Typography->Delimiter, ShouldFillSyntax, IndentationLevel, FALSE, TRUE); --RowsRequired; } fprintf(stderr, "%s ", RowsRequired == 1 ? Typography->LowerLeft : Typography->Vertical); config_pair NonSpeaking = { .Key = IDENT_NON_SPEAKING, .bool = R->NonSpeaking, .Type = PT_BOOL }; PrintPair(&NonSpeaking, Typography->Delimiter, ShouldFillSyntax, IndentationLevel, FALSE, TRUE); --RowsRequired; } void PrintLineage(string Lineage, bool AppendNewline) { string Parent = StripComponentFromPath(Lineage); PrintStringC(CS_BLACK_BOLD, Parent); if(Parent.Length > 0) { PrintC(CS_BLACK_BOLD, "/"); } string Us = GetFinalComponent(Lineage); PrintStringC(CS_BLUE_BOLD, Us); if(AppendNewline) { fprintf(stderr, "\n"); } } uint8_t GetColumnsRequiredForMedium(medium *M, typography *T) { uint8_t Result = M->ID.Length + StringLength(T->Delimiter) + M->Name.Length + sizeof(" (")-1 + M->Icon.Length + sizeof(")")-1; if(M->Hidden) { Result += sizeof(" [hidden]")-1; } return Result; } void PrintTitle(char *Text, typography *T, uint8_t Generation, int AvailableColumns, int64_t SectionItemCount /* NOTE(matt): Use any non-0 when the section is known to have content */) { char *LeftmostChar = "╾"; char *InnerChar = "─"; char *RightmostChar = "╼"; int LeftmostCharLength = 1; int SpacesRequired = 2; int RightmostCharLength = 1; int InnerCharsRequired = AvailableColumns - LeftmostCharLength - StringLength(Text) - RightmostCharLength - SpacesRequired; int LeftCharsRequired = 4; int RightCharsRequired = InnerCharsRequired - LeftCharsRequired; fprintf(stderr, "%s", LeftmostChar); for(int i = 0; i < LeftCharsRequired; ++i) { fprintf(stderr, "%s", InnerChar); } fprintf(stderr, " %s ", Text); for(int i = 0; i < RightCharsRequired; ++i) { fprintf(stderr, "%s", InnerChar); } fprintf(stderr, "%s", RightmostChar); if(SectionItemCount == 0) { //fprintf(stderr, "\n"); CarriageReturn(T, Generation); } } void PrintMedia(project *P, typography *T, uint8_t Generation, int AvailableColumns) { int IndentationLevel = 0; CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); PrintTitle("Media", T, Generation, AvailableColumns, P->Medium.ItemCount); CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); //AvailableColumns -= Generation + 1; int RunningAvailableColumns = AvailableColumns; medium *This = GetPlaceInBook(&P->Medium, 0); int ColumnsRequired = GetColumnsRequiredForMedium(This, T); if(ColumnsRequired <= RunningAvailableColumns) { PrintMedium(T, This, T->Delimiter, IndentationLevel, FALSE); } RunningAvailableColumns -= ColumnsRequired; int StringLengthOfSpacePlusSeparator = 2; for(int i = 1; i < P->Medium.ItemCount; ++i) { This = GetPlaceInBook(&P->Medium, i); ColumnsRequired = GetColumnsRequiredForMedium(This, T); if(RunningAvailableColumns >= StringLengthOfSpacePlusSeparator) { fprintf(stderr, " %s", T->Separator); RunningAvailableColumns -= StringLengthOfSpacePlusSeparator; if(ColumnsRequired + 1 <= RunningAvailableColumns) { fprintf(stderr, " "); --RunningAvailableColumns; PrintMedium(T, This, T->Delimiter, IndentationLevel, FALSE); } else { CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); RunningAvailableColumns = AvailableColumns; PrintMedium(T, This, T->Delimiter, IndentationLevel, FALSE); } } else { CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); RunningAvailableColumns = AvailableColumns; fprintf(stderr, "%s ", T->Separator); RunningAvailableColumns -= StringLengthOfSpacePlusSeparator; PrintMedium(T, This, T->Delimiter, IndentationLevel, FALSE); } RunningAvailableColumns -= ColumnsRequired; } } void PrintCredits(config *C, project *P, typography *T, uint8_t Generation, int IndentationLevel, int AvailableColumns) { for(int i = 0; i < C->Role.ItemCount; ++i) { role *Role = GetPlaceInBook(&C->Role, i); int CreditCount = 0; for(int j = 0; j < P->Credit.ItemCount; ++j) { credit *Credit = GetPlaceInBook(&P->Credit, j); if(Role == Credit->Role) { ++CreditCount; } } if(CreditCount > 0) { CarriageReturn(T, Generation); int Alignment = 0; Alignment += fprintf(stderr, "%s", T->Margin); if(CreditCount == 1) { Alignment += PrintStringC(CS_YELLOW_BOLD, Role->Name.Length ? Role->Name : Role->ID); } else { if(Role->Plural.Length > 0) { Alignment += PrintStringC(CS_YELLOW_BOLD, Role->Plural); } else { Alignment += PrintStringC(CS_YELLOW_BOLD, Role->Name.Length ? Role->Name : Role->ID); Alignment += PrintStringC(CS_YELLOW_BOLD, Wrap0("s")); } } Alignment += fprintf(stderr, "%s", T->Delimiter); bool Printed = FALSE; for(int j = 0; j < P->Credit.ItemCount; ++j) { credit *Credit = GetPlaceInBook(&P->Credit, j); if(Role == Credit->Role) { if(Printed) { CarriageReturn(T, Generation); AlignText(Alignment); } PrintStringC(CS_GREEN_BOLD, Credit->Person->Name.Length ? Credit->Person->Name : Credit->Person->ID); Printed = TRUE; } } } } } void TypesetPair(typography *T, uint8_t Generation, config_identifier_id Key, string Value, int AvailableColumns) { CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); int CharactersInKeyPlusDelimiter = StringLength(ConfigIdentifiers[Key].String) + StringLength(T->Delimiter); int AvailableColumnsForValue = AvailableColumns - CharactersInKeyPlusDelimiter; PrintC(CS_YELLOW_BOLD, ConfigIdentifiers[Key].String); fprintf(stderr, "%s", T->Delimiter); int CharactersToWrite = Value.Length; if(Value.Length > 0) { string Pen = { .Base = Value.Base + Value.Length - CharactersToWrite, .Length = MIN(Value.Length, AvailableColumnsForValue) }; PrintStringC(CS_GREEN_BOLD, Pen); CharactersToWrite -= Pen.Length; while(CharactersToWrite > 0) { CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); for(int i = 0; i < CharactersInKeyPlusDelimiter; ++i) { fprintf(stderr, "%s", T->Margin); } string Pen = { .Base = Value.Base + Value.Length - CharactersToWrite, .Length = MIN(CharactersToWrite, AvailableColumnsForValue) }; PrintStringC(CS_GREEN_BOLD, Pen); CharactersToWrite -= Pen.Length; } } else { PrintC(CS_YELLOW, "[unset]"); } } void TypesetNumber(typography *T, uint8_t Generation, config_identifier_id Key, int64_t Value) { CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); config_pair Pair = { .Key = Key, .int64_t = Value, .Type = PT_INT64 }; bool ShouldFillSyntax = FALSE; PrintPair(&Pair, T->Delimiter, ShouldFillSyntax, 0, FALSE, FALSE); } void TypesetBool(typography *T, uint8_t Generation, config_identifier_id Key, bool Value) { CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); config_pair Pair = { .Key = Key, .bool = Value, .Type = PT_BOOL }; bool ShouldFillSyntax = FALSE; PrintPair(&Pair, T->Delimiter, ShouldFillSyntax, 0, FALSE, FALSE); } void TypesetGenre(typography *T, uint8_t Generation, genre G) { CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); PrintC(CS_YELLOW_BOLD, ConfigIdentifiers[IDENT_GENRE].String); fprintf(stderr, "%s", T->Delimiter); PrintC(CS_GREEN_BOLD, GenreStrings[G]); } void TypesetVODPlatform(typography *T, uint8_t Generation, vod_platform P) { CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); PrintC(CS_YELLOW_BOLD, ConfigIdentifiers[IDENT_VOD_PLATFORM].String); fprintf(stderr, "%s", T->Delimiter); if(IsValidVODPlatform(P)) { PrintC(CS_GREEN_BOLD, VODPlatformStrings[P]); } else { PrintC(CS_YELLOW, "[unset]"); } } void TypesetNumberingMethod(typography *T, uint8_t Generation, numbering_method N) { CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); PrintC(CS_YELLOW_BOLD, ConfigIdentifiers[IDENT_NUMBERING_METHOD].String); fprintf(stderr, "%s", T-> Delimiter); PrintC(CS_GREEN_BOLD, NumberingMethodStrings[N]); } void TypesetNumberingScheme(typography *T, uint8_t Generation, numbering_scheme N) { CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); PrintC(CS_YELLOW_BOLD, ConfigIdentifiers[IDENT_NUMBERING_SCHEME].String); fprintf(stderr, "%s", T-> Delimiter); PrintC(CS_GREEN_BOLD, NumberingSchemeStrings[N]); } void PrintProject(config *C, project *P, typography *T, int Ancestors, int IndentationLevel, int TerminalColumns) { int Generation = Ancestors + 1; CarriageReturn(T, Ancestors); fprintf(stderr, "%s%s%s%s%s ", T->UpperLeftCorner, T->Horizontal, T->Horizontal, T->Horizontal, T->UpperRight); PrintLineage(P->Lineage, FALSE); int CharactersInMargin = 1; int AvailableColumns = TerminalColumns - (Generation + CharactersInMargin); if(P->Medium.ItemCount > 0) { PrintMedia(P, T, Generation, AvailableColumns); } CarriageReturn(T, Generation); CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); PrintTitle("Settings", T, Generation, AvailableColumns, -1); TypesetPair(T, Generation, IDENT_TITLE, P->Title, AvailableColumns); TypesetPair(T, Generation, IDENT_HTML_TITLE, P->HTMLTitle, AvailableColumns); TypesetPair(T, Generation, IDENT_DEFAULT_MEDIUM, P->DefaultMedium ? P->DefaultMedium->ID : EmptyString(), AvailableColumns); TypesetPair(T, Generation, IDENT_HMML_DIR, P->HMMLDir, AvailableColumns); TypesetPair(T, Generation, IDENT_TEMPLATES_DIR, P->TemplatesDir, AvailableColumns); TypesetPair(T, Generation, IDENT_SEARCH_TEMPLATE, P->SearchTemplatePath, AvailableColumns); TypesetPair(T, Generation, IDENT_PLAYER_TEMPLATE, P->PlayerTemplatePath, AvailableColumns); CarriageReturn(T, Generation); TypesetPair(T, Generation, IDENT_BASE_DIR, P->BaseDir, AvailableColumns); TypesetPair(T, Generation, IDENT_BASE_URL, P->BaseURL, AvailableColumns); TypesetPair(T, Generation, IDENT_SEARCH_LOCATION, P->SearchLocation, AvailableColumns); TypesetPair(T, Generation, IDENT_PLAYER_LOCATION, P->PlayerLocation, AvailableColumns); TypesetPair(T, Generation, IDENT_THEME, P->Theme, AvailableColumns); CarriageReturn(T, Generation); TypesetPair(T, Generation, IDENT_ART, P->Art, AvailableColumns); TypesetVariants(T, Generation, IDENT_ART_VARIANTS, P->ArtVariants, AvailableColumns, 0, TRUE, FALSE); TypesetPair(T, Generation, IDENT_ICON, P->Icon, AvailableColumns); TypesetPair(T, Generation, IDENT_ICON_NORMAL, P->IconNormal, AvailableColumns); TypesetPair(T, Generation, IDENT_ICON_FOCUSED, P->IconFocused, AvailableColumns); TypesetPair(T, Generation, IDENT_ICON_DISABLED, P->IconDisabled, AvailableColumns); TypesetIconType(T, Generation, P->IconType, TRUE, FALSE); TypesetVariants(T, Generation, IDENT_ICON_VARIANTS, P->IconVariants, AvailableColumns, 0, TRUE, FALSE); CarriageReturn(T, Generation); TypesetNumberingMethod(T, Generation, P->Numbering.Method); TypesetNumberingScheme(T, Generation, P->Numbering.Scheme); TypesetPair(T, Generation, IDENT_NUMBERING_UNIT, P->Numbering.Unit, AvailableColumns); TypesetPair(T, Generation, IDENT_NUMBERING_FILENAME_PREFIX, P->Numbering.FilenameDerived.Prefix, AvailableColumns); TypesetNumber(T, Generation, IDENT_NUMBERING_START, P->Numbering.Auto.StartingNumber); TypesetBool(T, Generation, IDENT_NUMBERING_ZERO_PAD, P->Numbering.Auto.ZeroPadded); CarriageReturn(T, Generation); TypesetGenre(T, Generation, P->Genre); TypesetVODPlatform(T, Generation, P->VODPlatform); TypesetBool(T, Generation, IDENT_IGNORE_PRIVACY, P->IgnorePrivacy); TypesetBool(T, Generation, IDENT_SINGLE_BROWSER_TAB, P->SingleBrowserTab); TypesetPair(T, Generation, IDENT_OWNER, P->Owner ? P->Owner->ID : Wrap0(""), AvailableColumns); CarriageReturn(T, Generation); CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); PrintTitle("Credits", T, Generation, AvailableColumns, P->Credit.ItemCount); if(P->Credit.ItemCount > 0) { PrintCredits(C, P, T, Generation, IndentationLevel, AvailableColumns); } else { fprintf(stderr, "%s", T->Margin); PrintC(CS_YELLOW, "[none]"); } if(P->Child.ItemCount) { CarriageReturn(T, Generation); CarriageReturn(T, Generation); fprintf(stderr, "%s", T->Margin); PrintTitle("Children", T, Generation, AvailableColumns, -1); ++Ancestors; for(int i = 0; i < P->Child.ItemCount; ++i) { PrintProject(C, GetPlaceInBook(&P->Child, i), T, Ancestors, IndentationLevel, TerminalColumns); } --Ancestors; } CarriageReturn(T, Ancestors); fprintf(stderr, "%s%s%s%s%s ", T->LowerLeftCorner, T->Horizontal, T->Horizontal, T->Horizontal, T->UpperRight); PrintLineage(P->Lineage, FALSE); } void PrintConfig(config *C, bool ShouldClearTerminal) { if(C) { int TermCols = GetTerminalColumns(); if(ShouldClearTerminal) { ClearTerminal(); } typography Typography = { .UpperLeftCorner = "┌", .UpperLeft = "╾", .Horizontal = "─", .UpperRight = "╼", .Vertical = "│", .LowerLeftCorner = "└", .LowerLeft = "╽", .Margin = " ", .Delimiter = ": ", .Separator = "•", }; // separate fprintf(stderr, "\n"); PrintTitle("Global Settings", &Typography, 0, TermCols, -1); int IndentationLevel = 0; int AvailableColumns = TermCols - StringLength(Typography.Margin); TypesetPair(&Typography, 0, IDENT_DB_LOCATION, C->DatabaseLocation, AvailableColumns); TypesetPair(&Typography, 0, IDENT_CACHE_DIR, C->CacheDir, AvailableColumns); fprintf(stderr, "\n"); TypesetPair(&Typography, 0, IDENT_GLOBAL_TEMPLATES_DIR, C->GlobalTemplatesDir, AvailableColumns); TypesetPair(&Typography, 0, IDENT_GLOBAL_SEARCH_TEMPLATE, C->GlobalSearchTemplatePath, AvailableColumns); TypesetPair(&Typography, 0, IDENT_GLOBAL_SEARCH_DIR, C->GlobalSearchDir, AvailableColumns); TypesetPair(&Typography, 0, IDENT_GLOBAL_SEARCH_URL, C->GlobalSearchURL, AvailableColumns); TypesetPair(&Typography, 0, IDENT_GLOBAL_THEME, C->GlobalTheme, AvailableColumns); fprintf(stderr, "\n"); TypesetPair(&Typography, 0, IDENT_ASSETS_ROOT_DIR, C->AssetsRootDir, AvailableColumns); TypesetPair(&Typography, 0, IDENT_ASSETS_ROOT_URL, C->AssetsRootURL, AvailableColumns); TypesetPair(&Typography, 0, IDENT_CSS_PATH, C->CSSDir, AvailableColumns); TypesetPair(&Typography, 0, IDENT_IMAGES_PATH, C->ImagesDir, AvailableColumns); TypesetPair(&Typography, 0, IDENT_JS_PATH, C->JSDir, AvailableColumns); TypesetPair(&Typography, 0, IDENT_QUERY_STRING, C->QueryString, AvailableColumns); fprintf(stderr, "\n" "\n" "%s", Typography.Margin); bool ShouldFillSyntax = FALSE; PrintBool("Respecting Privacy (derived from projects)", C->RespectingPrivacy, Typography.Delimiter, ShouldFillSyntax, IndentationLevel, FALSE, TRUE); fprintf(stderr, "%s", Typography.Margin); int SecondsPerMinute = 60; config_pair PrivacyCheckInterval = { .Key = IDENT_PRIVACY_CHECK_INTERVAL, .int64_t = C->PrivacyCheckInterval / SecondsPerMinute, .Type = PT_INT64 }; PrintPair(&PrivacyCheckInterval, Typography.Delimiter, ShouldFillSyntax, IndentationLevel, FALSE, TRUE); fprintf(stderr, "%s", Typography.Margin); PrintLogLevel(ConfigIdentifiers[IDENT_LOG_LEVEL].String, C->LogLevel, Typography.Delimiter, IndentationLevel, TRUE); fprintf(stderr, "\n"); PrintTitle("People", &Typography, 0, TermCols, C->Person.ItemCount); for(int i = 0; i < C->Person.ItemCount; ++i) { PrintPerson(GetPlaceInBook(&C->Person, i), &Typography); } fprintf(stderr, "\n"); PrintTitle("Roles", &Typography, 0, TermCols, C->Role.ItemCount); for(int i = 0; i < C->Role.ItemCount; ++i) { PrintRole(GetPlaceInBook(&C->Role, i), &Typography); } fprintf(stderr, "\n"); PrintTitle("Projects", &Typography, 0, TermCols, -1); for(int i = 0; i < C->Project.ItemCount; ++i) { PrintProject(C, GetPlaceInBook(&C->Project, i), &Typography, 0, IndentationLevel, TermCols); } } } void FreeProject(project *P) { FreeBook(&P->Medium); FreeBook(&P->Credit); for(int i = 0; i < P->Child.ItemCount; ++i) { FreeProject(GetPlaceInBook(&P->Child, i)); } FreeBook(&P->Child); FreeTemplate(&P->SearchTemplate); FreeTemplate(&P->PlayerTemplate); } // NOTE(matt): Making this a #define permits Free() to null out the incoming C #define FreeConfig(C)\ {\ if(C)\ {\ for(int i = 0; i < C->Person.ItemCount; ++i)\ {\ person *Person = GetPlaceInBook(&C->Person, i);\ FreeBook(&Person->Support);\ }\ FreeBook(&C->Person);\ FreeBook(&C->Role);\ for(int i = 0; i < C->Project.ItemCount; ++i)\ {\ FreeProject(GetPlaceInBook(&C->Project, i));\ }\ FreeBook(&C->Project);\ FreeBook(&C->ResolvedVariables);\ FreeTemplate(&C->SearchTemplate);\ \ Free(C);\ }\ }\ config_verifiers InitVerifiers(void) { config_verifiers Result = {}; Result.VariantStrings = InitBook(sizeof(variant_string), 8); Result.HMMLDirs.ID0 = IDENT_HMML_DIR; Result.HMMLDirs.Associations = InitBook(sizeof(config_string_association), 8); Result.BaseDirAndSearchLocation.ID0 = IDENT_BASE_DIR; Result.BaseDirAndSearchLocation.ID1 = IDENT_SEARCH_LOCATION; Result.BaseDirAndSearchLocation.Associations = InitBook(sizeof(config_string_association), 8); Result.BaseDirAndPlayerLocation.ID0 = IDENT_BASE_DIR; Result.BaseDirAndPlayerLocation.ID1 = IDENT_PLAYER_LOCATION; Result.BaseDirAndPlayerLocation.Associations = InitBook(sizeof(config_string_association), 8); return Result; } bool IsPositioned(config_pair *Position) { return Position && Position->int64_t != 0; } void PositionRolesInConfig(config *C, resolution_errors *E, config_verifiers *V, scope_tree *S) { memory_book RolesStaging = InitBookOfPointers(4); for(int i = 0; i < S->Trees.ItemCount; ++i) { scope_tree *Tree = GetPlaceInBook(&S->Trees, i); if(Tree->ID.Key == IDENT_ROLE) { scope_tree **This = MakeSpaceInBook(&RolesStaging); *This = Tree; } } int SlotCount = RolesStaging.ItemCount; scope_tree *RoleSlot[SlotCount]; Clear(RoleSlot, sizeof(scope_tree *) * SlotCount); for(int i = 0; i < SlotCount; ++i) { scope_tree **Role = GetPlaceInBook(&RolesStaging, i); config_pair *Position = GetPair(*Role, IDENT_POSITION); if(IsPositioned(Position)) { int TargetPos; if(Position->int64_t < 0) { TargetPos = SlotCount + Position->int64_t; Clamp(0, TargetPos, SlotCount - 1); while(RoleSlot[TargetPos] && TargetPos > 0) { --TargetPos; } while(RoleSlot[TargetPos] && TargetPos < SlotCount - 1) { ++TargetPos; } } else if(Position->int64_t > 0) { TargetPos = Position->int64_t - 1; Clamp(0, TargetPos, SlotCount - 1); while(RoleSlot[TargetPos] && TargetPos < SlotCount - 1) { ++TargetPos; } while(RoleSlot[TargetPos] && TargetPos > 0) { --TargetPos; } } RoleSlot[TargetPos] = *Role; } } for(int i = 0; i < SlotCount; ++i) { scope_tree **Role = GetPlaceInBook(&RolesStaging, i); config_pair *Position = GetPair(*Role, IDENT_POSITION); if(!IsPositioned(Position)) { for(int j = 0; j < SlotCount; ++j) { if(!RoleSlot[j]) { RoleSlot[j] = *Role; break; } } } } for(int i = 0; i < SlotCount; ++i) { PushRoleOntoConfig(C, E, V, RoleSlot[i]); } FreeBook(&RolesStaging); } config * ResolveVariables(scope_tree *S) { Assert(LOG_COUNT == ArrayCount(LogLevelStrings)); config *Result = calloc(1, sizeof(config)); Result->ResolvedVariables = InitBookOfStrings(Kilobytes(1)); Result->Person = InitBook(sizeof(person), 8); Result->Role = InitBook(sizeof(role), 4); Result->Project = InitBook(sizeof(project), 8); resolution_errors Errors = {}; Errors.Warnings = InitBook(sizeof(resolution_error), 16); Errors.Errors = InitBook(sizeof(resolution_error), 16); config_verifiers Verifiers = InitVerifiers(); for(int i = 0; i < S->Pairs.ItemCount; ++i) { config_pair *Pair = GetPlaceInBook(&S->Pairs, i); string Filepath = Wrap0(Pair->Position.Filename); switch(Pair->Key) { // NOTE(matt): String case IDENT_DB_LOCATION: { Result->DatabaseLocation = StripSlashes(ResolveString(Result, &Errors, S, Pair, TRUE), P_ABS); } break; case IDENT_GLOBAL_SEARCH_DIR: { Result->GlobalSearchDir = StripSlashes(ResolveString(Result, &Errors, S, Pair, TRUE), P_ABS); if(Result->GlobalSearchDir.Length > MAX_BASE_DIR_LENGTH) { ConfigErrorSizing(&Filepath, Pair->Position.LineNumber, Pair->Key, &Result->GlobalSearchDir, MAX_BASE_DIR_LENGTH); PushError(&Errors, S_ERROR, 0, Pair->Key); } } break; case IDENT_GLOBAL_SEARCH_URL: { Result->GlobalSearchURL = StripSlashes(ResolveString(Result, &Errors, S, Pair, TRUE), P_ABS); if(Result->GlobalSearchURL.Length > MAX_BASE_URL_LENGTH) { ConfigErrorSizing(&Filepath, Pair->Position.LineNumber, Pair->Key, &Result->GlobalSearchURL, MAX_BASE_URL_LENGTH); PushError(&Errors, S_ERROR, 0, Pair->Key); } } break; case IDENT_GLOBAL_TEMPLATES_DIR: { Result->GlobalTemplatesDir = StripSlashes(ResolveString(Result, &Errors, S, Pair, TRUE), P_ABS); } break; case IDENT_GLOBAL_SEARCH_TEMPLATE: { Result->GlobalSearchTemplatePath = StripSlashes(ResolveString(Result, &Errors, S, Pair, FALSE), P_REL); } break; case IDENT_GLOBAL_THEME: { Result->GlobalTheme = ResolveString(Result, &Errors, S, Pair, FALSE); if(Result->GlobalTheme.Length > MAX_THEME_LENGTH) { ConfigErrorSizing(&Filepath, Pair->Position.LineNumber, Pair->Key, &Result->GlobalTheme, MAX_THEME_LENGTH); PushError(&Errors, S_ERROR, 0, Pair->Key); } } break; case IDENT_ASSETS_ROOT_DIR: { Result->AssetsRootDir = StripSlashes(ResolveString(Result, &Errors, S, Pair, TRUE), P_ABS); if(Result->AssetsRootDir.Length > MAX_ROOT_DIR_LENGTH) { ConfigErrorSizing(&Filepath, Pair->Position.LineNumber, Pair->Key, &Result->AssetsRootDir, MAX_ROOT_DIR_LENGTH); PushError(&Errors, S_ERROR, 0, Pair->Key); } } break; case IDENT_ASSETS_ROOT_URL: { Result->AssetsRootURL = StripSlashes(ResolveString(Result, &Errors, S, Pair, TRUE), P_ABS); if(Result->AssetsRootURL.Length > MAX_ROOT_URL_LENGTH) { ConfigErrorSizing(&Filepath, Pair->Position.LineNumber, Pair->Key, &Result->AssetsRootURL, MAX_ROOT_URL_LENGTH); PushError(&Errors, S_ERROR, 0, Pair->Key); } } break; case IDENT_CSS_PATH: { Result->CSSDir = StripSlashes(ResolveString(Result, &Errors, S, Pair, FALSE), P_REL); if(Result->CSSDir.Length > MAX_RELATIVE_ASSET_LOCATION_LENGTH) { ConfigErrorSizing(&Filepath, Pair->Position.LineNumber, Pair->Key, &Result->CSSDir, MAX_RELATIVE_ASSET_LOCATION_LENGTH); PushError(&Errors, S_ERROR, 0, Pair->Key); } } break; case IDENT_IMAGES_PATH: { Result->ImagesDir = StripSlashes(ResolveString(Result, &Errors, S, Pair, FALSE), P_REL); if(Result->ImagesDir.Length > MAX_RELATIVE_ASSET_LOCATION_LENGTH) { ConfigErrorSizing(&Filepath, Pair->Position.LineNumber, Pair->Key, &Result->ImagesDir, MAX_RELATIVE_ASSET_LOCATION_LENGTH); PushError(&Errors, S_ERROR, 0, Pair->Key); } } break; case IDENT_JS_PATH: { Result->JSDir = StripSlashes(ResolveString(Result, &Errors, S, Pair, FALSE), P_REL); if(Result->JSDir.Length > MAX_RELATIVE_ASSET_LOCATION_LENGTH) { ConfigErrorSizing(&Filepath, Pair->Position.LineNumber, Pair->Key, &Result->JSDir, MAX_RELATIVE_ASSET_LOCATION_LENGTH); PushError(&Errors, S_ERROR, 0, Pair->Key); } } break; case IDENT_QUERY_STRING: { Result->QueryString = ResolveString(Result, &Errors, S, Pair, FALSE); } break; case IDENT_CACHE_DIR: { Result->CacheDir = StripSlashes(ResolveString(Result, &Errors, S, Pair, TRUE), P_ABS); } break; // NOTE(matt): int64_t case IDENT_PRIVACY_CHECK_INTERVAL: { Result->PrivacyCheckInterval = SECONDS_PER_MINUTE * Pair->int64_t; } break; case IDENT_LOG_LEVEL: { Result->LogLevel = Pair->int64_t; } break; // NOTE(matt): bool case IDENT_SUPPRESS_PROMPTS: { Result->SuppressingPrompts = Pair->bool; } break; default: break; } } for(int i = 0; i < S->Trees.ItemCount; ++i) { scope_tree *Tree = GetPlaceInBook(&S->Trees, i); if(Tree->ID.Key == IDENT_PERSON) { PushPersonOntoConfig(Result, &Errors, &Verifiers, Tree); } } PositionRolesInConfig(Result, &Errors, &Verifiers, S); for(int i = 0; i < S->Trees.ItemCount; ++i) { scope_tree *Tree = GetPlaceInBook(&S->Trees, i); if(Tree->ID.Key == IDENT_PROJECT) { PushProjectOntoConfig(Result, &Errors, &Verifiers, Tree); } } config_string_association *BaseDirAndSearchLocation = GetGlobalPathAssociation(&Verifiers.BaseDirAndSearchLocation, Result->GlobalSearchDir); if(BaseDirAndSearchLocation) { PushError(&Errors, S_ERROR, 0, IDENT_GLOBAL_SEARCH_DIR); AddProjectToAssociation(BaseDirAndSearchLocation, 0); } // TODO(matt): Mandate that, if a GlobalSearchDir or GlobalSearchURL is set, then both must be set if(!Result->GlobalTheme.Length) { ConfigErrorUnset(IDENT_GLOBAL_THEME); PushError(&Errors, S_ERROR, 0, IDENT_GLOBAL_THEME); } if(Errors.Errors.ItemCount > 0) { PrintAssociationClashes(&Verifiers.HMMLDirs); PrintAssociationClashes(&Verifiers.BaseDirAndSearchLocation); PrintAssociationClashes(&Verifiers.BaseDirAndPlayerLocation); PrintVariantInconsistencies(&Verifiers.VariantStrings); FreeConfig(Result); } FreeVerifiers(&Verifiers); FreeErrors(&Errors); return Result; } config * ParseConfig(string Path, memory_book *TokensList) { //PrintFunctionName("ParseConfig()"); _memory_book(config_type_spec) TypeSpecs = InitTypeSpecs(); #if 0 MEM_LOOP_PRE_FREE("InitTypeSpecs") FreeTypeSpecs(&TypeSpecs); MEM_LOOP_PRE_WORK() TypeSpecs = InitTypeSpecs(); MEM_LOOP_POST("InitTypeSpecs") #endif config *Result = 0; tokens *T = Tokenise(TokensList, Path); #if 0 MEM_LOOP_PRE_FREE("Tokenise") FreeTokensList(TokensList); //FreeAndResetCount(WatchHandles.Handle, WatchHandles.Count); MEM_LOOP_PRE_WORK() T = Tokenise(TokensList, Path); MEM_LOOP_POST("Tokenise") #endif if(T) { scope_tree *ScopeTree = InitRootScopeTree(); SetTypeSpec(ScopeTree, &TypeSpecs); SetDefaults(ScopeTree, &TypeSpecs); ScopeTree = ScopeTokens(ScopeTree, TokensList, T, &TypeSpecs, 0); // TODO(matt): Mem testing // #if 0 MEM_LOOP_PRE_FREE("ScopeTree") FreeScopeTree(ScopeTree); FreeTokensList(TokensList); MEM_LOOP_PRE_WORK() T = Tokenise(TokensList, Path); ScopeTree = InitRootScopeTree(); SetTypeSpec(ScopeTree, &TypeSpecs); SetDefaults(ScopeTree, &TypeSpecs); ScopeTree = ScopeTokens(ScopeTree, TokensList, T, &TypeSpecs, 0); MEM_LOOP_POST("ScopeTree") #endif // //// //PrintScopeTree(ScopeTree); if(ScopeTree) { //PrintC(CS_GREEN, "Resolving scope tree\n"); Result = ResolveVariables(ScopeTree); #if 0 MEM_LOOP_PRE_FREE("ResolveVariables") FreeConfig(Result); MEM_LOOP_PRE_WORK() Result = ResolveVariables(ScopeTree); MEM_LOOP_POST("ResolveVariables") #endif FreeScopeTree(ScopeTree); } FreeTokensList(TokensList); } FreeTypeSpecs(&TypeSpecs); return Result; } // // config #if 0 int main(int ArgC, char **Args) { //fprintf(stderr, "%s\n", ConfigFile.Buffer.Location); config_type_specs TypeSpecs = InitTypeSpecs(); config *Config = ParseConfig(&TypeSpecs, Wrap0("cinera.conf")); if(Config) { //PrintConfig(Config); FreeConfig(Config); } FreeTypeSpecs(&TypeSpecs); _exit(0); } #endif