From 457281783746b9fd1034970b1e99af4035137258 Mon Sep 17 00:00:00 2001 From: Matt Mascarenhas Date: Sat, 11 Nov 2017 00:34:47 +0000 Subject: [PATCH] cinera.c: Search [#7] Also put the stuff in a new directory called cinera, that contains only the files needed to get going, with example template files. --- README.md | 120 +- .../hmml_to_html.c => cinera/cinera.c | 1486 ++++++++++------- {hmml_to_html => cinera}/cinera.css | 90 +- {hmml_to_html => cinera}/cinera__hero.css | 34 + {hmml_to_html => cinera}/cinera__riscy.css | 42 +- .../cinera_icon_filter.png | Bin cinera/cinera_player_post.js | 186 +++ .../cinera.js => cinera/cinera_player_pre.js | 68 +- cinera/cinera_search.js | 240 +++ cinera/cinera_sprite_patreon.png | Bin 0 -> 881 bytes cinera/template_index.html | 12 + cinera/template_player.html | 17 + 12 files changed, 1649 insertions(+), 646 deletions(-) rename hmml_to_html/hmml_to_html.c => cinera/cinera.c (77%) rename {hmml_to_html => cinera}/cinera.css (86%) rename {hmml_to_html => cinera}/cinera__hero.css (81%) rename {hmml_to_html => cinera}/cinera__riscy.css (87%) rename {hmml_to_html => cinera}/cinera_icon_filter.png (100%) create mode 100644 cinera/cinera_player_post.js rename hmml_to_html/cinera.js => cinera/cinera_player_pre.js (94%) create mode 100644 cinera/cinera_search.js create mode 100644 cinera/cinera_sprite_patreon.png create mode 100644 cinera/template_index.html create mode 100644 cinera/template_player.html diff --git a/README.md b/README.md index e5b1b86..c61ab13 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,125 @@ Fair warning: This is all under development and not yet packaged up for easy deployment -## hmml_to_html +## cinera ### Download, and prepare the parser 1. `git clone git@gitssh.handmade.network:Annotation-Pushers/Annotation-System.git` 2. `cd Annotation-System/hmmlib` 3. `make` -4. `cp hmml.a hmmlib.h ../hmml_to_html/` -5. `cd ../hmml_to_html/` +4. `cp hmml.a hmmlib.h ../cinera/` +5. `cd ../cinera/` Note: For each parser update, remember to make and copy it into place ### Build -1. `zsh hmml_to_html.c` (replacing zsh with your shell as appropriate) +1. `zsh cinera.c` (replacing zsh with your shell as appropriate) ### Run -#### Ordinary operation +#### Single Edition operation - ./hmml_to_html test.hmml + cinera test.hmml This simply generates an HTML file (and updates `cinera_topics.css` if needed) from `test.hmml` and outputs to `out.html` -Note that if the `.hmml` file contains quotes (as `test.hmml` does), it would be -easiest for now just to remove the quote from `test.hmml`. If you want quotes -to work, run: +#### Project Edition operation - mkdir /home/matt/git/GitHub/insofaras/25fc16d58a297a486334 - git clone https://gist.github.com/insofaras/25fc16d58a297a486334 /home/matt/git/GitHub/insofaras/25fc16d58a297a486334 + cinera -p ProjectID - (Seriously, the path is hardcoded for now) +Setting the ProjectID with the `-p` flag triggers Project Edition. In this +edition `cinera` monitors the Project Input Directory for new, edited and +deleted .hmml files, and generates one table of contents / search page and a +player page each for valid sets of annotations (or removes them, if needed). -Note also that `cinera.css` contains a `body {}` block at the bottom which you -may want to remove / comment out +By default all directories - input and output - are set to the current working +directory. Typical operation will involve setting these flags: + + -d Project Input Directory, the directory where the .hmml files reside + -r Root Directory, path shallower than or equal to the CSS, Images and JS + directories + -u Root URL, corresponding to the Root Directory (optional if the Output + Base Directory resides in the Root Directory) + -b Output Base Directory, location of the table of contents / search page + -t Player Template Location + -x Index Template Location #### Integration - CINERA_MODE=INTEGRATE ./hmml_to_html test.hmml + CINERA_MODE=INTEGRATE cinera test.hmml -This will integrate into `template.html` (currently hardcoded) the player and -related elements generated from `test.hmml` and output to `out.html` +This will integrate into `template_player.html` (configurable with -t) the +player and related elements generated from `test.hmml` and output to +`out_integrated.html` -Feel free to play with `template.html` to your heart's content. If you do -anything invalid, `hmml_to_html` will tell you what's wrong +Feel free to play with `template_player.html` to your heart's content. If you do +anything invalid, `cinera` will tell you what's wrong + +#### Templates Valid tags: - `` _the day / episode name, intended to be used inside your own `` element, but may be used wherever and as many times as you want on your page_ + +*Index Template* +- `<!-- __CINERA_INCLUDES__ -->` _the necessary `.css` and `.js` files_ +- `<!-- __CINERA_INDEX__ -->` _the table of contents, and search functionality_ + +*Player Template* - `<!-- __CINERA_INCLUDES__ -->` _the necessary `.css` and `.js` files, and charset setting_ -- `<!-- __CINERA_MENUS__ -->` _ _the menu bar that typically appears above the - player in my samples_ +- `<!-- __CINERA_MENUS__ -->` _ _the menu bar that typically appears above the player in my samples_ - `<!-- __CINERA_PLAYER__ -->` _the player_ -- `<!-- __CINERA_SCRIPT__ -->` _the listeners that enable interaction with the player_ +- `<!-- __CINERA_SCRIPT__ -->` _the filter state objects and `.js` file, which must come after both the MENUS and PLAYER tags_ #### Arguments - Usage: ./hmml_to_html [option(s)] filename(s) + Usage: ./cinera [option(s)] filename(s) Options: - -c <CSS directory path> - Override default CSS directory (".") - -i <images directory path> - Override default images directory (".") - -j <JS directory path> - Override default JS directory (".") - -o <output location> - Override default output location ("out.html") - -q <quotes directory path> - Override default quotes directory ("/home/matt/git/GitHub/insofaras/25fc16d58a297a486334") - -t <template location> - Override default template location ("template.html") - and automatically enable integration - -h - display this help + Paths: + -r <root directory> + Override default root directory (".") + -u <root URL> + Override default root URL ("") + -b <base output directory> + Override project's default base output directory (".") + -c <CSS directory path> + Override default CSS directory (""), relative to root + -i <images directory path> + Override default images directory (""), relative to root + -j <JS directory path> + Override default JS directory (""), relative to root + -t <player template location> + Override default player template location ("template_player.html"), relative to root + and automatically enable integration + -x <index template location> + Override default index template location ("template_index.html"), relative to root + and automatically enable integration + + -o <output location> + Override default output player location for SINGLE_EDITION ("out.html") + -d <project directory> + Override default project directory (".") + + -f + Force integration with an incomplete template + -p <project ID> + Set the project ID, corresponding to the "project" field in the HMML files + -l <n> + Override default log level (0), where n is from 0 (terse) to 7 (verbose) + -m <default medium> + Override default default medium ("programming") + -U <seconds> + Override default update interval ("4") + -h + display this help + +#### Environment Variables + + CINERA_MODE=INTEGRATE + Enable integration diff --git a/hmml_to_html/hmml_to_html.c b/cinera/cinera.c similarity index 77% rename from hmml_to_html/hmml_to_html.c rename to cinera/cinera.c index 7d91aba..724bfbe 100644 --- a/hmml_to_html/hmml_to_html.c +++ b/cinera/cinera.c @@ -6,7 +6,18 @@ ctime -end ${0%.*}.ctm exit #endif -#define CINERA_VERSION "0.4.0" +typedef struct +{ + unsigned int Major, Minor, Patch; +} version; + +version CINERA_APP_VERSION = { + .Major = 0, + .Minor = 5, + .Patch = 0 +}; + +#define CINERA_DB_VERSION 1 #define DEBUG 0 #define DEBUG_MEM 0 @@ -70,15 +81,14 @@ enum RC_ERROR_HMML, RC_ERROR_MAX_REFS, RC_ERROR_MEMORY, + RC_ERROR_PROJECT, RC_ERROR_QUOTE, - RC_ERROR_TEMPLATE, - RC_FAILURE, + RC_ERROR_SEEK, RC_FOUND, RC_UNFOUND, - RC_INVALID_TEMPLATE, RC_INVALID_REFERENCE, + RC_INVALID_TEMPLATE, RC_NOOP, - RC_REFRESHED, RC_RIP, RC_SUCCESS } returns; @@ -90,15 +100,16 @@ typedef struct int Mode; int UpdateInterval; bool ForceIntegration; - char *RootDir; - char *BaseDir; // Relative to RootDir - char *CSSDir; // Relative to RootDir - char *ImagesDir; // Relative to RootDir + char *RootDir; // Absolute + char *RootURL; char *ProjectID; - char *JSDir; // Relative to RootDir - char *TemplateIndexLocation; // Relative to RootDir - char *TemplatePlayerLocation; // Relative to RootDir - char CacheDir[255]; + char *BaseDir; // Absolute + char *CSSDir; // Relative to RootDir and RootURL + char *ImagesDir; // Relative to RootDir and RootURL + char *JSDir; // Relative to RootDir and RootURL + char *TemplateIndexLocation; // Relative to RootDir and RootURL + char *TemplatePlayerLocation; // Relative to RootDir and RootURL + char CacheDir[256]; char *DefaultMedium; char *OutLocation; char *OutIntegratedLocation; @@ -129,7 +140,7 @@ typedef struct { buffer Buffer; FILE *Handle; - char Path[255]; + char Path[256]; int FileSize; } file_buffer; @@ -171,7 +182,7 @@ typedef struct typedef struct { - char Filename[120]; + char Filename[256]; tag_offset Tag[16]; int Validity; // NOTE(matt): Bitmask describing which page the template is valid for, i.e. contents and / or player page int TagCount; @@ -186,12 +197,14 @@ typedef struct typedef struct { buffer IncludesIndex; + buffer Search; buffer Index; + buffer ScriptIndex; buffer IncludesPlayer; buffer Menus; buffer Player; - buffer Script; - char Title[255]; + buffer ScriptPlayer; + char Title[256]; } buffers; // TODO(matt): Consider putting the ref_info and quote_info into linked lists on the heap, just to avoid all the hardcoded sizes @@ -244,12 +257,12 @@ typedef struct credential_info Credentials[] = { - { "Miblo", "Matt Mascarenhas", "http://miblodelcarpio.co.uk", "cinera_icon_patreon.png", "https://patreon.com/miblo"}, - { "miotatsu", "Mio Iwakura", "http://riscy.tv/", "cinera_icon_patreon.png", "https://patreon.com/miotatsu"}, + { "Miblo", "Matt Mascarenhas", "http://miblodelcarpio.co.uk", "cinera_sprite_patreon.png", "https://patreon.com/miblo"}, + { "miotatsu", "Mio Iwakura", "http://riscy.tv/", "cinera_sprite_patreon.png", "https://patreon.com/miotatsu"}, { "nothings", "Sean Barrett", "https://nothings.org/", "", ""}, - { "cmuratori", "Casey Muratori", "https://handmadehero.org", "cinera_icon_patreon.png", "https://patreon.com/cmuratori"}, + { "cmuratori", "Casey Muratori", "https://handmadehero.org", "cinera_sprite_patreon.png", "https://patreon.com/cmuratori"}, { "fierydrake", "Mike Tunnicliffe", "", "", ""}, - { "abnercoimbre", "Abner Coimbre", "https://handmade.network/m/abnercoimbre", "cinera_icon_patreon.png", "https://patreon.com/handmade_dev"}, + { "abnercoimbre", "Abner Coimbre", "https://handmade.network/m/abnercoimbre", "cinera_sprite_patreon.png", "https://patreon.com/handmade_dev"}, { "/y_lee", "Yunsup Lee", "https://www.linkedin.com/in/yunsup-lee-385b692b/", "", ""}, { "/a_waterman", "Andrew Waterman", "https://www.linkedin.com/in/andrew-waterman-76805788", "", ""}, { "debiatan", "Miguel Lechón", "http://blog.debiatan.net/", "", ""}, @@ -278,16 +291,42 @@ category_medium CategoryMedium[] = { "trivia", "🎲", "Trivia"}, }; +enum +{ + NS_CALENDRICAL, + NS_LINEAR, + NS_SEASONAL, +} numbering_schemes; + +typedef struct +{ + char *ProjectID; + char *FullName; + char *Unit; // e.g. Day, Episode, Session + int NumberingScheme; +} project_info; + +project_info ProjectInfo[] = +{ + { "hero", "Handmade Hero", "Day", NS_LINEAR }, + { "hmdshow", "HandmadeDev Show", "", NS_SEASONAL }, + { "obbg", "Open Block Building Game", "Episode", NS_LINEAR }, + { "riscy", "RISCY BUSINESS", "Day", NS_LINEAR }, + { "sysadmin", "SysAdmin", "Session", NS_LINEAR }, +}; + #define ArrayCount(A) sizeof(A)/sizeof(*(A)) __attribute__ ((format (printf, 2, 3))) -void +int CopyString(char Dest[], char *Format, ...) { + int Length = 0; va_list Args; va_start(Args, Format); - vsprintf(Dest, Format, Args); + Length = vsprintf(Dest, Format, Args); va_end(Args); + return Length; } int @@ -355,7 +394,7 @@ CopyStringToBuffer(buffer *Dest, char *Format, ...) va_start(Args, Format); int Length = vsnprintf(Dest->Ptr, Dest->Size - (Dest->Ptr - Dest->Location), Format, Args); va_end(Args); - // TODO(matt): + // TODO(matt) { if(Length + (Dest->Ptr - Dest->Location) >= Dest->Size) { @@ -407,6 +446,28 @@ CopyStringToBufferHTMLSafe(buffer *Dest, char *String) } } +void +CopyStringToBufferJSONSafe(buffer *Dest, char *String) +{ + while(*String) + { + if(Dest->Ptr - Dest->Location >= Dest->Size) + { + fprintf(stderr, "CopyStringToBufferHTMLSafe: %s cannot accommodate %d-character string\n", Dest->ID, StringLength(String)); + __asm__("int3"); + } + switch(*String) + { + case '\\': + case '\"': + *Dest->Ptr++ = '\\'; + default: + *Dest->Ptr++ = *String++; + break; + } + } +} + int StringsDiffer(char *A, char *B) // NOTE(matt): Two null-terminated strings { @@ -480,7 +541,7 @@ void LogUsage(buffer *Buffer) { #if DEBUG - char LogPath[255]; + char LogPath[256]; CopyString(LogPath, "%s/%s", Config.CacheDir, "buffers.log"); FILE *LogFile; if(!(LogFile = fopen(LogPath, "a+"))) @@ -507,7 +568,7 @@ LogError(int LogLevel, char *Format, ...) { if(Config.LogLevel >= LogLevel) { - char LogPath[255]; + char LogPath[256]; CopyString(LogPath, "%s/%s", Config.CacheDir, "errors.log"); FILE *LogFile; if(!(LogFile = fopen(LogPath, "a+"))) @@ -534,6 +595,7 @@ void FreeBuffer(buffer *Buffer) { free(Buffer->Location); + Buffer->Location = '\0'; #if DEBUG_MEM FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); fprintf(MemLog, " Freed %s\n", Buffer->ID); @@ -597,6 +659,7 @@ DeclaimBuffer(buffer *Buffer) LogError(LOG_ERROR, "%s used %.2f%% of its allotted memory\n", Buffer->ID, PercentageUsed); fprintf(stderr, "Warning: %s used %.2f%% of its allotted memory\n", Buffer->ID, PercentageUsed); } + Buffer->Size = 0; } void RewindBuffer(buffer *Buffer) @@ -620,7 +683,7 @@ ClaimTemplate(template **Template, char *Location) (*Template)->Buffer.Location = MemoryArena.Ptr + sizeof(template); (*Template)->Buffer.Ptr = (*Template)->Buffer.Location; (*Template)->Buffer.ID = Location; - CopyString((*Template)->Metadata.Filename, Location); + CopyString((*Template)->Metadata.Filename, "%s/%s", Config.RootDir, Location); FILE *File; if(!(File = fopen((*Template)->Metadata.Filename, "r"))) @@ -734,7 +797,7 @@ StringToColourHash(hsl_colour *Colour, char *String) { Colour->Hue = 0; Colour->Saturation = 0; - Colour->Lightness = 26; + Colour->Lightness = 74; int i; for(i = 0; String[i]; ++i) @@ -774,7 +837,7 @@ enum CreditsError_NoHost, CreditsError_NoAnnotator, CreditsError_NoCredentials -}; +} credits_errors; int SearchCredentials(buffer *CreditsMenu, bool *HasCreditsMenu, char *Person, char *Role) @@ -788,10 +851,10 @@ SearchCredentials(buffer *CreditsMenu, bool *HasCreditsMenu, char *Person, char if(*HasCreditsMenu == FALSE) { CopyStringToBuffer(CreditsMenu, - " <div class=\"menu credits\">\n" - " <div class=\"mouse_catcher\"></div>\n" - " <span>Credits</span>\n" - " <div class=\"credits_container\">\n"); + " <div class=\"menu credits\">\n" + " <div class=\"mouse_catcher\"></div>\n" + " <span>Credits</span>\n" + " <div class=\"credits_container\">\n"); *HasCreditsMenu = TRUE; } @@ -822,22 +885,38 @@ SearchCredentials(buffer *CreditsMenu, bool *HasCreditsMenu, char *Person, char if(*Credentials[CredentialIndex].SupportIcon && *Credentials[CredentialIndex].SupportURL) { - if(Config.Edition == EDITION_PROJECT) + char URLPrefix[1024] = { 0 }; + char *Ptr = URLPrefix; + if(StringsDiffer(Config.RootURL, "")) { - CopyStringToBuffer(CreditsMenu, - " <a class=\"support\" href=\"%s\" target=\"_blank\"><img src=\"../%s/%s\"></a>\n", - Credentials[CredentialIndex].SupportURL, - Config.ImagesDir, - Credentials[CredentialIndex].SupportIcon); + Ptr += CopyString(Ptr, "%s/", Config.RootURL); } else { - CopyStringToBuffer(CreditsMenu, - " <a class=\"support\" href=\"%s\" target=\"_blank\"><img src=\"%s/%s\"></a>\n", - Credentials[CredentialIndex].SupportURL, - Config.ImagesDir, - Credentials[CredentialIndex].SupportIcon); + if(Config.Edition == EDITION_PROJECT) + { + Ptr += CopyString(Ptr, "../"); + } } + if(StringsDiffer(Config.ImagesDir, "")) + { + Ptr += CopyString(Ptr, "%s/", Config.ImagesDir); + } + + if(Config.Edition == EDITION_PROJECT) + { + if(!StringsDiffer(Config.RootURL, "")) + { + char Temp[1027]; + CopyString(Temp, "../%s", URLPrefix); + CopyString(URLPrefix, Temp); + } + } + CopyStringToBuffer(CreditsMenu, + " <a class=\"support\" href=\"%s\" target=\"_blank\"><div class=\"support_icon\" style=\"background-image: url(%s%s);\"></div></a>\n", + Credentials[CredentialIndex].SupportURL, + URLPrefix, + Credentials[CredentialIndex].SupportIcon); } CopyStringToBuffer(CreditsMenu, @@ -849,7 +928,7 @@ SearchCredentials(buffer *CreditsMenu, bool *HasCreditsMenu, char *Person, char int BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Metadata) - // TODO(matt): Make this take the Credentials, once we are parsing them from a config +// TODO(matt): Make this take the Credentials, once we are parsing them from a config { if(Metadata->member) { @@ -1192,7 +1271,14 @@ BuildCategories(buffer *AnnotationClass, buffer *TopicDots, categories *LocalTop for(int i = 0; i < LocalMedia->Count; ++i) { + if(!StringsDiffer(LocalMedia->Category[i].Marker, "afk")) // TODO(matt): Initially hidden config + { + CopyStringToBuffer(AnnotationClass, " off_%s skip", SanitisePunctuation(LocalMedia->Category[i].Marker)); // TODO(matt): Bulletproof this? + } + else + { CopyStringToBuffer(AnnotationClass, " %s", SanitisePunctuation(LocalMedia->Category[i].Marker)); + } } CopyStringToBuffer(AnnotationClass, "\""); @@ -1295,9 +1381,9 @@ BuildQuote(quote_info *Info, char *Speaker, int ID) { // TODO(matt): Rebuild cache option - char QuoteCacheDir[255]; + char QuoteCacheDir[256]; CopyString(QuoteCacheDir, "%s/quotes", Config.CacheDir); - char QuoteCachePath[255]; + char QuoteCachePath[256]; CopyString(QuoteCachePath, "%s/%s", QuoteCacheDir, Speaker); FILE *QuoteCache; char QuotesURL[256]; @@ -1401,13 +1487,14 @@ GenerateTopicColours(char *Topic) file_buffer Topics; Topics.Buffer.ID = "Topics"; - // TODO(matt): Correctly locate cinera_topics.css once the config takes everything we need - //char TopicsPath[255]; - CopyString(Topics.Path, "%s/cinera_topics.css", Config.CSSDir); -#if 0 - printf("BaseDir: %s\n" - "CSSDir relative to BaseDir: %s\n", Config->BaseDir, Config->CSSDir); -#endif + if(StringsDiffer(Config.CSSDir, "")) + { + CopyString(Topics.Path, "%s/%s/cinera_topics.css", Config.RootDir, Config.CSSDir); + } + else + { + CopyString(Topics.Path, "%s/cinera_topics.css", Config.RootDir); + } if((Topics.Handle = fopen(Topics.Path, "a+"))) { @@ -1422,10 +1509,10 @@ GenerateTopicColours(char *Topic) } #if DEBUG_MEM - FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); - fprintf(MemLog, " Allocated Topics (%d)\n", Topics.Size); - fclose(MemLog); - printf(" Allocated Topics (%d)\n", Topics.Size); + FILE *MemLog = fopen("/home/matt/cinera_mem", "a+"); + fprintf(MemLog, " Allocated Topics (%d)\n", Topics.Size); + fclose(MemLog); + printf(" Allocated Topics (%d)\n", Topics.Size); #endif Topics.Buffer.Ptr = Topics.Buffer.Location; @@ -1471,8 +1558,10 @@ PrintUsage(char *BinaryLocation, config *DefaultConfig) " Paths: \n" " -r <root directory>\n" " Override default root directory (\"%s\")\n" + " -u <root URL>\n" + " Override default root URL (\"%s\")\n" " -b <base output directory>\n" - " Override default base output directory (\"%s\"), relative to root\n" + " Override project's default base output directory (\"%s\")\n" " -c <CSS directory path>\n" " Override default CSS directory (\"%s\"), relative to root\n" " -i <images directory path>\n" @@ -1488,18 +1577,18 @@ PrintUsage(char *BinaryLocation, config *DefaultConfig) "\n" " -o <output location>\n" " Override default output player location for SINGLE_EDITION (\"%s\")\n" - " -p <project directory>\n" + " -d <project directory>\n" " Override default project directory (\"%s\")\n" "\n" " -f\n" " Force integration with an incomplete template\n" - " -I <project ID>\n" + " -p <project ID>\n" " Set the project ID, corresponding to the \"project\" field in the HMML files\n" " -l <n>\n" " Override default log level (%d), where n is from 0 (terse) to 7 (verbose)\n" " -m <default medium>\n" " Override default default medium (\"%s\")\n" - " -u <seconds>\n" + " -U <seconds>\n" " Override default update interval (\"%d\")\n" //" -c config location\n" " -h\n" @@ -1521,7 +1610,7 @@ PrintUsage(char *BinaryLocation, config *DefaultConfig) "\n" "HMML Specification:\n" " https://git.handmade.network/Annotation-Pushers/Annotation-System/wikis/hmmlspec\n", - BinaryLocation, DefaultConfig->RootDir, DefaultConfig->BaseDir, DefaultConfig->CSSDir, DefaultConfig->ImagesDir, DefaultConfig->JSDir, DefaultConfig->TemplatePlayerLocation, DefaultConfig->TemplateIndexLocation, DefaultConfig->OutLocation, DefaultConfig->ProjectDir, DefaultConfig->LogLevel, DefaultConfig->DefaultMedium, DefaultConfig->UpdateInterval); + BinaryLocation, DefaultConfig->RootDir, DefaultConfig->RootURL, DefaultConfig->BaseDir, DefaultConfig->CSSDir, DefaultConfig->ImagesDir, DefaultConfig->JSDir, DefaultConfig->TemplatePlayerLocation, DefaultConfig->TemplateIndexLocation, DefaultConfig->OutLocation, DefaultConfig->ProjectDir, DefaultConfig->LogLevel, DefaultConfig->DefaultMedium, DefaultConfig->UpdateInterval); } void @@ -1671,8 +1760,6 @@ Here: } } - //FreeBuffer(&(*Template)->Buffer); - if(FoundIndex) { (*Template)->Metadata.Validity |= PAGE_INDEX; @@ -1710,10 +1797,11 @@ HMMLToBuffers(buffers *CollationBuffers, char *Filename) RewindBuffer(&CollationBuffers->IncludesPlayer); RewindBuffer(&CollationBuffers->Menus); RewindBuffer(&CollationBuffers->Player); - RewindBuffer(&CollationBuffers->Script); + RewindBuffer(&CollationBuffers->ScriptPlayer); RewindBuffer(&CollationBuffers->IncludesIndex); + RewindBuffer(&CollationBuffers->ScriptIndex); - char Filepath[255]; + char Filepath[256]; if(Config.Edition == EDITION_PROJECT) { CopyString(Filepath, "%s/%s", Config.ProjectDir, Filename); @@ -1828,12 +1916,30 @@ HMMLToBuffers(buffers *CollationBuffers, char *Filename) #if DEBUG printf("\n\n --- Entering Annotations Loop ---\n\n\n\n"); #endif + + RewindBuffer(&CollationBuffers->Search); + char *FilenamePtr = Filename; + FilenamePtr += StringLength(Filename) - StringLength(".hmml"); + *FilenamePtr = '\0'; + CopyStringToBuffer(&CollationBuffers->Search, "name: \"%s\"\n" + "title: \"", Filename); + *FilenamePtr = '.'; + CopyStringToBufferJSONSafe(&CollationBuffers->Search, HMML.metadata.title); + CopyStringToBuffer(&CollationBuffers->Search, "\"\n" + "markers:\n"); + for(int AnnotationIndex = 0; AnnotationIndex < HMML.annotation_count; ++AnnotationIndex) { #if DEBUG printf("%d\n", AnnotationIndex); #endif + HMML_Annotation *Anno = HMML.annotations + AnnotationIndex; + + CopyStringToBuffer(&CollationBuffers->Search, "\"%d\": \"", TimecodeToSeconds(Anno->time)); + CopyStringToBufferJSONSafe(&CollationBuffers->Search, Anno->text); + CopyStringToBuffer(&CollationBuffers->Search, "\"\n"); + categories LocalTopics = { 0 }; categories LocalMedia = { 0 }; bool HasQuote = FALSE; @@ -2272,6 +2378,11 @@ AppendedIdentifier: CopyBuffer(&Annotation, &Text); + if(LocalTopics.Count > 0) + { + CopyBuffer(&Annotation, &TopicDots); + } + CopyStringToBuffer(&Annotation, "</div>\n" " </div>\n" " <div class=\"progress main\">\n" @@ -2280,6 +2391,11 @@ AppendedIdentifier: CopyBuffer(&Annotation, &Text); + if(LocalTopics.Count > 0) + { + CopyBuffer(&Annotation, &TopicDots); + } + CopyStringToBuffer(&Annotation, "</div>\n" " </div>\n" " </div>\n"); @@ -2301,6 +2417,7 @@ AppendedIdentifier: DeclaimBuffer(&AnnotationHeader); DeclaimBuffer(&Annotation); } + CopyStringToBuffer(&CollationBuffers->Search, "---\n"); #if DEBUG printf("\n\n --- End of Annotations Loop ---\n\n\n\n"); @@ -2371,7 +2488,29 @@ AppendedIdentifier: if(HasFilterMenu) { + CopyStringToBuffer(&FilterState, "<script>\n" + " var filterInitState = {\n"); + for(int i = 0; i < Topics.Count; ++i) + { + CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": false },\n", + Topics.Category[i].Marker, "topic"); + } + for(int i = 0; i < Media.Count; ++i) + { + if(!StringsDiffer(Media.Category[i].Marker, "afk")) // TODO(matt): Make this configurable? + { + CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": true },\n", + Media.Category[i].Marker, "medium"); + } + else + { + CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": false },\n", + Media.Category[i].Marker, "medium"); + } + } CopyStringToBuffer(&FilterState, + " };\n" + "\n" " var filterState = {\n"); for(int i = 0; i < Topics.Count; ++i) { @@ -2380,31 +2519,58 @@ AppendedIdentifier: } for(int i = 0; i < Media.Count; ++i) { - CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": false },\n", - Media.Category[i].Marker, "medium"); + if(!StringsDiffer(Media.Category[i].Marker, "afk")) // TODO(matt): Make this configurable? + { + CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": true },\n", + Media.Category[i].Marker, "medium"); + } + else + { + CopyStringToBuffer(&FilterState, "\"%s\":\t{ \"type\": \"%s\",\t\"off\": false },\n", + Media.Category[i].Marker, "medium"); + } } - CopyStringToBuffer(&FilterState, - " };\n"); - if(Config.Edition == EDITION_PROJECT) + CopyStringToBuffer(&FilterState, + " };\n" + " </script>\n"); + + char URLPrefix[1024] = { 0 }; + char *Ptr = URLPrefix; + + if(StringsDiffer(Config.RootURL, "")) { - CopyStringToBuffer(&FilterMenu, - " <div class=\"menu filter\">\n" - " <span><img src=\"../%s/cinera_icon_filter.png\"></span>\n" - " <div class=\"filter_container\">\n" - " <div class=\"filter_mode inclusive\">Filter mode: </div>\n" - " <div class=\"filters\">\n", Config.ImagesDir); + Ptr += CopyString(Ptr, "%s/", Config.RootURL); } else { - CopyStringToBuffer(&FilterMenu, - " <div class=\"menu filter\">\n" - " <span><img src=\"%s/cinera_icon_filter.png\"></span>\n" - " <div class=\"filter_container\">\n" - " <div class=\"filter_mode inclusive\">Filter mode: </div>\n" - " <div class=\"filters\">\n", Config.ImagesDir); + if(Config.Edition == EDITION_PROJECT) + { + Ptr += CopyString(Ptr, "../"); + } + if(StringsDiffer(Config.ImagesDir, "")) + { + Ptr += CopyString(Ptr, "%s/", Config.ImagesDir); + } } + if(Config.Edition == EDITION_PROJECT) + { + if(!StringsDiffer(Config.RootURL, "")) + { + char Temp[1027]; + CopyString(Temp, "../%s", URLPrefix); + CopyString(URLPrefix, Temp); + } + } + CopyStringToBuffer(&FilterMenu, + " <div class=\"menu filter\">\n" + " <span><img src=\"%scinera_icon_filter.png\"></span>\n" + " <div class=\"filter_container\">\n" + " <div class=\"filter_mode inclusive\">Filter mode: </div>\n" + " <div class=\"filters\">\n", + URLPrefix); + if(Topics.Count > 0) { CopyStringToBuffer(&FilterMenu, @@ -2441,14 +2607,28 @@ AppendedIdentifier: } } - CopyStringToBuffer(&FilterMedia, - " <div class=\"filter_content %s\">\n" - " <span class=\"icon\">%s</span><span class=\"text\">%s</span>\n" - " </div>\n", - Media.Category[i].Marker, - CategoryMedium[j].Icon, - CategoryMedium[j].WrittenName - ); + if(!StringsDiffer(Media.Category[i].Marker, "afk")) // TODO(matt): Initially hidden config + { + CopyStringToBuffer(&FilterMedia, + " <div class=\"filter_content %s off\">\n" + " <span class=\"icon\">%s</span><span class=\"text\">%s</span>\n" + " </div>\n", + Media.Category[i].Marker, + CategoryMedium[j].Icon, + CategoryMedium[j].WrittenName + ); + } + else + { + CopyStringToBuffer(&FilterMedia, + " <div class=\"filter_content %s\">\n" + " <span class=\"icon\">%s</span><span class=\"text\">%s</span>\n" + " </div>\n", + Media.Category[i].Marker, + CategoryMedium[j].Icon, + CategoryMedium[j].WrittenName + ); + } } CopyStringToBuffer(&FilterMedia, " </div>\n"); @@ -2732,48 +2912,65 @@ AppendedIdentifier: " </div>"); // TODO(matt): Maybe do something about indentation levels - // TODO(matt): We may need to do some actual logic here to figure out - // where the style paths are in relation to us, rather than assuming them - // to be one directory up the tree - if(Config.Edition == EDITION_PROJECT) - { - CopyStringToBuffer(&CollationBuffers->IncludesIndex, - "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s/cinera.css\">\n" - " <link rel=\"stylesheet\" type=\"text/css\" href=\"%s/cinera__%s.css\">\n" - " <link rel=\"stylesheet\" type=\"text/css\" href=\"%s/cinera_topics.css\">\n" - "\n" - " <meta charset=\"UTF-8\">\n" - " <meta name=\"generator\" content=\"Cinera\">\n", - Config.CSSDir, - Config.CSSDir, - HMML.metadata.project, - Config.CSSDir); - CopyStringToBuffer(&CollationBuffers->IncludesPlayer, - "<link rel=\"stylesheet\" type=\"text/css\" href=\"../%s/cinera.css\">\n" - " <link rel=\"stylesheet\" type=\"text/css\" href=\"../%s/cinera__%s.css\">\n" - " <link rel=\"stylesheet\" type=\"text/css\" href=\"../%s/cinera_topics.css\">\n", - Config.CSSDir, - Config.CSSDir, - HMML.metadata.project, - Config.CSSDir); + char URLPrefix[1024] = { 0 }; + char *Ptr = URLPrefix; + if(StringsDiffer(Config.RootURL, "")) + { + Ptr += CopyString(Ptr, "%s/", Config.RootURL); } else { - CopyStringToBuffer(&CollationBuffers->IncludesPlayer, - "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s/cinera.css\">\n" - " <link rel=\"stylesheet\" type=\"text/css\" href=\"%s/cinera__%s.css\">\n" - " <link rel=\"stylesheet\" type=\"text/css\" href=\"%s/cinera_topics.css\">\n", - Config.CSSDir, - Config.CSSDir, - HMML.metadata.project, - Config.CSSDir); + if(Config.Edition == EDITION_PROJECT) + { + Ptr += CopyString(Ptr, "../"); + } + } + if(StringsDiffer(Config.CSSDir, "")) + { + CopyString(Ptr, "%s/", Config.CSSDir); + } + + CopyStringToBuffer(&CollationBuffers->IncludesIndex, + "<link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera.css\">\n" + " <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera__%s.css\">\n" + " <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera_topics.css\">\n" + " <style>body { overflow-y: scroll; }</style>\n" + "\n" + " <meta charset=\"UTF-8\">\n" + " <meta name=\"generator\" content=\"Cinera %d.%d.%d\">\n", + URLPrefix, + URLPrefix, HMML.metadata.project, + URLPrefix, + + CINERA_APP_VERSION.Major, + CINERA_APP_VERSION.Minor, + CINERA_APP_VERSION.Patch); + + if(Config.Edition == EDITION_PROJECT) + { + if(!StringsDiffer(Config.RootURL, "")) + { + char Temp[1027]; + CopyString(Temp, "../%s", URLPrefix); + CopyString(URLPrefix, Temp); + } } CopyStringToBuffer(&CollationBuffers->IncludesPlayer, - "\n" + "<link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera.css\">\n" + " <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera__%s.css\">\n" + " <link rel=\"stylesheet\" type=\"text/css\" href=\"%scinera_topics.css\">\n" + "\n" " <meta charset=\"UTF-8\">\n" - " <meta name=\"generator\" content=\"Cinera\">\n"); + " <meta name=\"generator\" content=\"Cinera %d.%d.%d\">\n", + URLPrefix, + URLPrefix, HMML.metadata.project, + URLPrefix, + + CINERA_APP_VERSION.Major, + CINERA_APP_VERSION.Minor, + CINERA_APP_VERSION.Patch); if(Topics.Count || Media.Count) { @@ -2802,210 +2999,32 @@ AppendedIdentifier: if(Config.Edition == EDITION_PROJECT) { - CopyStringToBuffer(&CollationBuffers->IncludesPlayer, - " <script type=\"text/javascript\" src=\"../%s/cinera.js\"></script>\n", - Config.JSDir); - } - else - { - CopyStringToBuffer(&CollationBuffers->IncludesPlayer, - " <script type=\"text/javascript\" src=\"%s/cinera.js\"></script>\n", - Config.JSDir); + if(!StringsDiffer(Config.RootURL, "")) + { + Ptr = URLPrefix; + Ptr += StringLength("../../"); + Ptr += CopyString(Ptr, "%s", Config.JSDir); + if(StringsDiffer(Config.JSDir, "")) + { + Ptr[0] = '/'; + Ptr[1] = '\0'; + } + } } - CopyStringToBuffer(&CollationBuffers->Script, - " <script type=\"text/javascript\">\n" - "var menuState = [];\n"); + CopyStringToBuffer(&CollationBuffers->IncludesPlayer, + " <script type=\"text/javascript\" src=\"%scinera_player_pre.js\"></script>\n", + URLPrefix); - CopyStringToBuffer(&CollationBuffers->Script, - "var quotesMenu = document.querySelector(\".quotes_container\");\n"); - - if(HasQuoteMenu) - { - CopyStringToBuffer(&CollationBuffers->Script, - "if(quotesMenu)\n" - "{\n" - "menuState.push(quotesMenu);\n" - "var quoteItems = quotesMenu.querySelectorAll(\".ref\");\n" - "for(var i = 0; i < quoteItems.length; ++i)\n" - "{\n" - " quoteItems[i].addEventListener(\"mouseenter\", function(ev) {\n" - " mouseOverQuotes(this);\n" - " })\n" - "};\n" - "var lastFocusedQuote = null;\n" - "}\n"); - } - - CopyStringToBuffer(&CollationBuffers->Script, - "var referencesMenu = document.querySelector(\".references_container\");\n"); - - if(HasReferenceMenu) - { - CopyStringToBuffer(&CollationBuffers->Script, - "if(referencesMenu)\n" - "{\n" - "menuState.push(referencesMenu);\n" - "var referenceItems = referencesMenu.querySelectorAll(\".ref\");\n" - "for(var i = 0; i < referenceItems.length; ++i)\n" - "{\n" - " referenceItems[i].addEventListener(\"mouseenter\", function(ev) {\n" - " mouseOverReferences(this);\n" - " })\n" - "};\n" - "var lastFocusedReference = null;\n" - "var lastFocusedIdentifier = null;\n" - "}\n"); - } - - CopyStringToBuffer(&CollationBuffers->Script, - "var filterMenu = document.querySelector(\".filter_container\");\n"); + CopyStringToBuffer(&CollationBuffers->ScriptPlayer, + " <script type=\"text/javascript\" src=\"%scinera_player_post.js\"></script>\n", + URLPrefix); if(HasFilterMenu) { - CopyStringToBuffer(&CollationBuffers->Script, - "if(filterMenu)\n" - "{\n" - " menuState.push(filterMenu);\n" - " var lastFocusedCategory = null;\n" - " var lastFocusedTopic = null;\n" - " var lastFocusedMedium = null;\n" - "\n" - " var filter = filterMenu.parentNode;\n" - "\n" - " var filterModeElement = filter.querySelector(\".filter_mode\");\n" - " filterModeElement.addEventListener(\"click\", function(ev) {\n" - " toggleFilterMode();\n" - " });\n" - "\n" - " var filterMode = filterModeElement.classList[1];\n" - " var filterItems = filter.querySelectorAll(\".filter_content\");\n" - " for(var i = 0; i < filterItems.length; ++i)\n" - " {\n" - " filterItems[i].addEventListener(\"mouseenter\", function(ev) {\n" - " navigateFilter(this);\n" - " })\n" - "\n" - " filterItems[i].addEventListener(\"click\", function(ev) {\n" - " filterItemToggle(this);\n" - " });\n" - "\n" - "%s" - " }\n" - "}\n", FilterState.Location); + CopyBuffer(&CollationBuffers->ScriptPlayer, &FilterState); } - CopyStringToBuffer(&CollationBuffers->Script, - "var creditsMenu = document.querySelector(\".credits_container\");\n"); - - if(HasCreditsMenu) - { - CopyStringToBuffer(&CollationBuffers->Script, - "if(creditsMenu)\n" - "{\n" - " menuState.push(creditsMenu);\n" - " var lastFocusedCreditItem = null;\n" - "\n" - " var creditItems = creditsMenu.querySelectorAll(\".person, .support\");\n" - " for(var i = 0; i < creditItems.length; ++i)\n" - " {\n" - " creditItems[i].addEventListener(\"mouseenter\", function(ev) {\n" - " if(this != lastFocusedCreditItem)\n" - " {\n" - " lastFocusedCreditItem.classList.remove(\"focused\");\n" - " lastFocusedCreditItem = this;\n" - " focusedElement = lastFocusedCreditItem;\n" - " focusedElement.classList.add(\"focused\");\n" - " }\n" - " })\n" - " }\n" - "}\n"); - } - - CopyStringToBuffer(&CollationBuffers->Script, - "var sourceMenus = document.querySelectorAll(\".menu\");\n" - "\n" - "var helpButton = document.querySelector(\".help\");\n" - "var helpDocumentation = helpButton.querySelector(\".help_container\");\n" - "helpButton.addEventListener(\"click\", function(ev) {\n" - " handleMouseOverMenu(this, ev.type);\n" - "})\n" - "\n" - "var focusedElement = null;\n" - "var focusedIdentifier = null;\n" - "\n" - "var playerContainer = document.querySelector(\".player_container\")\n" - "var player = new Player(playerContainer, onRefChanged);\n" - "window.addEventListener(\"resize\", function() { player.updateSize(); });\n" - "document.addEventListener(\"keydown\", function(ev) {\n" - " var key = ev.key;\n" - " if(ev.getModifierState(\"Shift\") && key == \" \")\n" - " {\n" - " key = \"capitalSpace\";\n" - " }\n" - "\n" - " if(handleKey(key) == true && focusedElement)\n" - " {\n" - " ev.preventDefault();\n" - " }\n" - "});\n" - "\n" - "for(var i = 0; i < sourceMenus.length; ++i)\n" - "{\n" - " sourceMenus[i].addEventListener(\"mouseenter\", function(ev) {\n" - " handleMouseOverMenu(this, ev.type);\n" - " })\n" - " sourceMenus[i].addEventListener(\"mouseleave\", function(ev) {\n" - " handleMouseOverMenu(this, ev.type);\n" - " })\n" - "};\n" - "\n" - "var refTimecodes = document.querySelectorAll(\".refs .ref .timecode\");\n" - "for (var i = 0; i < refTimecodes.length; ++i) {\n" - " refTimecodes[i].addEventListener(\"click\", function(ev) {\n" - " if (player) {\n" - " var time = ev.currentTarget.getAttribute(\"data-timestamp\");\n" - " mouseSkipToTimecode(player, time, ev);\n" - " }\n" - " });\n" - "}\n" - "\n" - "var refSources = document.querySelectorAll(\".refs .ref\"); // This is for both quotes and refs\n" - "for (var i = 0; i < refSources.length; ++i) {\n" - " refSources[i].addEventListener(\"click\", function(ev) {\n" - " if (player) {\n" - " player.pause();\n" - " }\n" - " });\n" - "}\n" - "\n" - "var testMarkers = playerContainer.querySelectorAll(\".marker\");\n" - "\n" - "window.addEventListener(\"blur\", function(){\n" - " document.getElementById(\"focus-warn\").style.display = \"block\";\n" - "});\n" - "\n" - "window.addEventListener(\"focus\", function(){\n" - " document.getElementById(\"focus-warn\").style.display = \"none\";\n" - "});\n" - "\n" - "var colouredItems = playerContainer.querySelectorAll(\".author, .member, .project\");\n" - "for(i = 0; i < colouredItems.length; ++i)\n" - "{\n" - " setTextLightness(colouredItems[i]);\n" - "}\n" - "\n" - "var topicDots = document.querySelectorAll(\".category\");\n" - "for(var i = 0; i < topicDots.length; ++i)\n" - "{\n" - " setDotLightness(topicDots[i]);\n" - "}\n" - "\n" - "if(location.hash) {\n" - " player.setTime(location.hash.startsWith('#') ? location.hash.substr(1) : location.hash);\n" - "}\n" - " </script>"); - // NOTE(matt): Tree structure of "global" buffer dependencies // FilterState // CreditsMenu @@ -3065,10 +3084,10 @@ BuffersToHTML(buffers *CollationBuffers, template *Template, char *OutputPath, i } #if DEBUG_MEM - MemLog = fopen("/home/matt/cinera_mem", "a+"); - fprintf(MemLog, " Allocated Output (%d)\n", Output.Size); - fclose(MemLog); - printf(" Allocated Output (%d)\n", Output.Size); + MemLog = fopen("/home/matt/cinera_mem", "a+"); + fprintf(MemLog, " Allocated Output (%d)\n", Output.Size); + fclose(MemLog); + printf(" Allocated Output (%d)\n", Output.Size); #endif Output.Ptr = Output.Location; @@ -3101,7 +3120,7 @@ BuffersToHTML(buffers *CollationBuffers, template *Template, char *OutputPath, i CopyBuffer(&Output, &CollationBuffers->Player); break; case TAG_SCRIPT: - CopyBuffer(&Output, &CollationBuffers->Script); + CopyBuffer(&Output, &CollationBuffers->ScriptPlayer); break; } DepartComment(&Template->Buffer); @@ -3118,10 +3137,10 @@ BuffersToHTML(buffers *CollationBuffers, template *Template, char *OutputPath, i free(Output.Location); #if DEBUG_MEM - MemLog = fopen("/home/matt/cinera_mem", "a+"); - fprintf(MemLog, " Freed Output\n"); - fclose(MemLog); - printf(" Freed Output\n"); + MemLog = fopen("/home/matt/cinera_mem", "a+"); + fprintf(MemLog, " Freed Output\n"); + fclose(MemLog); + printf(" Freed Output\n"); #endif return RC_ERROR_FILE; @@ -3132,10 +3151,10 @@ BuffersToHTML(buffers *CollationBuffers, template *Template, char *OutputPath, i free(Output.Location); #if DEBUG_MEM - MemLog = fopen("/home/matt/cinera_mem", "a+"); - fprintf(MemLog, " Freed Output\n"); - fclose(MemLog); - printf(" Freed Output\n"); + MemLog = fopen("/home/matt/cinera_mem", "a+"); + fprintf(MemLog, " Freed Output\n"); + fclose(MemLog); + printf(" Freed Output\n"); #endif return RC_SUCCESS; @@ -3166,7 +3185,7 @@ BuffersToHTML(buffers *CollationBuffers, template *Template, char *OutputPath, i CopyStringToBuffer(&Master, "\n"); CopyBuffer(&Master, &CollationBuffers->Player); CopyStringToBuffer(&Master, "\n"); - CopyBuffer(&Master, &CollationBuffers->Script); + CopyBuffer(&Master, &CollationBuffers->ScriptPlayer); CopyStringToBuffer(&Master, "\n"); } else @@ -3236,14 +3255,134 @@ WriteBufferToFile(file_buffer *File, buffer *Buffer, int BytesToWrite, bool Keep return RC_SUCCESS; } +enum +{ + C_SEEK_FORWARDS, + C_SEEK_BACKWARDS +} seek_directions; + +enum +{ + C_SEEK_START, // First character of string + C_SEEK_BEFORE, // Character before first character + C_SEEK_END, // Last character of string + C_SEEK_AFTER // Character after last character +} seek_positions; + +int +SeekBufferForString(buffer *Buffer, char *String, + int Direction, /* seek_directions */ + int Position /* seek_positions */) +{ + // TODO(matt): Optimise? Some means of analysing the String to increment + // the pointer in bigger strides + + // Perhaps count up runs of consecutive chars and seek for the char with + // the longest run, in strides of that run-length + + char *InitialLocation = Buffer->Ptr; + if(Direction == C_SEEK_FORWARDS) + { + while(Buffer->Ptr - Buffer->Location < Buffer->Size - StringLength(String) + && StringsDifferT(String, Buffer->Ptr, 0)) + { + ++Buffer->Ptr; + } + } + else + { + while(Buffer->Ptr > Buffer->Location + && StringsDifferT(String, Buffer->Ptr, 0)) + { + --Buffer->Ptr; + } + } + + if(StringsDifferT(String, Buffer->Ptr, 0)) + { + Buffer->Ptr = InitialLocation; + return RC_UNFOUND; + } + + switch(Position) + { + case C_SEEK_START: + break; + case C_SEEK_BEFORE: + if(Buffer->Ptr > Buffer->Location) + { + --Buffer->Ptr; + break; + } + else + { + return RC_ERROR_SEEK; // Ptr remains at string start + } + case C_SEEK_END: + Buffer->Ptr += StringLength(String) - 1; + break; + case C_SEEK_AFTER: + if(Buffer->Size >= Buffer->Ptr - Buffer->Location + StringLength(String)) + { + Buffer->Ptr += StringLength(String); + break; + } + else + { + return RC_ERROR_SEEK; // Ptr remains at string start + // NOTE(matt): Should it, however, be left at the end of the string? + } + } + return RC_SUCCESS; +} + +// TODO(matt): Increment CINERA_DB_VERSION! +typedef struct +{ + unsigned int DBVersion; // NOTE(matt): Put this first to aid reliability + version AppVersion; + version HMMLVersion; + unsigned int EntryCount; +} index_header; + +typedef struct +{ + int Size; + char BaseFilename[32]; +} index_metadata; + +typedef struct +{ + file_buffer File; + file_buffer Metadata; + index_header Header; + index_metadata Entry; +} index; +// TODO(matt): Increment CINERA_DB_VERSION! + int InsertIntoIndex(buffers *CollationBuffers, char *BaseFilename) { - file_buffer Index; - Index.Buffer.ID = "Index"; - CopyString(Index.Path, "%s/%s.index", Config.CacheDir, Config.ProjectID); - int FileReadCode = ReadFileIntoBuffer(&Index, 0); - switch(FileReadCode) + // NOTE(matt): The index will be stored in two files: + // 1) ProjectID.index + // 2) ProjectID.metadata + // + // .index is all of the stuff needed for the search + // .metadata is: int DBVersion + // version AppVersion + // version HMMLVersion + // int EntryCount + // for each Entry + // int Size from start of "name:" to end of "---\n") + // char BaseFilename[32] (0-padded) + // + + + index Index = { 0 }; + Index.Metadata.Buffer.ID = "IndexMetadata"; + CopyString(Index.Metadata.Path, "%s/%s.metadata", Config.BaseDir, Config.ProjectID); + int IndexMetadataFileReadCode = ReadFileIntoBuffer(&Index.Metadata, 0); + switch(IndexMetadataFileReadCode) { case RC_ERROR_MEMORY: return RC_ERROR_MEMORY; @@ -3252,116 +3391,164 @@ InsertIntoIndex(buffers *CollationBuffers, char *BaseFilename) break; } - int EntryInsertionOffset = -1; - int TitleInsertionOffset = -1; - int TitleInsertionEnd = -1; - int EntryCount = 0; + Index.File.Buffer.ID = "Index"; + CopyString(Index.File.Path, "%s/%s.index", Config.BaseDir, Config.ProjectID); + int IndexFileReadCode = ReadFileIntoBuffer(&Index.File, 0); + switch(IndexFileReadCode) + { + case RC_ERROR_MEMORY: + return RC_ERROR_MEMORY; + case RC_ERROR_FILE: + case RC_SUCCESS: + break; + } - char InputFile[255]; + int MetadataInsertionOffset = -1; + int IndexEntryInsertionStart = -1; + int IndexEntryInsertionEnd = -1; + Index.Header.EntryCount = 0; + char *IndexEntryStart; + bool Found = FALSE; + + char InputFile[StringLength(BaseFilename) + StringLength(".hmml")]; CopyString(InputFile, "%s.hmml", BaseFilename); HMMLToBuffers(CollationBuffers, InputFile); - if(FileReadCode == RC_SUCCESS) + Index.Entry.Size = CollationBuffers->Search.Ptr - CollationBuffers->Search.Location; + for(int i = 0; i < ArrayCount(Index.Entry.BaseFilename); ++i) { - EntryCount = *(int *)Index.Buffer.Ptr; - Index.Buffer.Ptr += sizeof(int); + Index.Entry.BaseFilename[i] = '\0'; + } + CopyString(Index.Entry.BaseFilename, BaseFilename); - while(Index.Buffer.Ptr - Index.Buffer.Location < Index.FileSize) + int EntryIndex; + if(IndexMetadataFileReadCode == RC_SUCCESS && IndexFileReadCode == RC_SUCCESS) + { + // TODO(matt): Index validation? + // Maybe at least if(!StringsDiffer(..., "name: \""); + // and check that we won't crash through the end of the file when skipping to the next entry + + Index.Header = *(index_header *)Index.Metadata.Buffer.Ptr; + Index.Metadata.Buffer.Ptr += sizeof(Index.Header); + Index.File.Buffer.Ptr += StringLength("---\n"); + IndexEntryStart = Index.File.Buffer.Ptr; + + for(EntryIndex = 0; EntryIndex < Index.Header.EntryCount; ++EntryIndex) { - char IndexedFile[32]; - char *Ptr = IndexedFile; - Index.Buffer.Ptr += CopyStringNoFormatT(Ptr, Index.Buffer.Ptr, ',') + 1; - if(!StringsDiffer(IndexedFile, BaseFilename)) + index_metadata This = *(index_metadata *)Index.Metadata.Buffer.Ptr; + if(!StringsDiffer(This.BaseFilename, BaseFilename)) { - if(!StringsDifferT(CollationBuffers->Title, Index.Buffer.Ptr, '\n')) - { - FreeBuffer(&Index.Buffer); - return RC_NOOP; - } - else - { - TitleInsertionOffset = Index.Buffer.Ptr - Index.Buffer.Location; - while(*Index.Buffer.Ptr != '\n' && Index.Buffer.Ptr - Index.Buffer.Location < Index.FileSize) - { - ++Index.Buffer.Ptr; - } - ++Index.Buffer.Ptr; - TitleInsertionEnd = Index.Buffer.Ptr - Index.Buffer.Location; - break; - } + // Reinsert + MetadataInsertionOffset = Index.Metadata.Buffer.Ptr - Index.Metadata.Buffer.Location; + IndexEntryInsertionStart = IndexEntryStart - Index.File.Buffer.Location; + IndexEntryInsertionEnd = IndexEntryInsertionStart + Index.Entry.Size; + Found = TRUE; + break; + } - else if(StringsDiffer(IndexedFile, BaseFilename) > 0) + else if(StringsDiffer(This.BaseFilename, BaseFilename) > 0) { - while(*Index.Buffer.Ptr != '\n' && Index.Buffer.Ptr > Index.Buffer.Location + sizeof(int)) - { - --Index.Buffer.Ptr; - } - if(Index.Buffer.Ptr > Index.Buffer.Location + sizeof(int)) - { - ++Index.Buffer.Ptr; - } - EntryInsertionOffset = Index.Buffer.Ptr - Index.Buffer.Location; + // Insert + MetadataInsertionOffset = Index.Metadata.Buffer.Ptr - Index.Metadata.Buffer.Location; + IndexEntryInsertionStart = IndexEntryStart - Index.File.Buffer.Location; break; } else { - while(*Index.Buffer.Ptr != '\n' && Index.Buffer.Ptr - Index.Buffer.Location < Index.FileSize) - { - ++Index.Buffer.Ptr; - } - ++Index.Buffer.Ptr; + Index.Metadata.Buffer.Ptr += sizeof(Index.Entry); + + IndexEntryStart += This.Size; + Index.File.Buffer.Ptr = IndexEntryStart; } } } - - if(!(Index.Handle = fopen(Index.Path, "w"))) { FreeBuffer(&Index.Buffer); return RC_ERROR_FILE; } - - if(FileReadCode == RC_ERROR_FILE) + else { - Index.Buffer.Size = sizeof(int); - if(!(Index.Buffer.Location = malloc(Index.Buffer.Size))) + // NOTE(matt): Initialising new index_header + Index.Header.DBVersion = CINERA_DB_VERSION; + Index.Header.AppVersion = CINERA_APP_VERSION; + Index.Header.HMMLVersion.Major = hmml_version.Major; + Index.Header.HMMLVersion.Minor = hmml_version.Minor; + Index.Header.HMMLVersion.Patch = hmml_version.Patch; + DIR *OutputDirectoryHandle; + if(!(OutputDirectoryHandle = opendir(Config.BaseDir))) { - fclose(Index.Handle); - return RC_ERROR_MEMORY; + if(MakeDir(Config.BaseDir) == RC_ERROR_DIRECTORY) + { + LogError(LOG_ERROR, "Unable to create directory %s: %s", Config.BaseDir, strerror(errno)); + fprintf(stderr, "Unable to create directory %s: %s\n", Config.BaseDir, strerror(errno)); + return RC_ERROR_DIRECTORY; + }; } + closedir(OutputDirectoryHandle); } - if(TitleInsertionOffset < 0) { EntryCount++; } + if(!(Index.Metadata.Handle = fopen(Index.Metadata.Path, "w"))) { FreeBuffer(&Index.Metadata.Buffer); return RC_ERROR_FILE; } + if(!(Index.File.Handle = fopen(Index.File.Path, "w"))) { FreeBuffer(&Index.File.Buffer); return RC_ERROR_FILE; } - Index.Buffer.Ptr = Index.Buffer.Location; - *(int *)Index.Buffer.Ptr = EntryCount; - fwrite(Index.Buffer.Ptr, sizeof(int), 1, Index.Handle); - Index.Buffer.Ptr += sizeof(int); + if(!Found) { ++Index.Header.EntryCount; } - if(EntryInsertionOffset >= 0) + fwrite(&Index.Header, sizeof(Index.Header), 1, Index.Metadata.Handle); + + if(IndexMetadataFileReadCode == RC_SUCCESS) { - fwrite(Index.Buffer.Ptr, EntryInsertionOffset - sizeof(int), 1, Index.Handle); - fprintf(Index.Handle, "%s,%s\n", BaseFilename, CollationBuffers->Title); - fwrite(Index.Buffer.Ptr + EntryInsertionOffset - sizeof(int), Index.FileSize - EntryInsertionOffset, 1, Index.Handle); + Index.Metadata.Buffer.Ptr = Index.Metadata.Buffer.Location + sizeof(Index.Header); + } + + if(Found) + { + // NOTE(matt): We hit this during the start-up sync and when copying in a .hmml file over an already existing one, but + // would need to fool about with the inotify event processing to get to this branch in the case that saving + // a file triggers an IN_DELETE followed by an IN_CLOSE_WRITE event + + // Reinsert + fwrite(Index.Metadata.Buffer.Ptr, MetadataInsertionOffset - sizeof(Index.Header), 1, Index.Metadata.Handle); + fwrite(&Index.Entry, sizeof(Index.Entry), 1, Index.Metadata.Handle); + fwrite(Index.Metadata.Buffer.Ptr - sizeof(Index.Header) + MetadataInsertionOffset + sizeof(Index.Entry), Index.Metadata.FileSize - MetadataInsertionOffset - sizeof(Index.Entry), 1, Index.Metadata.Handle); + + fwrite(Index.File.Buffer.Location, IndexEntryInsertionStart, 1, Index.File.Handle); + fwrite(CollationBuffers->Search.Location, Index.Entry.Size, 1, Index.File.Handle); + fwrite(Index.File.Buffer.Location + IndexEntryInsertionEnd, Index.File.FileSize - IndexEntryInsertionEnd, 1, Index.File.Handle); + + LogError(LOG_NOTICE, "Reinserted %s - %s", BaseFilename, CollationBuffers->Title); + fprintf(stderr, "\e[1;33mReinserted\e[0m %s - %s\n", BaseFilename, CollationBuffers->Title); + } + else if(MetadataInsertionOffset >= 0 && IndexEntryInsertionStart >= 0) + { + // Insert new + fwrite(Index.Metadata.Buffer.Ptr, MetadataInsertionOffset - sizeof(Index.Header), 1, Index.Metadata.Handle); + fwrite(&Index.Entry, sizeof(Index.Entry), 1, Index.Metadata.Handle); + fwrite(Index.Metadata.Buffer.Ptr - sizeof(Index.Header) + MetadataInsertionOffset, Index.Metadata.FileSize - MetadataInsertionOffset, 1, Index.Metadata.Handle); + + fwrite(Index.File.Buffer.Location, IndexEntryInsertionStart, 1, Index.File.Handle); + fwrite(CollationBuffers->Search.Location, Index.Entry.Size, 1, Index.File.Handle); + fwrite(Index.File.Buffer.Location + IndexEntryInsertionStart, Index.File.FileSize - IndexEntryInsertionStart, 1, Index.File.Handle); + LogError(LOG_NOTICE, "Inserted %s - %s", BaseFilename, CollationBuffers->Title); - fprintf(stderr, "Inserted %s - %s\n", BaseFilename, CollationBuffers->Title); - } - else if(TitleInsertionOffset >= 0) - { - fwrite(Index.Buffer.Ptr, TitleInsertionOffset - sizeof(int), 1, Index.Handle); - fprintf(Index.Handle, "%s\n", CollationBuffers->Title); - fwrite(Index.Buffer.Ptr + TitleInsertionEnd - sizeof(int), Index.FileSize - TitleInsertionEnd, 1, Index.Handle); - LogError(LOG_NOTICE, "Edited %s - %s", BaseFilename, CollationBuffers->Title); - fprintf(stderr, "Edited %s - %s\n", BaseFilename, CollationBuffers->Title); + fprintf(stderr, "\e[1;32mInserted\e[0m %s - %s\n", BaseFilename, CollationBuffers->Title); } else { - if(FileReadCode == RC_SUCCESS) + // Append new + if(IndexMetadataFileReadCode == RC_SUCCESS) { - fwrite(Index.Buffer.Ptr, Index.FileSize - sizeof(int), 1, Index.Handle); // Write existing stuff back out + fwrite(Index.Metadata.Buffer.Ptr, Index.Metadata.FileSize - sizeof(Index.Header), 1, Index.Metadata.Handle); + fwrite(Index.File.Buffer.Location, Index.File.FileSize, 1, Index.File.Handle); } - fprintf(Index.Handle, "%s,%s\n", BaseFilename, CollationBuffers->Title); - LogError(LOG_NOTICE, "Inserted %s - %s", BaseFilename, CollationBuffers->Title); - fprintf(stderr, "Inserted %s - %s\n", BaseFilename, CollationBuffers->Title); + else + { + fprintf(Index.File.Handle, "---\n"); + } + fwrite(&Index.Entry, sizeof(Index.Entry), 1, Index.Metadata.Handle); + fwrite(CollationBuffers->Search.Location, Index.Entry.Size, 1, Index.File.Handle); + LogError(LOG_NOTICE, "Appended %s - %s", BaseFilename, CollationBuffers->Title); + fprintf(stderr, "\e[1;32mAppended\e[0m %s - %s\n", BaseFilename, CollationBuffers->Title); } - fclose(Index.Handle); - FreeBuffer(&Index.Buffer); + fclose(Index.Metadata.Handle); + fclose(Index.File.Handle); + FreeBuffer(&Index.Metadata.Buffer); + FreeBuffer(&Index.File.Buffer); return RC_SUCCESS; } @@ -3369,70 +3556,85 @@ int DeleteFromIndex(char *BaseFilename) { // TODO(matt): LogError() - file_buffer Index; - Index.Buffer.ID = "Index"; - CopyString(Index.Path, "%s/%s.index", Config.CacheDir, Config.ProjectID); + index Index; - switch(ReadFileIntoBuffer(&Index, 0)) + Index.Metadata.Buffer.ID = "IndexMetadata"; + CopyString(Index.Metadata.Path, "%s/%s.metadata", Config.BaseDir, Config.ProjectID); + switch(ReadFileIntoBuffer(&Index.Metadata, 0)) { case RC_ERROR_FILE: return RC_ERROR_FILE; - break; case RC_ERROR_MEMORY: LogError(LOG_ERROR, "DeleteFromIndex(): %s", strerror(errno)); return RC_ERROR_MEMORY; - break; case RC_SUCCESS: break; } - int EntryCount = *(int *)Index.Buffer.Ptr; - Index.Buffer.Ptr += sizeof(int); + Index.File.Buffer.ID = "Index"; + CopyString(Index.File.Path, "%s/%s.index", Config.BaseDir, Config.ProjectID); + switch(ReadFileIntoBuffer(&Index.File, 0)) + { + case RC_ERROR_FILE: + return RC_ERROR_FILE; + case RC_ERROR_MEMORY: + LogError(LOG_ERROR, "DeleteFromIndex(): %s", strerror(errno)); + return RC_ERROR_MEMORY; + case RC_SUCCESS: + break; + } + + Index.Header = *(index_header *)Index.Metadata.Buffer.Ptr; + Index.Metadata.Buffer.Ptr += sizeof(Index.Header); bool Found = FALSE; - int DeleteFrom = -1; - int DeleteTo = -1; + int DeleteMetadataFrom = -1; + int DeleteFileFrom = -1; + int DeleteFileTo = -1; + int SizeAcc = 0; - while(Index.Buffer.Ptr - Index.Buffer.Location < Index.FileSize) + for(int EntryIndex = 0; EntryIndex < Index.Header.EntryCount; ++EntryIndex, Index.Metadata.Buffer.Ptr += sizeof(index_metadata)) { - if(!StringsDifferT(BaseFilename, Index.Buffer.Ptr, ',')) + index_metadata This = *(index_metadata *)Index.Metadata.Buffer.Ptr; + if(!StringsDiffer(This.BaseFilename, BaseFilename)) { Found = TRUE; - --EntryCount; - DeleteFrom = Index.Buffer.Ptr - Index.Buffer.Location; - while(*Index.Buffer.Ptr != '\n' && Index.Buffer.Ptr - Index.Buffer.Location < Index.FileSize) - { - ++Index.Buffer.Ptr; - } - ++Index.Buffer.Ptr; - DeleteTo = Index.Buffer.Ptr - Index.Buffer.Location; + --Index.Header.EntryCount; + DeleteMetadataFrom = Index.Metadata.Buffer.Ptr - Index.Metadata.Buffer.Location; + DeleteFileFrom = StringLength("---\n") + SizeAcc; + DeleteFileTo = DeleteFileFrom + This.Size; break; } - ++Index.Buffer.Ptr; + SizeAcc += This.Size; } if(Found) { - if(EntryCount == 0) + if(Index.Header.EntryCount == 0) { - remove(Index.Path); + remove(Index.Metadata.Path); + remove(Index.File.Path); } else { - if(!(Index.Handle = fopen(Index.Path, "w"))) - { - free(Index.Buffer.Location); - return RC_ERROR_FILE; - } - Index.Buffer.Ptr = Index.Buffer.Location; - *(int *)Index.Buffer.Ptr = EntryCount; - fwrite(Index.Buffer.Ptr, DeleteFrom, 1, Index.Handle); - fwrite(Index.Buffer.Ptr + DeleteTo, Index.FileSize - DeleteTo, 1, Index.Handle); - fclose(Index.Handle); + if(!(Index.Metadata.Handle = fopen(Index.Metadata.Path, "w"))) { FreeBuffer(&Index.Metadata.Buffer); return RC_ERROR_FILE; } + if(!(Index.File.Handle = fopen(Index.File.Path, "w"))) { FreeBuffer(&Index.File.Buffer); return RC_ERROR_FILE; } + + fwrite(&Index.Header, sizeof(Index.Header), 1, Index.Metadata.Handle); + Index.Metadata.Buffer.Ptr = Index.Metadata.Buffer.Location + sizeof(Index.Header); + + fwrite(Index.Metadata.Buffer.Ptr, DeleteMetadataFrom - sizeof(Index.Header), 1, Index.Metadata.Handle); + fwrite(Index.Metadata.Buffer.Ptr + DeleteMetadataFrom - sizeof(Index.Header) + sizeof(Index.Entry), Index.Metadata.FileSize - DeleteMetadataFrom - sizeof(Index.Entry), 1, Index.Metadata.Handle); + fclose(Index.Metadata.Handle); + + fwrite(Index.File.Buffer.Location, DeleteFileFrom, 1, Index.File.Handle); + fwrite(Index.File.Buffer.Location + DeleteFileTo, Index.File.FileSize - DeleteFileTo, 1, Index.File.Handle); + fclose(Index.File.Handle); } } - free(Index.Buffer.Location); + FreeBuffer(&Index.Metadata.Buffer); + FreeBuffer(&Index.File.Buffer); return Found ? RC_SUCCESS : RC_NOOP; } @@ -3443,44 +3645,129 @@ IndexToBuffer(buffers *CollationBuffers) // TODO(matt): Consider parsing the index into a linked list, or do something to save us having to iterate through the index // file multiple times - file_buffer Index; - Index.Buffer.ID = "Index"; - CopyString(Index.Path, "%s/%s.index", Config.CacheDir, Config.ProjectID); - ReadFileIntoBuffer(&Index, 0); + index Index; + Index.Metadata.Buffer.ID = "IndexMetadata"; + CopyString(Index.Metadata.Path, "%s/%s.metadata", Config.BaseDir, Config.ProjectID); + int IndexMetadataFileReadCode = ReadFileIntoBuffer(&Index.Metadata, 0); - //int EntryCount = *(int *)Index.Buffer.Ptr; - Index.Buffer.Ptr += sizeof(int); + Index.File.Buffer.ID = "Index"; + CopyString(Index.File.Path, "%s/%s.index", Config.BaseDir, Config.ProjectID); + int IndexFileReadCode = ReadFileIntoBuffer(&Index.File, 0); - CopyStringToBuffer(&CollationBuffers->Index, "<dl>\n"); - while(Index.Buffer.Ptr - Index.Buffer.Location < Index.FileSize) + if(IndexMetadataFileReadCode == RC_SUCCESS && IndexFileReadCode == RC_SUCCESS) { - char IndexedFile[32]; - char *Ptr = IndexedFile; - Index.Buffer.Ptr += CopyStringNoFormatT(Ptr, Index.Buffer.Ptr, ',') + 1; - - char Title[255]; - Ptr = Title; - Index.Buffer.Ptr += CopyStringNoFormatT(Ptr, Index.Buffer.Ptr, '\n') + 1; + Index.Header = *(index_header*)Index.Metadata.Buffer.Ptr; + Index.Metadata.Buffer.Ptr += sizeof(Index.Header); + Index.File.Buffer.Ptr += StringLength("---\n"); + char *IndexEntryStart = Index.File.Buffer.Ptr; CopyStringToBuffer(&CollationBuffers->Index, - " <dt>\n" - " <a href=\"%s\">%s</a>\n" - " </dt>\n", - IndexedFile, Title); + "<div class=\"queryContainer\">\n" + " <label for=\"query\">Query:</label>\n" + " <div class=\"inputContainer\">\n" + " <input type=\"text\" id=\"query\" autofocus=\"\">\n" + " <div class=\"spinner\">\n" + " Downloading data...\n" + " </div>\n" + " </div>\n" + " </div>\n" + " <div id=\"resultsSummary\">Found: 0 episodes, 0 markers, 0h 0m 0s total.</div>\n" + " <div id=\"results\"></div>\n" + "\n" + " <dl id=\"cineraIndex\" class=\"riscy\">\n"); + bool ProjectFound = FALSE; + int ProjectIndex; + for(ProjectIndex = 0; ProjectIndex < ArrayCount(ProjectInfo); ++ProjectIndex) + { + if(!StringsDiffer(ProjectInfo[ProjectIndex].ProjectID, Config.ProjectID)) + { + ProjectFound = TRUE; + break; + } + } + + if(!ProjectFound) + { + fprintf(stderr, "Missing Project Info for %s\n", Config.ProjectID); + return RC_ERROR_PROJECT; + } + + for(int EntryIndex = 0; EntryIndex < Index.Header.EntryCount; ++EntryIndex) + { + index_metadata This = *(index_metadata *)Index.Metadata.Buffer.Ptr; + char Number[16]; + CopyString(Number, This.BaseFilename + StringLength(Config.ProjectID)); + if(ProjectInfo[ProjectIndex].NumberingScheme == NS_LINEAR) + { + for(int i = 0; Number[i]; ++i) + { + if(Number[i] == '_') + { + Number[i] = '.'; + } + } + } + + SeekBufferForString(&Index.File.Buffer, "title: \"", C_SEEK_FORWARDS, C_SEEK_AFTER); + char Title[256]; + CopyStringNoFormatT(Title, Index.File.Buffer.Ptr, '\n'); + Title[StringLength(Title) - 1] = '\0'; + + CopyStringToBuffer(&CollationBuffers->Index, + " <dt>\n" + " <a href=\"%s\">%s %s: %s</a>\n" + " </dt>\n", + This.BaseFilename, + ProjectInfo[ProjectIndex].Unit, // TODO(matt): Do we need to special-case the various numbering schemes? + Number, + Title); + + Index.Metadata.Buffer.Ptr += sizeof(Index.Entry); + IndexEntryStart += This.Size; + Index.File.Buffer.Ptr = IndexEntryStart; + } + + char URLPrefix[1024] = { 0 }; + char *Ptr = URLPrefix; + if(StringsDiffer(Config.RootURL, "")) + { + Ptr += CopyString(Ptr, "%s/", Config.RootURL); + } + else + { + Ptr += CopyString(Ptr, "../"); + } + if(StringsDiffer(Config.JSDir, "")) + { + Ptr += CopyString(Ptr, "%s/", Config.JSDir); + } + + CopyStringToBuffer(&CollationBuffers->Index, " </dl>\n" + " <script type=\"text/javascript\">\n" + " var indexLocation = \"%s.index\";\n" + " var projectID = \"%s\";\n" + " </script>\n" + " <script type=\"text/javascript\" src=\"%scinera_search.js\"></script>\n", + Config.ProjectID, + Config.ProjectID, + URLPrefix); + + FreeBuffer(&Index.Metadata.Buffer); + return RC_SUCCESS; + } + else + { + return RC_ERROR_FILE; } - CopyStringToBuffer(&CollationBuffers->Index, " </dl>"); - - FreeBuffer(&Index.Buffer); - return RC_SUCCESS; } int GeneratePlayerPage(buffers *CollationBuffers, template *PlayerTemplate, char *BaseFilename) { - char OutputDir[255]; + char OutputDir[256]; CopyString(OutputDir, "%s/%s", Config.BaseDir, BaseFilename); - char PlayerPagePath[255]; + char PlayerPagePath[256]; CopyString(PlayerPagePath, "%s/index.html", OutputDir); DIR *OutputDirectoryHandle; @@ -3502,7 +3789,7 @@ GeneratePlayerPage(buffers *CollationBuffers, template *PlayerTemplate, char *Ba int GenerateIndexPage(buffers *CollationBuffers, template *IndexTemplate) { - char IndexPagePath[255]; + char IndexPagePath[256]; CopyString(IndexPagePath, "%s/index.html", Config.BaseDir); IndexToBuffer(CollationBuffers); BuffersToHTML(CollationBuffers, IndexTemplate, IndexPagePath, PAGE_INDEX); @@ -3513,13 +3800,13 @@ int DeletePlayerPageFromFilesystem(char *BaseFilename) { // NOTE(matt): Once we have the notion of an output filename format, we'll need to use that here - char PlayerDirPath[255]; + char PlayerDirPath[256]; CopyString(PlayerDirPath, "%s/%s", Config.BaseDir, BaseFilename); DIR *PlayerDir; if((PlayerDir = opendir(PlayerDirPath))) // There is a directory for the Player, which there probably should be if not for manual intervention { - char PlayerPagePath[255]; + char PlayerPagePath[256]; CopyString(PlayerPagePath, "%s/index.html", PlayerDirPath); FILE *PlayerPage; if((PlayerPage = fopen(PlayerPagePath, "r"))) @@ -3531,13 +3818,13 @@ DeletePlayerPageFromFilesystem(char *BaseFilename) closedir(PlayerDir); if((remove(PlayerDirPath) == -1)) { - LogError(LOG_NOTICE, "Mostly removed %s. Unable to remove directory %s: %s", BaseFilename, PlayerDirPath, strerror(errno)); - fprintf(stderr, "Mostly removed %s. Unable to remove directory %s: %s", BaseFilename, PlayerDirPath, strerror(errno)); + LogError(LOG_NOTICE, "Mostly deleted %s. Unable to remove directory %s: %s", BaseFilename, PlayerDirPath, strerror(errno)); + fprintf(stderr, "\e[1;30mMostly deleted\e[0m %s. \e[1;31mUnable to remove directory\e[0m %s: %s", BaseFilename, PlayerDirPath, strerror(errno)); } else { - LogError(LOG_INFORMATIONAL, "Fully removed %s from the system", BaseFilename); - fprintf(stderr, "Fully removed %s from the system\n", BaseFilename); + LogError(LOG_INFORMATIONAL, "Deleted %s", BaseFilename); + fprintf(stderr, "\e[1;30mDeleted\e[0m %s\n", BaseFilename); } } return RC_SUCCESS; @@ -3564,7 +3851,7 @@ MonitorDirectory(buffers *CollationBuffers, template *IndexTemplate, template *P // TODO(matt): Maybe straight up store the IndexPath in the Config to save us having to derive it near / at the usage site buffer Events; - if(ClaimBuffer(&Events, "inotify Events", Kilobytes(4)) == RC_ARENA_FULL) { return RC_ARENA_FULL; }; + if(ClaimBuffer(&Events, "inotify Events", Kilobytes(32)) == RC_ARENA_FULL) { return RC_ARENA_FULL; }; struct inotify_event *Event; int BytesRead; @@ -3583,28 +3870,22 @@ MonitorDirectory(buffers *CollationBuffers, template *IndexTemplate, template *P if(!(StringsDiffer(Ptr, ".hmml"))) { *Ptr = '\0'; - char BaseFilename[255]; + char BaseFilename[256]; CopyString(BaseFilename, Event->name); *Ptr = '.'; + // TODO(matt): Maybe handle IN_ALL_EVENTS + if(Event->mask & IN_DELETE || Event->mask & IN_MOVED_FROM) { - // TODO(matt): Remove from Index and synchronise Table of Contents and player pages accordingly DeleteEntry(BaseFilename); - GenerateIndexPage(CollationBuffers, IndexTemplate); } else { - switch(InsertIntoIndex(CollationBuffers, BaseFilename)) - { - case RC_SUCCESS: - GeneratePlayerPage(CollationBuffers, PlayerTemplate, BaseFilename); - GenerateIndexPage(CollationBuffers, IndexTemplate); - break; - case RC_NOOP: - break; - } + InsertIntoIndex(CollationBuffers, BaseFilename); + GeneratePlayerPage(CollationBuffers, PlayerTemplate, BaseFilename); } + GenerateIndexPage(CollationBuffers, IndexTemplate); } } } @@ -3617,35 +3898,38 @@ typedef struct { bool Present; char ID[32]; -} index_entry; +} index_entry; // Metadata, unless we actually want to bolster this? int DeleteDeadIndexEntries() { - file_buffer Index; - Index.Buffer.ID = "Index"; - CopyString(Index.Path, "%s/%s.index", Config.CacheDir, Config.ProjectID); - if(ReadFileIntoBuffer(&Index, 0) == RC_ERROR_FILE) + // TODO(matt): More rigorously figure out who we should delete + // Maybe compare the output directory and the input HMML names + index Index; + Index.Metadata.Buffer.ID = "IndexMetadata"; + CopyString(Index.Metadata.Path, "%s/%s.metadata", Config.BaseDir, Config.ProjectID); + if(ReadFileIntoBuffer(&Index.Metadata, 0) == RC_ERROR_FILE) { return RC_ERROR_FILE; } - // TODO(matt): Stuff entries into an array, through which we can iterate, marking off input files that are still present - int EntryCount = *(int *)Index.Buffer.Ptr; - Index.Buffer.Ptr += sizeof(int); - - index_entry Entries[EntryCount]; - - int i = 0; - while(Index.Buffer.Ptr - Index.Buffer.Location < Index.FileSize) + Index.Header = *(index_header *)Index.Metadata.Buffer.Ptr; + if(Index.Header.DBVersion != CINERA_DB_VERSION) { - Index.Buffer.Ptr += CopyStringNoFormatT(Entries[i].ID, Index.Buffer.Ptr, ','); - Entries[i].Present = FALSE; - while(*Index.Buffer.Ptr != '\n') - { - ++Index.Buffer.Ptr; - } - ++Index.Buffer.Ptr, ++i; + fprintf(stderr, "\n\e[1;31mHandle conversion from CINERA_DB_VERSION %d to %d!\e[0m\n\n", Index.Header.DBVersion, CINERA_DB_VERSION); + exit(EXIT_FAILURE); + } + + Index.Metadata.Buffer.Ptr += sizeof(Index.Header); + + index_entry Entries[Index.Header.EntryCount]; + + for(int EntryIndex = 0; EntryIndex < Index.Header.EntryCount; ++EntryIndex) + { + index_metadata This = *(index_metadata *)Index.Metadata.Buffer.Ptr; + CopyStringNoFormat(Entries[EntryIndex].ID, This.BaseFilename); + Entries[EntryIndex].Present = FALSE; + Index.Metadata.Buffer.Ptr += sizeof(This);; } DIR *ProjectDirHandle; @@ -3666,7 +3950,7 @@ DeleteDeadIndexEntries() if(!(StringsDiffer(Ptr, ".hmml"))) { *Ptr = '\0'; - for(int i = 0; i < EntryCount; ++i) + for(int i = 0; i < Index.Header.EntryCount; ++i) { if(!StringsDiffer(Entries[i].ID, ProjectFiles->d_name)) { @@ -3679,7 +3963,7 @@ DeleteDeadIndexEntries() closedir(ProjectDirHandle); bool Deleted = FALSE; - for(int i = 0; i < EntryCount; ++i) + for(int i = 0; i < Index.Header.EntryCount; ++i) { if(Entries[i].Present == FALSE) { @@ -3688,14 +3972,18 @@ DeleteDeadIndexEntries() } } - FreeBuffer(&Index.Buffer); + FreeBuffer(&Index.Metadata.Buffer); return Deleted ? RC_SUCCESS : RC_NOOP; } int SyncIndexWithInput(buffers *CollationBuffers, template *IndexTemplate, template *PlayerTemplate) { - DeleteDeadIndexEntries(); + bool Deleted = FALSE; + if(DeleteDeadIndexEntries() == RC_SUCCESS) + { + Deleted = TRUE; + } DIR *ProjectDirHandle; if(!(ProjectDirHandle = opendir(Config.ProjectDir))) @@ -3715,36 +4003,41 @@ SyncIndexWithInput(buffers *CollationBuffers, template *IndexTemplate, template if(!(StringsDiffer(Ptr, ".hmml"))) { *Ptr = '\0'; - switch(InsertIntoIndex(CollationBuffers, ProjectFiles->d_name)) - { - case RC_SUCCESS: - GeneratePlayerPage(CollationBuffers, PlayerTemplate, ProjectFiles->d_name); - Inserted = TRUE; - break; - case RC_NOOP: - break; - } + InsertIntoIndex(CollationBuffers, ProjectFiles->d_name); + GeneratePlayerPage(CollationBuffers, PlayerTemplate, ProjectFiles->d_name); + Inserted = TRUE; } } closedir(ProjectDirHandle); - if(Inserted) + if(Deleted || Inserted) { GenerateIndexPage(CollationBuffers, IndexTemplate); } return RC_SUCCESS; } +char * +StripTrailingSlash(char *String) +{ + int Length = StringLength(String); + if(Length > 0 && String[Length - 1] == '/') + { + String[Length - 1] = '\0'; + } + return String; +} + int main(int ArgC, char **Args) { // TODO(matt): Read all defaults from the config config DefaultConfig = { .BaseDir = ".", - .CSSDir = ".", + .CSSDir = "", .Edition = EDITION_SINGLE, - .ImagesDir = ".", - .JSDir = ".", + .ImagesDir = "", + .JSDir = "", .LogLevel = LOG_EMERGENCY, .DefaultMedium = "programming", .Mode = getenv("CINERA_MODE") ? MODE_INTEGRATE : MODE_BARE, @@ -3754,6 +4047,7 @@ main(int ArgC, char **Args) .ProjectDir = ".", .ProjectID = "", .RootDir = ".", + .RootURL = "", .TemplatePlayerLocation = "template_player.html", .TemplateIndexLocation = "template_index.html", .UpdateInterval = 4 @@ -3777,27 +4071,27 @@ main(int ArgC, char **Args) } char CommandLineArg; - while((CommandLineArg = getopt(ArgC, Args, "b:c:fhi:I:j:l:m:o:p:r:t:u:x:")) != -1) + while((CommandLineArg = getopt(ArgC, Args, "b:c:d:fhi:j:l:m:o:p:r:u:t:U:x:")) != -1) { switch(CommandLineArg) { case 'b': - Config.BaseDir = optarg; + Config.BaseDir = StripTrailingSlash(optarg); break; case 'c': - Config.CSSDir = optarg; + Config.CSSDir = StripTrailingSlash(optarg); + break; + case 'd': + Config.ProjectDir = StripTrailingSlash(optarg); break; case 'f': Config.ForceIntegration = TRUE; break; case 'i': - Config.ImagesDir = optarg; - break; - case 'I': - Config.ProjectID = optarg; + Config.ImagesDir = StripTrailingSlash(optarg); break; case 'j': - Config.JSDir = optarg; + Config.JSDir = StripTrailingSlash(optarg); break; case 'l': // TODO(matt): Make this actually take a string, rather than requiring the LogLevel number @@ -3811,16 +4105,19 @@ main(int ArgC, char **Args) Config.OutIntegratedLocation = optarg; break; case 'p': - Config.ProjectDir = optarg; + Config.ProjectID = optarg; break; case 'r': - Config.RootDir = optarg; + Config.RootDir = StripTrailingSlash(optarg); break; case 't': Config.TemplatePlayerLocation = optarg; Config.Mode = MODE_INTEGRATE; break; case 'u': + Config.RootURL = StripTrailingSlash(optarg); + break; + case 'U': Config.UpdateInterval = StringToInt(optarg); break; case 'x': @@ -3834,32 +4131,9 @@ main(int ArgC, char **Args) } } - // TODO(matt): Sanitise this entry into Project Mode, probably once we switch to the config system - if(StringsDiffer(Config.BaseDir, ".") || StringsDiffer(Config.ProjectDir, ".")) + if(StringsDiffer(Config.ProjectID, "")) { - if(StringsDiffer(Config.BaseDir, ".") && StringsDiffer(Config.ProjectDir, ".")) - { - if(StringsDiffer(Config.ProjectID, "")) - { - Config.Edition = EDITION_PROJECT; - } - else - { - fprintf(stderr, "%s: Project ID must be set using the -I flag in order for us to enter Project Mode\n", Args[0]); - return 1; - - } - } - else - { - fprintf(stderr, "%s: Both the Base Output Directory and the Project Directory must be set in order for us to enter Project Mode\n", Args[0]); - return 1; - } - } - - if(Config.CSSDir[StringLength(Config.CSSDir) - 1] == '/') - { - Config.CSSDir[StringLength(Config.CSSDir) - 1] = '\0'; + Config.Edition = EDITION_PROJECT; } bool ValidDefaultMedium = FALSE; @@ -3882,15 +4156,6 @@ main(int ArgC, char **Args) return 1; } - if(Config.ImagesDir[StringLength(Config.ImagesDir) - 1] == '/') - { - Config.ImagesDir[StringLength(Config.ImagesDir) - 1] = '\0'; - } - if(Config.JSDir[StringLength(Config.JSDir) - 1] == '/') - { - Config.JSDir[StringLength(Config.JSDir) - 1] = '\0'; - } - // NOTE(matt): Templating // // Config will contain paths of multiple templates @@ -3926,7 +4191,7 @@ main(int ArgC, char **Args) // IncludesPlayer // Menus // Player - // Script + // ScriptPlayer // // IncludesIndex // Index @@ -3935,10 +4200,12 @@ main(int ArgC, char **Args) if(ClaimBuffer(&CollationBuffers.IncludesPlayer, "IncludesPlayer", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; }; if(ClaimBuffer(&CollationBuffers.Menus, "Menus", Kilobytes(24)) == RC_ARENA_FULL) { goto RIP; }; if(ClaimBuffer(&CollationBuffers.Player, "Player", Kilobytes(256)) == RC_ARENA_FULL) { goto RIP; }; - if(ClaimBuffer(&CollationBuffers.Script, "Script", Kilobytes(8)) == RC_ARENA_FULL) { goto RIP; }; + if(ClaimBuffer(&CollationBuffers.ScriptPlayer, "ScriptPlayer", Kilobytes(8)) == RC_ARENA_FULL) { goto RIP; }; if(ClaimBuffer(&CollationBuffers.IncludesIndex, "IncludesIndex", Kilobytes(1)) == RC_ARENA_FULL) { goto RIP; }; + if(ClaimBuffer(&CollationBuffers.Search, "Search", Kilobytes(32)) == RC_ARENA_FULL) { goto RIP; }; if(ClaimBuffer(&CollationBuffers.Index, "Index", Kilobytes(8)) == RC_ARENA_FULL) { goto RIP; }; + if(ClaimBuffer(&CollationBuffers.ScriptIndex, "ScriptIndex", 256) == RC_ARENA_FULL) { goto RIP; }; *CollationBuffers.Title = '\0'; template *PlayerTemplate; @@ -3977,6 +4244,7 @@ main(int ArgC, char **Args) // // Integrating or not + curl_version_info_data *CurlVersion = curl_version_info(CURLVERSION_NOW); if(Config.Edition == EDITION_PROJECT) { @@ -3986,16 +4254,59 @@ main(int ArgC, char **Args) fclose(MemLog); #endif - printf("[Cinera %s]\n" + // TODO(matt): Also log these startup messages? + printf( "Cinera: %d.%d.%d\n" + "Cinera DB: %d\n" + "hmmlib: %d.%d.%d\n" + "libcurl: %s\n" "\n" - "Project ID: %s\n" - "Project Directory: %s\n" + "Globals\n" + " Cache Directory:\t\t%s\n" "\n" - "Synchronising with annotation files in Project Directory\n", CINERA_VERSION, Config.ProjectID, Config.ProjectDir); + " Root\n" + " Directory:\t\t%s\n" + " URL:\t\t\t%s\n" + " Paths relative to root\n" + " CSS:\t\t\t%s\n" + " Images:\t\t\t%s\n" + " JS:\t\t\t%s\n", + CINERA_APP_VERSION.Major, CINERA_APP_VERSION.Minor, CINERA_APP_VERSION.Patch, + CINERA_DB_VERSION, + hmml_version.Major, hmml_version.Minor, hmml_version.Patch, + CurlVersion->version, + Config.CacheDir, + Config.RootDir, + StringsDiffer(Config.RootURL, "") ? Config.RootURL : "[empty]", + + StringsDiffer(Config.CSSDir, "") ? Config.CSSDir : "(same as root)", + StringsDiffer(Config.ImagesDir, "") ? Config.ImagesDir : "(same as root)", + StringsDiffer(Config.JSDir, "") ? Config.JSDir : "(same as root)"); + + if(Config.Mode == MODE_INTEGRATE) + { + printf(" Index Template:\t\t%s\n" + " Player Template:\t%s\n", + StringsDiffer(Config.TemplateIndexLocation, "") ? Config.TemplateIndexLocation : "(same as root)", + StringsDiffer(Config.TemplatePlayerLocation, "") ? Config.TemplatePlayerLocation : "(same as root)"); + } + + printf("\n" + "Project\n" + " ID:\t\t\t\t%s\n" + " Input Directory:\t\t%s\n" + " Output Base Directory:\t%s\n" + " Default Medium:\t\t%s\n" + "\n" + "┌╼ Synchronising with annotation files in Project Input Directory ╾┐\n", + + Config.ProjectID, + Config.ProjectDir, + Config.BaseDir, + Config.DefaultMedium); SyncIndexWithInput(&CollationBuffers, IndexTemplate, PlayerTemplate); - printf("\nMonitoring Project Directory for new, edited or deleted .hmml files\n"); + printf("\n┌╼ Monitoring Project Directory for \e[1;32mnew\e[0m, \e[1;33medited\e[0m and \e[1;30mdeleted\e[0m .hmml files ╾┐\n"); int inotifyInstance = inotify_init1(IN_NONBLOCK); // NOTE(matt): Do we want to also watch IN_DELETE_SELF events? @@ -4057,9 +4368,12 @@ NextFile: } } + DeclaimBuffer(&CollationBuffers.ScriptIndex); DeclaimBuffer(&CollationBuffers.Index); + DeclaimBuffer(&CollationBuffers.Search); DeclaimBuffer(&CollationBuffers.IncludesIndex); - DeclaimBuffer(&CollationBuffers.Script); + + DeclaimBuffer(&CollationBuffers.ScriptPlayer); DeclaimBuffer(&CollationBuffers.Player); DeclaimBuffer(&CollationBuffers.Menus); DeclaimBuffer(&CollationBuffers.IncludesPlayer); diff --git a/hmml_to_html/cinera.css b/cinera/cinera.css similarity index 86% rename from hmml_to_html/cinera.css rename to cinera/cinera.css index b0fe468..1c63cb9 100644 --- a/hmml_to_html/cinera.css +++ b/cinera/cinera.css @@ -1,3 +1,87 @@ +/* Index */ + +.queryContainer { + width: 1024px; + margin: 15px auto; + display: flex; + flex-direction: horizontal; +} + +.queryContainer label { + flex-grow: 0; + flex-shrink: 0; +} + +.queryContainer .inputContainer { + flex-grow: 1; + position: relative; +} + +#query { + width: 100%; +} + +.spinner { + position: absolute; + top: 2px; + right: 5px; + color: black; + height: 100%; + display: none; +} + +.spinner.show { + display: block; +} + +#results, +#cineraIndex { + width: 800px; + margin: 0 auto; +} + +.dayName { + width: 200px; + display: inline-block; + vertical-align: top; + font-size: 12px; + line-height: 16px; + box-sizing: border-box; + padding: 5px; +} + +#cineraIndex dt a { + display: block; + padding: 5px; + text-decoration: none; +} + +.markerList { + display: inline-block; + width: 600px; + box-sizing: border-box; + vertical-align: top; +} + +.markerList > .marker { + padding: 10px 5px; + cursor: pointer; + border-top: 1px solid; + display: block; + text-decoration: none; +} + +.markerList > .marker:first-child { + border: none; +} + +#resultsSummary { + text-align: center; + margin: 10px 0; +} + +/* Player */ + /* Structure */ .title, @@ -181,10 +265,14 @@ } .title > .menu > .credits_container .credit .support { - text-align: right; padding: 16px; } +.title > .menu > .credits_container .credit .support .support_icon { + height: 16px; + width: 16px; +} + .title > .menu > .refs .ref:last-child, .title > .menu > .credits_container .credit:last-child { border: none; diff --git a/hmml_to_html/cinera__hero.css b/cinera/cinera__hero.css similarity index 81% rename from hmml_to_html/cinera__hero.css rename to cinera/cinera__hero.css index 3a98ffa..1c42056 100644 --- a/hmml_to_html/cinera__hero.css +++ b/cinera/cinera__hero.css @@ -1,3 +1,37 @@ +/* Search */ + +.dayContainer.hero, +#cineraIndex.hero dt { + background-color: #161616; +} + +.dayContainer:nth-child(2n).hero, +#cineraIndex.hero dt:nth-child(2n) { + background-color: #303030; +} + +.dayContainer.hero > .dayName, +#cineraIndex.hero dt a { + color: #8A877D; +} + +.markerList.hero > .marker +{ + border-color: rgba(255, 255, 255, 0.1); + color: #ddd; +} + +.markerList.hero > .marker b { + color: black; + background-color: rgb(255, 155, 0); +} + +.markerList.hero > .marker:hover { + background-color: #444; +} + +/* Player */ + .title.hero, .title.hero .menu .refs, .title.hero .menu > .refs .ref, diff --git a/hmml_to_html/cinera__riscy.css b/cinera/cinera__riscy.css similarity index 87% rename from hmml_to_html/cinera__riscy.css rename to cinera/cinera__riscy.css index 1cd74ea..604e4b8 100644 --- a/hmml_to_html/cinera__riscy.css +++ b/cinera/cinera__riscy.css @@ -1,3 +1,6 @@ +.dayContainer.riscy, +#cineraIndex.riscy dt, + .title.riscy, .title.riscy > .menu .refs, .title.riscy > .menu .filter_container, @@ -8,9 +11,31 @@ .markers_container.riscy, .markers_container.riscy > .marker { background-color: #EEE; +} + +.dayContainer:nth-child(2n).riscy, +#cineraIndex.riscy dt:nth-child(2n) { + background-color: #FFF; +} + +.markerList.riscy > .marker, + +.title.riscy, +.title.riscy > .menu .refs, +.title.riscy > .menu .filter_container, +.title.riscy > .menu > .refs .ref, +.title.riscy > .menu > .filter_container .filter_mode, +.title.riscy > .menu > .credits_container, +.title.riscy > .menu > .credits_container .credit, +.markers_container.riscy, +.markers_container.riscy > .marker { border-color: rgba(246, 178, 26, 0.8); } +.dayContainer.riscy > .dayName, +.markerList.riscy > .marker, +#cineraIndex.riscy dt a, + .title.riscy, .title.riscy > .menu > .refs .ref, /*.title.riscy > .menu > .refs .ref .timecode:hover:before,*/ @@ -36,23 +61,22 @@ } .title.riscy > .menu > .refs .ref.current .ref_indices .timecode.focused { -color: rgb(246, 178, 26); + color: rgb(246, 178, 26); } +.markerList.riscy > .marker:hover, +#cineraIndex.riscy dt:hover, + /*.title.riscy > .menu:hover,*/ .title.riscy > .menu.visible, - /*.title.riscy > .menu > .refs .ref:hover,*/ .title.riscy > .menu > .quotes_container .ref.focused, .title.riscy > .menu > .references_container .ref.focused, - .title.riscy > .menu > .filter_container .filter_mode:hover, /*.title.riscy > .menu > .filter_container .filter_content:hover,*/ .title.riscy > .menu > .filter_container .filter_content.focused, - /*.title.riscy > .menu > .credits_container .credit *:hover,*/ .title.riscy > .menu > .credits_container .credit *.focused, - .markers_container.riscy > .marker:hover > .content { background-color: #FFF8E7; } @@ -63,15 +87,13 @@ color: rgb(246, 178, 26); color: #FFF8E7; } +.markerList.riscy > .marker b, + .title.riscy > .menu > .refs .ref.current, /*.title.riscy > .menu > .refs .ref.current .timecode:hover:before,*/ -.markers_container.riscy > .marker > .progress .content { - color: #FFF; -} - -.title.riscy > .menu > .refs .ref.current, .markers_container.riscy > .marker > .progress .content { background-color: rgb(42, 49, 114); + color: #FFF; } /*.title.riscy > .menu > .refs .ref.current:hover,*/ diff --git a/hmml_to_html/cinera_icon_filter.png b/cinera/cinera_icon_filter.png similarity index 100% rename from hmml_to_html/cinera_icon_filter.png rename to cinera/cinera_icon_filter.png diff --git a/cinera/cinera_player_post.js b/cinera/cinera_player_post.js new file mode 100644 index 0000000..a22a212 --- /dev/null +++ b/cinera/cinera_player_post.js @@ -0,0 +1,186 @@ +var menuState = []; +var titleBar = document.querySelector(".title"); +var quotesMenu = titleBar.querySelector(".quotes_container"); +if(quotesMenu) +{ + menuState.push(quotesMenu); + var quoteTimecodes = quotesMenu.querySelectorAll(".refs .ref .ref_indices .timecode"); + for (var i = 0; i < quoteTimecodes.length; ++i) { + quoteTimecodes[i].addEventListener("click", function(ev) { + if (player) { + var time = ev.currentTarget.getAttribute("data-timestamp"); + mouseSkipToTimecode(player, time, ev); + } + }); + } + var lastFocusedQuote = null; +} + +var referencesMenu = titleBar.querySelector(".references_container"); +if(referencesMenu) +{ + menuState.push(referencesMenu); + var referenceItems = referencesMenu.querySelectorAll(".ref"); + if(referenceItems) + { + for(var i = 0; i < referenceItems.length; ++i) + { + referenceItems[i].addEventListener("mouseenter", function(ev) { + mouseOverReferences(this); + }) + }; + var lastFocusedReference = null; + var lastFocusedIdentifier = null; + } + + var refTimecodes = referencesMenu.querySelectorAll(".refs .ref .ref_indices .timecode"); + for (var i = 0; i < refTimecodes.length; ++i) { + refTimecodes[i].addEventListener("click", function(ev) { + if (player) { + var time = ev.currentTarget.getAttribute("data-timestamp"); + mouseSkipToTimecode(player, time, ev); + } + }); + } +} + +if(referencesMenu || quotesMenu) +{ + var refSources = titleBar.querySelectorAll(".refs .ref"); // This is for both quotes and refs + for (var i = 0; i < refSources.length; ++i) { + refSources[i].addEventListener("click", function(ev) { + if (player) { + player.pause(); + } + }); + } +} + +var filterMenu = titleBar.querySelector(".filter_container"); +if(filterMenu) +{ + menuState.push(filterMenu); + var lastFocusedCategory = null; + var lastFocusedTopic = null; + var lastFocusedMedium = null; + + var filter = filterMenu.parentNode; + + var filterModeElement = filter.querySelector(".filter_mode"); + filterModeElement.addEventListener("click", function(ev) { + toggleFilterMode(); + }); + + var filterMode = filterModeElement.classList[1]; + var filterItems = filter.querySelectorAll(".filter_content"); + for(var i = 0; i < filterItems.length; ++i) + { + filterItems[i].addEventListener("mouseenter", function(ev) { + navigateFilter(this); + }) + + filterItems[i].addEventListener("click", function(ev) { + filterItemToggle(this); + }); + } +} + +var creditsMenu = titleBar.querySelector(".credits_container"); +if(creditsMenu) +{ + menuState.push(creditsMenu); + var lastFocusedCreditItem = null; + + var creditItems = creditsMenu.querySelectorAll(".person, .support"); + for(var i = 0; i < creditItems.length; ++i) + { + creditItems[i].addEventListener("mouseenter", function(ev) { + if(this != lastFocusedCreditItem) + { + lastFocusedCreditItem.classList.remove("focused"); + if(lastFocusedCreditItem.classList.contains("support")) + { + setIconLightness(lastFocusedCreditItem.firstChild); + } + lastFocusedCreditItem = this; + focusedElement = lastFocusedCreditItem; + focusedElement.classList.add("focused"); + if(focusedElement.classList.contains("support")) + { + setIconLightness(focusedElement.firstChild); + } + } + }) + } + + var supportIcons = creditsMenu.querySelectorAll(".support_icon"); + { + for(var i = 0; i < supportIcons.length; ++i) + { + setIconLightness(supportIcons[i]); + } + } + +} +var sourceMenus = titleBar.querySelectorAll(".menu"); + +var helpButton = titleBar.querySelector(".help"); +var helpDocumentation = helpButton.querySelector(".help_container"); +helpButton.addEventListener("click", function(ev) { + handleMouseOverMenu(this, ev.type); +}) + +var focusedElement = null; +var focusedIdentifier = null; + +var playerContainer = document.querySelector(".player_container") +var player = new Player(playerContainer, onRefChanged); +window.addEventListener("resize", function() { player.updateSize(); }); +document.addEventListener("keydown", function(ev) { + var key = ev.key; + if(ev.getModifierState("Shift") && key == " ") + { + key = "capitalSpace"; + } + + if(handleKey(key) == true && focusedElement) + { + ev.preventDefault(); + } +}); + +for(var i = 0; i < sourceMenus.length; ++i) +{ + sourceMenus[i].addEventListener("mouseenter", function(ev) { + handleMouseOverMenu(this, ev.type); + }) + sourceMenus[i].addEventListener("mouseleave", function(ev) { + handleMouseOverMenu(this, ev.type); + }) +}; + +var testMarkers = playerContainer.querySelectorAll(".marker"); + +window.addEventListener("blur", function(){ + document.getElementById("focus-warn").style.display = "block"; +}); + +window.addEventListener("focus", function(){ + document.getElementById("focus-warn").style.display = "none"; +}); + +var colouredItems = playerContainer.querySelectorAll(".author, .member, .project"); +for(i = 0; i < colouredItems.length; ++i) +{ + setTextLightness(colouredItems[i]); +} + +var topicDots = document.querySelectorAll(".category"); +for(var i = 0; i < topicDots.length; ++i) +{ + setDotLightness(topicDots[i]); +} + +if(location.hash) { + player.setTime(location.hash.startsWith('#') ? location.hash.substr(1) : location.hash); +} diff --git a/hmml_to_html/cinera.js b/cinera/cinera_player_pre.js similarity index 94% rename from hmml_to_html/cinera.js rename to cinera/cinera_player_pre.js index 792d0f5..add4009 100644 --- a/hmml_to_html/cinera.js +++ b/cinera/cinera_player_pre.js @@ -392,14 +392,22 @@ function toggleMenuVisibility(element) { if(element.querySelectorAll(".credit .support")[0]) { lastFocusedCreditItem = element.querySelectorAll(".credit .support")[0]; + focusedElement = lastFocusedCreditItem; + focusedElement.classList.add("focused"); + setIconLightness(focusedElement.firstChild); } else { lastFocusedCreditItem = element.querySelectorAll(".credit .person")[0]; + focusedElement = lastFocusedCreditItem; + focusedElement.classList.add("focused"); } } - focusedElement = lastFocusedCreditItem; - focusedElement.classList.add("focused"); + else + { + focusedElement = lastFocusedCreditItem; + focusedElement.classList.add("focused"); + } } } } @@ -532,15 +540,18 @@ function handleKey(key) { if(focusedElement.parentNode.previousElementSibling.querySelector(".support") && focusedElement.classList.contains("support")) { + setIconLightness(focusedElement.firstChild); lastFocusedCreditItem = focusedElement.parentNode.previousElementSibling.querySelector(".support"); focusedElement = lastFocusedCreditItem; + focusedElement.classList.add("focused"); + setIconLightness(focusedElement.firstChild); } else { lastFocusedCreditItem = focusedElement.parentNode.previousElementSibling.querySelector(".person"); focusedElement = lastFocusedCreditItem; + focusedElement.classList.add("focused"); } - focusedElement.classList.add("focused"); } } } @@ -596,15 +607,18 @@ function handleKey(key) { if(focusedElement.parentNode.nextElementSibling.querySelector(".support") && focusedElement.classList.contains("support")) { + setIconLightness(focusedElement.firstChild); lastFocusedCreditItem = focusedElement.parentNode.nextElementSibling.querySelector(".support"); focusedElement = lastFocusedCreditItem; + focusedElement.classList.add("focused"); + setIconLightness(focusedElement.firstChild); } else { lastFocusedCreditItem = focusedElement.parentNode.nextElementSibling.querySelector(".person"); focusedElement = lastFocusedCreditItem; + focusedElement.classList.add("focused"); } - focusedElement.classList.add("focused"); } } } @@ -618,11 +632,17 @@ function handleKey(key) { if(focusedIdentifier.previousElementSibling) { focusedIdentifier.classList.remove("focused"); - lastFocusedIdentifier = focusedIdentifier.previousElementSibling; focusedIdentifier = lastFocusedIdentifier; focusedIdentifier.classList.add("focused"); } + else if(focusedIdentifier.parentNode.previousElementSibling.classList.contains("ref_indices")) + { + focusedIdentifier.classList.remove("focused"); + lastFocusedIdentifier = focusedIdentifier.parentNode.previousElementSibling.lastElementChild; + focusedIdentifier = lastFocusedIdentifier; + focusedIdentifier.classList.add("focused"); + } } else if(focusedElement.classList.contains("filter_content")) { @@ -648,6 +668,7 @@ function handleKey(key) { focusedElement.classList.remove("focused"); lastFocusedCreditItem = focusedElement.previousElementSibling; + setIconLightness(focusedElement.firstChild); focusedElement = lastFocusedCreditItem; focusedElement.classList.add("focused"); } @@ -668,6 +689,13 @@ function handleKey(key) { focusedIdentifier = lastFocusedIdentifier; focusedIdentifier.classList.add("focused"); } + else if(focusedIdentifier.parentNode.nextElementSibling) + { + focusedIdentifier.classList.remove("focused"); + lastFocusedIdentifier = focusedIdentifier.parentNode.nextElementSibling.firstElementChild; + focusedIdentifier = lastFocusedIdentifier; + focusedIdentifier.classList.add("focused"); + } } else if(focusedElement.classList.contains("filter_content")) { @@ -696,6 +724,7 @@ function handleKey(key) { lastFocusedCreditItem = focusedElement.nextElementSibling; focusedElement = lastFocusedCreditItem; focusedElement.classList.add("focused"); + setIconLightness(focusedElement.firstChild); } } } @@ -951,11 +980,16 @@ function filterItemToggle(filterItem) { function resetFilter() { for(i in filterItems) { - if(filterItems[i].classList && filterItems[i].classList.contains("off")) + if(filterItems[i].classList) { - filterItemToggle(filterItems[i]); + var selectedCategory = filterItems[i].classList[1]; + if(filterInitState[selectedCategory].off ^ filterState[selectedCategory].off) + { + filterItemToggle(filterItems[i]); + } } } + if(filterMode == "exclusive") { toggleFilterMode(); @@ -1082,7 +1116,7 @@ function mouseOverReferences(reference) { focusedElement = lastFocusedReference; focusedElement.classList.add("focused"); - var ourIdentifiers = reference.querySelector(".ref_indices").children; + var ourIdentifiers = reference.querySelectorAll(".timecode"); weWereLastFocused = false; for(var k = 0; k < ourIdentifiers.length; ++k) { @@ -1094,7 +1128,7 @@ function mouseOverReferences(reference) { if(!weWereLastFocused) { lastFocusedIdentifier.classList.remove("focused"); - lastFocusedIdentifier = reference.querySelector(".ref_indices").firstElementChild; + lastFocusedIdentifier = ourIdentifiers[0]; } focusedIdentifer = lastFocusedIdentifier; focusedIdentifer.classList.add("focused"); @@ -1219,7 +1253,19 @@ function setDotLightness(topicDot) } else { - topicDot.style.backgroundColor = ("hsl(" + Hue + ", " + Saturation + "%, 24%)"); - topicDot.style.borderColor = ("hsl(" + Hue + ", " + Saturation + "%, 24%)"); + topicDot.style.backgroundColor = ("hsl(" + Hue + ", " + Saturation + "%, 47%)"); + topicDot.style.borderColor = ("hsl(" + Hue + ", " + Saturation + "%, 47%)"); + } +} + +function setIconLightness(iconElement) +{ + if(getBackgroundBrightness(iconElement) < 127) + { + iconElement.style.backgroundPosition = ("0px 0px"); + } + else + { + iconElement.style.backgroundPosition = ("16px 0px"); } } diff --git a/cinera/cinera_search.js b/cinera/cinera_search.js new file mode 100644 index 0000000..a66d8ac --- /dev/null +++ b/cinera/cinera_search.js @@ -0,0 +1,240 @@ +if (location.hash && location.hash.length > 0) { + var initialQuery = location.hash; + if (initialQuery[0] == "#") { + initialQuery = initialQuery.slice(1); + } + document.getElementById("query").value = decodeURIComponent(initialQuery); +} + + +var indexContainer = document.getElementById("cineraIndex"); +var lastQuery = null; +var resultsToRender = []; +var resultsIndex = 0; +var resultsMarkerIndex = 0; +var resultsContainer = document.getElementById("results"); +var rendering = false; + +var dayContainerPrototype = document.createElement("DIV"); +dayContainerPrototype.classList.add("dayContainer"); +dayContainerPrototype.classList.add(projectID); + +var dayNamePrototype = document.createElement("SPAN"); +dayNamePrototype.classList.add("dayName"); +dayContainerPrototype.appendChild(dayNamePrototype); + +var markerListPrototype = document.createElement("DIV"); +markerListPrototype.classList.add("markerList"); +markerListPrototype.classList.add(projectID); +dayContainerPrototype.appendChild(markerListPrototype); + +var markerPrototype = document.createElement("A"); +markerPrototype.classList.add("marker"); +markerPrototype.setAttribute("target", "_blank"); + +var highlightPrototype = document.createElement("B"); + +var episodes = []; + +function getEpisodeName(filename) { + var day = filename; + var dayParts = day.match(/([a-zA-Z_-]+)([0-9]+)?([a-zA-Z]+)?/); + day = dayParts[1].slice(0, 1).toUpperCase() + dayParts[1].slice(1) + (dayParts[2] ? " " + dayParts[2] : "") + (dayParts[3] ? " " + dayParts[3].toUpperCase() : ""); + return day; +} + +function markerTime(totalTime) { + var markTime = "("; + var hours = Math.floor(totalTime / 60 / 60); + var minutes = Math.floor(totalTime / 60) % 60; + var seconds = totalTime % 60; + if (hours > 0) { + markTime += padTimeComponent(hours) + ":"; + } + + markTime += padTimeComponent(minutes) + ":" + padTimeComponent(seconds) + ")"; + + return markTime; +} + +function padTimeComponent(component) { + return (component < 10 ? "0" + component : component); +} + +function runSearch() { + var queryStr = document.getElementById("query").value; + if (lastQuery != queryStr) { + var oldResultsContainer = resultsContainer; + resultsContainer = oldResultsContainer.cloneNode(false); + oldResultsContainer.parentNode.insertBefore(resultsContainer, oldResultsContainer); + oldResultsContainer.remove(); + resultsIndex = 0; + resultsMarkerIndex = 0; + } + lastQuery = queryStr; + resultsToRender = []; + var numEpisodes = 0; + var numMarkers = 0; + var totalSeconds = 0; + if (queryStr && queryStr.length > 0) { + indexContainer.style.display = "none"; + if (episodes.length > 0) { + var query = new RegExp(queryStr.replace("(", "\\(").replace(")", "\\)").replace(/(^|[^\\])\\$/, "$1"), "gi"); + for (var i = 0; i < episodes.length; ++i) { + var episode = episodes[i]; + var matches = []; + for (var j = 0; j < episode.markers.length; ++j) { + query.lastIndex = 0; + var result = query.exec(episode.markers[j].text); + if (result && result[0].length > 0) { + numMarkers++; + matches.push(episode.markers[j]); + if (j < episode.markers.length-1) { + totalSeconds += episode.markers[j+1].totalTime - episode.markers[j].totalTime; + } + } + } + if (matches.length > 0) { + numEpisodes++; + resultsToRender.push({ + query: query, + episode: episode, + matches: matches + }); + } + } + + if (!rendering) { + renderResults(); + } + } else { + document.querySelector(".spinner").classList.add("show"); + } + } + else + { + indexContainer.style.display = "block"; + } + + var totalTime = Math.floor(totalSeconds/60/60) + "h " + Math.floor(totalSeconds/60)%60 + "m " + totalSeconds%60 + "s "; + + document.getElementById("resultsSummary").textContent = "Found: " + numEpisodes + " episodes, " + numMarkers + " markers, " + totalTime + "total."; +} + +function renderMatches(renderStart) { + var query = resultsToRender[resultsIndex].query; + var episode = resultsToRender[resultsIndex].episode; + var matches = resultsToRender[resultsIndex].matches; + var markerList = null; + if (resultsMarkerIndex == 0) { + var dayContainer = dayContainerPrototype.cloneNode(true); + var dayName = dayContainer.children[0]; + markerList = dayContainer.children[1]; + dayName.textContent = episode.day + ": " + episode.title; + resultsContainer.appendChild(dayContainer); + } else { + markerList = document.querySelector("#results > .dayContainer:nth-child(" + (resultsIndex+1) + ") .markerList"); + } + + do { + var match = matches[resultsMarkerIndex]; + var marker = markerPrototype.cloneNode(); + marker.setAttribute("href", episode.filename.replace(/"/g, "") + "/#" + match.totalTime); + query.lastIndex = 0; + var cursor = 0; + var text = match.text; + var result = null; + marker.appendChild(document.createTextNode(match.prettyTime + " ")); + while (result = query.exec(text)) { + if (result.index > cursor) { + marker.appendChild(document.createTextNode(text.slice(cursor, result.index))); + } + var highlightEl = highlightPrototype.cloneNode(); + highlightEl.textContent = result[0]; + marker.appendChild(highlightEl); + cursor = result.index + result[0].length; + } + + if (cursor < text.length) { + marker.appendChild(document.createTextNode(text.slice(cursor, text.length))); + } + markerList.appendChild(marker); + resultsMarkerIndex++; + } while (resultsMarkerIndex < matches.length && performance.now() - renderStart < 1); + + return resultsMarkerIndex == matches.length; +} + +function renderResults() { + if (resultsIndex < resultsToRender.length) { + rendering = true; + var renderStart = performance.now(); + while (resultsIndex < resultsToRender.length && performance.now() - renderStart < 1) { + var done = renderMatches(renderStart); + if (done) { + resultsMarkerIndex = 0; + resultsIndex++; + } + } + requestAnimationFrame(renderResults); + } else { + rendering = false; + } +} + +var queryEl = document.getElementById("query") +queryEl.addEventListener("input", function(ev) { + history.replaceState(null, null, "#" + encodeURIComponent(queryEl.value)); + runSearch(); +}); + +var xhr = new XMLHttpRequest(); +xhr.addEventListener("load", function() { + var contents = xhr.response; + var lines = contents.split("\n"); + var mode = "none"; + var episode = null; + for (var i = 0; i < lines.length; ++i) { + var line = lines[i]; + if (line.trim().length == 0) { continue; } + if (line == "---") { + if (episode != null) { + episode.filename = episode.name; + episode.day = getEpisodeName(episode.filename + ".html.md"); + episodes.push(episode); + } + episode = {}; + mode = "none"; + } else if (line.startsWith("name:")) { + episode.name = line.slice(6); + } else if (line.startsWith("title:")) { + episode.title = line.slice(7).replace(/"/g, ""); + } else if (line.startsWith("markers")) { + mode = "markers"; + episode.markers = []; + } else if (mode == "markers") { + var match = line.match(/"(\d+)": "(.+)"/); + if (match == null) { + console.log(name, line); + } else { + var totalTime = parseInt(line.slice(1)); + var marker = { + totalTime: totalTime, + prettyTime: markerTime(totalTime), + text: match[2].replace(/\\"/g, "\"") + } + episode.markers.push(marker); + } + } + } + document.querySelector(".spinner").classList.remove("show"); + runSearch(); +}); +xhr.addEventListener("error", function() { + console.error("Failed to load content"); +}); +xhr.open("GET", indexLocation); +xhr.setRequestHeader("Content-Type", "text/plain"); +xhr.send(); + +runSearch(); diff --git a/cinera/cinera_sprite_patreon.png b/cinera/cinera_sprite_patreon.png new file mode 100644 index 0000000000000000000000000000000000000000..33b19eb1e86e4987beea5a1c20b7fede19129959 GIT binary patch literal 881 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Et!3-oF?)7E^DVB6cUq=Rpjs4tz5?O(Kg=CK) zUj~LMH3o);76yi2K%s^g3=E|P3=FRl7#OT(FffQ0%-I!a!@$5O9^ez=3RLhjBLt5A zqXJf4PdI|BL1rK;hp5CV3m1f_hj203EIJ;@Qp~7aWGP&1WHXV~z}*QE#3bR42dV_J zXDaBi0D~r~B*-tAA)~&A!L+0{lrs~?IQK>E_{YQxU{d965fJTH-LI<Ar_rBs_(IB& z>{I8nPub12oA=1`k*E6x_YEQ6R<nYzSmmxsc4CzqfMix(c3t4Y^}q;WU`+CMcVYdJ z|MEDH!&%@FS<Jw|Eeyhp4727)00r4gJbhi+A98bw>#_#fvUdW7?s&R5hG?8mPDn{g zOJraQGb`isQ#gC{>|yRlpFeVQa5YLC3RH3k3el0`IHD>cp_06$p(b;thQfrY&6*R! zJK7jrJD60TYH$QJY6bMRF`8~*`l=yufkFF1%W3!QMv;uePZyUQ;3^U1`tqfbi${Qq z=O9;)09TK11DB5=*O>!cX9T%)9Ks@$L?RqzGI_%jn9G*v#x%;hG}`*yV|?W6K4Xut z&Vo6y2R^OKyU<Ydo#AwK)j<)1ir*GLe*H8);5125!9c@8MMUrl2SaEB!xU|fWUh3_ zXYK{=pOii`@;F5V&3bvT9_UZi64!{5l*E!$tK_0oAjM#0U}&yuXsByo8DeB?WnyM! vWT|anU}a$7)?n+7q9HdwB{QuOU4yBWfhj~oWPb2Rpaup{S3j3^P6<r_`_zG2 literal 0 HcmV?d00001 diff --git a/cinera/template_index.html b/cinera/template_index.html new file mode 100644 index 0000000..32d2c8b --- /dev/null +++ b/cinera/template_index.html @@ -0,0 +1,12 @@ +<!-- Index template --> +<html> + <head> + <title>RISCY BUSINESS Episode Guide + + + +

RISCY BUSINESS Episode Guide

+
This should just be the table of contents, heroes and heroines
+ + + diff --git a/cinera/template_player.html b/cinera/template_player.html new file mode 100644 index 0000000..489ae96 --- /dev/null +++ b/cinera/template_player.html @@ -0,0 +1,17 @@ + + + + <!-- __CINERA_TITLE__ --> - RISCY BUSINESS + + + + + Awesome Contents +

+ + + + + + +