Compare commits
	
		
			2 Commits
		
	
	
		
			14dafa4abe
			...
			e5ffec7a55
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | e5ffec7a55 | |
|  | a67a5ee34b | 
							
								
								
									
										842
									
								
								cinera/cinera.c
								
								
								
								
							
							
						
						
									
										842
									
								
								cinera/cinera.c
								
								
								
								
							|  | @ -23,7 +23,7 @@ typedef struct | ||||||
| version CINERA_APP_VERSION = { | version CINERA_APP_VERSION = { | ||||||
|     .Major = 0, |     .Major = 0, | ||||||
|     .Minor = 10, |     .Minor = 10, | ||||||
|     .Patch = 30 |     .Patch = 31 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| #define __USE_XOPEN2K8 // NOTE(matt): O_NOFOLLOW
 | #define __USE_XOPEN2K8 // NOTE(matt): O_NOFOLLOW
 | ||||||
|  | @ -8350,16 +8350,70 @@ BuildTimestampClass(buffer *TimestampClass, _memory_book(category_info) *LocalTo | ||||||
|     CopyStringToBuffer(TimestampClass, "\""); |     CopyStringToBuffer(TimestampClass, "\""); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void | bool | ||||||
| BuildCategoryIcons(buffer *CategoryIcons, _memory_book(category_info) *LocalTopics, _memory_book(category_info) *LocalMedia, string DefaultMedium, bool *RequiresCineraJS) | IsWhitespace(char C) | ||||||
| { | { | ||||||
|     bool CategoriesSpan = FALSE; |     return (C == ' ' || C == '\t' || C == '\n'); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool | ||||||
|  | ContainsWhitespace(string S) | ||||||
|  | { | ||||||
|  |     bool Result = FALSE; | ||||||
|  |     for(int i = 0; i < S.Length; ++i) | ||||||
|  |     { | ||||||
|  |         if(IsWhitespace(S.Base[i])) | ||||||
|  |         { | ||||||
|  |             Result = TRUE; | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return Result; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void | ||||||
|  | ConsumeWhitespace(buffer *B) | ||||||
|  | { | ||||||
|  |     while(B->Ptr - B->Location < B->Size && IsWhitespace(*B->Ptr)) | ||||||
|  |     { | ||||||
|  |         ++B->Ptr; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void | ||||||
|  | PushCategorySearchEntry(buffer *SearchEntry, HMML_Timestamp *Timestamp, category_info *Category, bool Underway) | ||||||
|  | { | ||||||
|  |     if(Underway || Timestamp->text[0]) | ||||||
|  |     { | ||||||
|  |         CopyStringToBuffer(SearchEntry, " "); | ||||||
|  |     } | ||||||
|  |     if(!Underway) | ||||||
|  |     { | ||||||
|  |         CopyStringToBuffer(SearchEntry, "["); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     CopyStringToBuffer(SearchEntry, ":"); | ||||||
|  |     bool NeedsQuoting = ContainsWhitespace(Category->Marker); | ||||||
|  |     if(NeedsQuoting) | ||||||
|  |     { | ||||||
|  |         CopyStringToBuffer(SearchEntry, "\""); | ||||||
|  |     } | ||||||
|  |     CopyStringToBufferNoFormat(SearchEntry, Category->Marker); | ||||||
|  |     if(NeedsQuoting) | ||||||
|  |     { | ||||||
|  |         CopyStringToBuffer(SearchEntry, "\""); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void | ||||||
|  | BuildCategoryIcons(buffer *SearchEntry, buffer *CategoryIcons, _memory_book(category_info) *LocalTopics, _memory_book(category_info) *LocalMedia, string DefaultMedium, bool *RequiresCineraJS, HMML_Timestamp *Timestamp) | ||||||
|  | { | ||||||
|  |     bool Underway = FALSE; | ||||||
|     category_info *FirstLocalTopic = GetPlaceInBook(LocalTopics, 0); |     category_info *FirstLocalTopic = GetPlaceInBook(LocalTopics, 0); | ||||||
|     category_info *FirstLocalMedium = GetPlaceInBook(LocalMedia, 0); |     category_info *FirstLocalMedium = GetPlaceInBook(LocalMedia, 0); | ||||||
|     if(!(LocalTopics->ItemCount == 1 && StringsMatch(FirstLocalTopic->Marker, Wrap0("nullTopic")) |     if(!(LocalTopics->ItemCount == 1 && StringsMatch(FirstLocalTopic->Marker, Wrap0("nullTopic")) | ||||||
|             && LocalMedia->ItemCount == 1 && StringsMatch(DefaultMedium, FirstLocalMedium->Marker))) |             && LocalMedia->ItemCount == 1 && StringsMatch(FirstLocalMedium->Marker, DefaultMedium))) | ||||||
|     { |     { | ||||||
|         CategoriesSpan = TRUE; |  | ||||||
|         CopyStringToBuffer(CategoryIcons, "<span class=\"cineraCategories\">"); |         CopyStringToBuffer(CategoryIcons, "<span class=\"cineraCategories\">"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -8368,6 +8422,14 @@ BuildCategoryIcons(buffer *CategoryIcons, _memory_book(category_info) *LocalTopi | ||||||
|         for(int i = 0; i < LocalTopics->ItemCount; ++i) |         for(int i = 0; i < LocalTopics->ItemCount; ++i) | ||||||
|         { |         { | ||||||
|             category_info *This = GetPlaceInBook(LocalTopics, i); |             category_info *This = GetPlaceInBook(LocalTopics, i); | ||||||
|  | 
 | ||||||
|  |             // .index
 | ||||||
|  |             if(SearchEntry) | ||||||
|  |             { | ||||||
|  |                 PushCategorySearchEntry(SearchEntry, Timestamp, This, Underway); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // .html
 | ||||||
|             // NOTE(matt): Stack-string
 |             // NOTE(matt): Stack-string
 | ||||||
|             char SanitisedMarker[This->Marker.Length + 1]; |             char SanitisedMarker[This->Marker.Length + 1]; | ||||||
|             CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)This->Marker.Length, This->Marker.Base); |             CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)This->Marker.Length, This->Marker.Base); | ||||||
|  | @ -8385,14 +8447,24 @@ BuildCategoryIcons(buffer *CategoryIcons, _memory_book(category_info) *LocalTopi | ||||||
|             } |             } | ||||||
|             CopyStringToBuffer(CategoryIcons, |             CopyStringToBuffer(CategoryIcons, | ||||||
|                     "></div>"); |                     "></div>"); | ||||||
|  | 
 | ||||||
|  |             Underway = TRUE; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if(!(LocalMedia->ItemCount == 1 && StringsMatch(DefaultMedium, FirstLocalMedium->Marker))) |     if(!(LocalMedia->ItemCount == 1 && StringsMatch(FirstLocalMedium->Marker, DefaultMedium))) | ||||||
|     { |     { | ||||||
|         for(int i = 0; i < LocalMedia->ItemCount; ++i) |         for(int i = 0; i < LocalMedia->ItemCount; ++i) | ||||||
|         { |         { | ||||||
|             category_info *This = GetPlaceInBook(LocalMedia, i); |             category_info *This = GetPlaceInBook(LocalMedia, i); | ||||||
|  | 
 | ||||||
|  |             // .index
 | ||||||
|  |             if(SearchEntry) | ||||||
|  |             { | ||||||
|  |                 PushCategorySearchEntry(SearchEntry, Timestamp, This, Underway); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // .html
 | ||||||
|             // NOTE(matt): Stack-string
 |             // NOTE(matt): Stack-string
 | ||||||
|             char SanitisedMarker[This->Marker.Length + 1]; |             char SanitisedMarker[This->Marker.Length + 1]; | ||||||
|             CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)This->Marker.Length, This->Marker.Base); |             CopyString(SanitisedMarker, sizeof(SanitisedMarker), "%.*s", (int)This->Marker.Length, This->Marker.Base); | ||||||
|  | @ -8409,11 +8481,20 @@ BuildCategoryIcons(buffer *CategoryIcons, _memory_book(category_info) *LocalTopi | ||||||
| 
 | 
 | ||||||
|                 CopyStringToBuffer(CategoryIcons, "</div>"); |                 CopyStringToBuffer(CategoryIcons, "</div>"); | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|  |             Underway = TRUE; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if(CategoriesSpan) |     if(Underway) | ||||||
|     { |     { | ||||||
|  |         // .index
 | ||||||
|  |         if(SearchEntry) | ||||||
|  |         { | ||||||
|  |             CopyStringToBuffer(SearchEntry, "]"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // .html
 | ||||||
|         CopyStringToBuffer(CategoryIcons, "</span>"); |         CopyStringToBuffer(CategoryIcons, "</span>"); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -8431,15 +8512,10 @@ String0ToInt(char *String) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| size_t | size_t | ||||||
| CurlIntoBuffer(char *InPtr, size_t CharLength, size_t Chars, char **OutputPtr) | CurlIntoBuffer(char *InPtr, size_t CharLength, size_t Chars, void *OutPtr) | ||||||
| { | { | ||||||
|     int Length = CharLength * Chars; |     size_t Length = CharLength * Chars; | ||||||
|     int i; |     CopyStringToBufferNoFormat(OutPtr, Wrap0i_(InPtr, Length)); | ||||||
|     for(i = 0; InPtr[i] && i < Length; ++i) |  | ||||||
|     { |  | ||||||
|         *((*OutputPtr)++) = InPtr[i]; |  | ||||||
|     } |  | ||||||
|     **OutputPtr = '\0'; |  | ||||||
|     return Length; |     return Length; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -8452,7 +8528,7 @@ CurlQuotes(buffer *QuoteStaging, char *QuotesURL) | ||||||
| 
 | 
 | ||||||
|     CURL *curl = curl_easy_init(); |     CURL *curl = curl_easy_init(); | ||||||
|     if(curl) { |     if(curl) { | ||||||
|         curl_easy_setopt(curl, CURLOPT_WRITEDATA, &QuoteStaging->Ptr); |         curl_easy_setopt(curl, CURLOPT_WRITEDATA, QuoteStaging); | ||||||
|         curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlIntoBuffer); |         curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlIntoBuffer); | ||||||
|         curl_easy_setopt(curl, CURLOPT_URL, QuotesURL); |         curl_easy_setopt(curl, CURLOPT_URL, QuotesURL); | ||||||
|         /*      */ MEM_TEST_MID(); |         /*      */ MEM_TEST_MID(); | ||||||
|  | @ -9225,21 +9301,6 @@ StripSurroundingSlashes(char *String) // NOTE(matt): For relative paths | ||||||
|     return Ptr; |     return Ptr; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| bool |  | ||||||
| IsWhitespace(char C) |  | ||||||
| { |  | ||||||
|     return (C == ' ' || C == '\t' || C == '\n'); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void |  | ||||||
| ConsumeWhitespace(buffer *B) |  | ||||||
| { |  | ||||||
|     while(B->Ptr - B->Location < B->Size && IsWhitespace(*B->Ptr)) |  | ||||||
|     { |  | ||||||
|         ++B->Ptr; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| string | string | ||||||
| StripPWDIndicators(string Path) | StripPWDIndicators(string Path) | ||||||
| { | { | ||||||
|  | @ -10654,8 +10715,8 @@ ClaimMenuIndexAndPlayerBuffers(menu_buffers *MenuBuffers, index_buffers *IndexBu | ||||||
|     if(ClaimBuffer(&IndexBuffers->Text, BID_INDEX_BUFFERS_TEXT, Kilobytes(4)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; }; |     if(ClaimBuffer(&IndexBuffers->Text, BID_INDEX_BUFFERS_TEXT, Kilobytes(4)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; }; | ||||||
|     if(ClaimBuffer(&IndexBuffers->CategoryIcons, BID_INDEX_BUFFERS_CATEGORY_ICONS, Kilobytes(1)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; }; |     if(ClaimBuffer(&IndexBuffers->CategoryIcons, BID_INDEX_BUFFERS_CATEGORY_ICONS, Kilobytes(1)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; }; | ||||||
| 
 | 
 | ||||||
|     if(ClaimBuffer(&PlayerBuffers->Menus, BID_PLAYER_BUFFERS_MENUS, Kilobytes(32)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; }; |     if(ClaimBuffer(&PlayerBuffers->Menus, BID_PLAYER_BUFFERS_MENUS, Kilobytes(64)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; }; | ||||||
|     if(ClaimBuffer(&PlayerBuffers->Main, BID_PLAYER_BUFFERS_MAIN, Kilobytes(512)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; }; |     if(ClaimBuffer(&PlayerBuffers->Main, BID_PLAYER_BUFFERS_MAIN, Megabytes(1)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; }; | ||||||
|     if(ClaimBuffer(&PlayerBuffers->Script, BID_PLAYER_BUFFERS_SCRIPT, Kilobytes(8)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; }; |     if(ClaimBuffer(&PlayerBuffers->Script, BID_PLAYER_BUFFERS_SCRIPT, Kilobytes(8)) == RC_ARENA_FULL) { Result = RC_ARENA_FULL; }; | ||||||
| 
 | 
 | ||||||
|     return Result; |     return Result; | ||||||
|  | @ -10867,113 +10928,288 @@ TimecodeIs(v4 Timecode, int Hours, int Minutes, int Seconds, int Milliseconds) | ||||||
|     return Timecode.Hours == Hours && Timecode.Minutes == Minutes && Timecode.Seconds == Seconds && Timecode.Milliseconds == Milliseconds; |     return Timecode.Hours == Hours && Timecode.Minutes == Minutes && Timecode.Seconds == Seconds && Timecode.Milliseconds == Milliseconds; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| rc | void | ||||||
| ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, memory_book *Strings, | ProcessTimestampCoda(buffer *SearchEntry, index_buffers *IndexBuffers) | ||||||
|         menu_buffers *MenuBuffers, index_buffers *IndexBuffers, player_buffers *PlayerBuffers, | { | ||||||
|         medium *DefaultMedium, speakers *Speakers, string Author, |     // .index
 | ||||||
|         _memory_book(ref_info) *ReferencesArray, |     CopyStringToBuffer(SearchEntry, "\"\n"); | ||||||
|         bool *HasQuoteMenu, bool *HasReferenceMenu, bool *HasFilterMenu, bool *RequiresCineraJS, | 
 | ||||||
|         int *QuoteIdentifier, int *RefIdentifier, |     // .html
 | ||||||
|         _memory_book(category_info) *Topics, _memory_book(category_info) *Media, |     CopyStringToBuffer(&IndexBuffers->Master, "</div>\n" | ||||||
|         HMML_Timestamp *Timestamp, v4 *PreviousTimecode) |             "                    </div>\n" | ||||||
|  |             "                </div>\n"); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | rc | ||||||
|  | ProcessTimestampCategories(neighbourhood *N, | ||||||
|  |         buffer *SearchEntry, index_buffers *IndexBuffers, | ||||||
|  |         medium *DefaultMedium, | ||||||
|  |         bool *HasFilterMenu, | ||||||
|  |         bool *RequiresCineraJS, | ||||||
|  |         _memory_book(category_info) *Topics, _memory_book(category_info) *LocalTopics, | ||||||
|  |         _memory_book(category_info) *Media, _memory_book(category_info) *LocalMedia, | ||||||
|  |         HMML_Timestamp *Timestamp, v4 Timecode, | ||||||
|  |         int *MarkerIndex, bool HasQuote, bool HasReference) | ||||||
| { | { | ||||||
|     MEM_TEST_TOP(); |  | ||||||
|     // TODO(matt): Introduce and use a SystemError() in here
 |  | ||||||
|     rc Result = RC_SUCCESS; |     rc Result = RC_SUCCESS; | ||||||
| 
 | 
 | ||||||
|     v4 Timecode = V4(Timestamp->h, Timestamp->m, Timestamp->s, Timestamp->ms); |     while(*MarkerIndex < Timestamp->marker_count) | ||||||
|     if(TimecodeToDottedSeconds(Timecode) >= TimecodeToDottedSeconds(*PreviousTimecode)) |  | ||||||
|     { |  | ||||||
|         *PreviousTimecode = Timecode; |  | ||||||
| 
 |  | ||||||
|         memory_book LocalTopics = InitBook(sizeof(category_info), 8); |  | ||||||
|         memory_book LocalMedia = InitBook(sizeof(category_info), 8); |  | ||||||
| 
 |  | ||||||
|         quote_info QuoteInfo = { }; |  | ||||||
| 
 |  | ||||||
|         bool HasQuote = FALSE; |  | ||||||
|         bool HasReference = FALSE; |  | ||||||
| 
 |  | ||||||
|         RewindBuffer(&IndexBuffers->Master); |  | ||||||
|         RewindBuffer(&IndexBuffers->Header); |  | ||||||
|         RewindBuffer(&IndexBuffers->Class); |  | ||||||
|         RewindBuffer(&IndexBuffers->Data); |  | ||||||
|         RewindBuffer(&IndexBuffers->Text); |  | ||||||
|         RewindBuffer(&IndexBuffers->CategoryIcons); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         CopyStringToBuffer(&IndexBuffers->Header, |  | ||||||
|                 "                <div data-timestamp=\"%.3f\"", |  | ||||||
|                 TimecodeToDottedSeconds(Timecode)); |  | ||||||
| 
 |  | ||||||
|         CopyStringToBuffer(&IndexBuffers->Class, |  | ||||||
|                 " class=\"marker"); |  | ||||||
| 
 |  | ||||||
|         speaker *Speaker = GetSpeaker(&Speakers->Speakers, Author); |  | ||||||
|         if(!IsCategorisedAFK(Timestamp)) |  | ||||||
|         { |  | ||||||
|             // NOTE(matt): I reckon it's fair to only cite the speaker when there are a multiple of them
 |  | ||||||
|             if(Speakers->Speakers.ItemCount > 1 && Speaker && !IsCategorisedAuthored(Timestamp)) |  | ||||||
|             { |  | ||||||
|                 string DisplayName = !Speaker->Seen ? Speaker->Person->Name : Speaker->Person->Abbreviations[Speakers->AbbrevScheme]; |  | ||||||
| 
 |  | ||||||
|                 CopyStringToBuffer(&IndexBuffers->Text, |  | ||||||
|                         "<span class=\"author\" data-hue=\"%d\" data-saturation=\"%d%%\"", |  | ||||||
|                         Speaker->Colour.Hue, |  | ||||||
|                         Speaker->Colour.Saturation); |  | ||||||
| 
 |  | ||||||
|                 if(Speaker->Seen) |  | ||||||
|                 { |  | ||||||
|                     CopyStringToBuffer(&IndexBuffers->Text, " title=\""); |  | ||||||
|                     CopyStringToBufferHTMLSafe(&IndexBuffers->Text, Speaker->Person->Name); |  | ||||||
|                     CopyStringToBuffer(&IndexBuffers->Text, "\""); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 CopyStringToBuffer(&IndexBuffers->Text, |  | ||||||
|                         ">%.*s</span>: ", (int)DisplayName.Length, DisplayName.Base); |  | ||||||
| 
 |  | ||||||
|                 Speaker->Seen = TRUE; |  | ||||||
|             } |  | ||||||
|             else if(Author.Length > 0) |  | ||||||
|             { |  | ||||||
|                 if(!*HasFilterMenu) |  | ||||||
|                 { |  | ||||||
|                     *HasFilterMenu = TRUE; |  | ||||||
|                 } |  | ||||||
|                 InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0("authored"), NULL); |  | ||||||
|                 hsl_colour AuthorColour; |  | ||||||
|                 StringToColourHash(&AuthorColour, Author); |  | ||||||
|                 // TODO(matt): That EDITION_NETWORK site database API-polling stuff
 |  | ||||||
|                 CopyStringToBuffer(&IndexBuffers->Text, |  | ||||||
|                         "<span class=\"author\" data-hue=\"%d\" data-saturation=\"%d%%\">%.*s</span> ", |  | ||||||
|                         AuthorColour.Hue, AuthorColour.Saturation, |  | ||||||
|                         (int)Author.Length, Author.Base); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         char *InPtr = Timestamp->text; |  | ||||||
| 
 |  | ||||||
|         int MarkerIndex = 0, RefIndex = 0; |  | ||||||
|         while(*InPtr || RefIndex < Timestamp->reference_count) |  | ||||||
|         { |  | ||||||
|             if(MarkerIndex < Timestamp->marker_count && |  | ||||||
|                     InPtr - Timestamp->text == Timestamp->markers[MarkerIndex].offset) |  | ||||||
|             { |  | ||||||
|                 char *Readable = Timestamp->markers[MarkerIndex].parameter |  | ||||||
|                     ? Timestamp->markers[MarkerIndex].parameter |  | ||||||
|                     : Timestamp->markers[MarkerIndex].marker; |  | ||||||
|                 HMML_MarkerType Type = Timestamp->markers[MarkerIndex].type; |  | ||||||
|                 if(Type == HMML_CATEGORY) |  | ||||||
|     { |     { | ||||||
|         hsl_colour TopicColour = {}; |         hsl_colour TopicColour = {}; | ||||||
|                     Result = GenerateTopicColours(N, Wrap0(Timestamp->markers[MarkerIndex].marker), &TopicColour); |         Result = GenerateTopicColours(N, Wrap0(Timestamp->markers[*MarkerIndex].marker), &TopicColour); | ||||||
|         if(Result == RC_SUCCESS) |         if(Result == RC_SUCCESS) | ||||||
|         { |         { | ||||||
|             if(!*HasFilterMenu) |             if(!*HasFilterMenu) | ||||||
|             { |             { | ||||||
|                 *HasFilterMenu = TRUE; |                 *HasFilterMenu = TRUE; | ||||||
|             } |             } | ||||||
|                         InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0(Timestamp->markers[MarkerIndex].marker), &TopicColour); |             InsertCategory(Topics, LocalTopics, Media, LocalMedia, Wrap0(Timestamp->markers[*MarkerIndex].marker), &TopicColour); | ||||||
|  |             ++*MarkerIndex; | ||||||
|  |         } | ||||||
|  |         else | ||||||
|  |         { | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if(Result == RC_SUCCESS) | ||||||
|  |     { | ||||||
|  |         if(LocalTopics->ItemCount == 0) | ||||||
|  |         { | ||||||
|  |             hsl_colour TopicColour = {}; | ||||||
|  |             Result = GenerateTopicColours(N, Wrap0("nullTopic"), &TopicColour); | ||||||
|  |             if(Result == RC_SUCCESS) | ||||||
|  |             { | ||||||
|  |                 InsertCategory(Topics, LocalTopics, Media, LocalMedia, Wrap0("nullTopic"), &TopicColour); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if(Result == RC_SUCCESS) | ||||||
|  |         { | ||||||
|  |             if(LocalMedia->ItemCount == 0) | ||||||
|  |             { | ||||||
|  |                 InsertCategory(Topics, LocalTopics, Media, LocalMedia, DefaultMedium->ID, NULL); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             BuildTimestampClass(&IndexBuffers->Class, LocalTopics, LocalMedia, DefaultMedium->ID); | ||||||
|  |             CopyLandmarkedBuffer(&IndexBuffers->Header, &IndexBuffers->Class, 0, PAGE_PLAYER); | ||||||
|  | 
 | ||||||
|  |             if(HasQuote || HasReference) | ||||||
|  |             { | ||||||
|  |                 CopyStringToBuffer(&IndexBuffers->Data, "\""); | ||||||
|  |                 CopyLandmarkedBuffer(&IndexBuffers->Header, &IndexBuffers->Data, 0, PAGE_PLAYER); | ||||||
|  |             } | ||||||
|  |             CopyStringToBuffer(&IndexBuffers->Header, ">\n"); | ||||||
|  | 
 | ||||||
|  |             CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Header, 0, PAGE_PLAYER); | ||||||
|  |             CopyStringToBuffer(&IndexBuffers->Master, | ||||||
|  |                     "                    <div class=\"cineraContent\"><span class=\"timecode\">"); | ||||||
|  |             CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode); | ||||||
|  |             CopyStringToBuffer(&IndexBuffers->Master, "</span>"); | ||||||
|  | 
 | ||||||
|  |             CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Text, 0, PAGE_PLAYER); | ||||||
|  | 
 | ||||||
|  |             if(LocalTopics->ItemCount > 0) | ||||||
|  |             { | ||||||
|  |                 BuildCategoryIcons(SearchEntry, &IndexBuffers->Master, LocalTopics, LocalMedia, DefaultMedium->ID, RequiresCineraJS, Timestamp); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             CopyStringToBuffer(&IndexBuffers->Master, "</div>\n" | ||||||
|  |                     "                    <div class=\"progress faded\">\n" | ||||||
|  |                     "                        <div class=\"cineraContent\"><span class=\"timecode\">"); | ||||||
|  |             CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode); | ||||||
|  |             CopyStringToBuffer(&IndexBuffers->Master, "</span>"); | ||||||
|  | 
 | ||||||
|  |             CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Text, 0, PAGE_PLAYER); | ||||||
|  | 
 | ||||||
|  |             if(LocalTopics->ItemCount > 0) | ||||||
|  |             { | ||||||
|  |                 BuildCategoryIcons(0, &IndexBuffers->Master, LocalTopics, LocalMedia, DefaultMedium->ID, RequiresCineraJS, Timestamp); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             CopyStringToBuffer(&IndexBuffers->Master, "</div>\n" | ||||||
|  |                     "                    </div>\n" | ||||||
|  |                     "                    <div class=\"progress main\">\n" | ||||||
|  |                     "                        <div class=\"cineraContent\"><span class=\"timecode\">"); | ||||||
|  |             CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode); | ||||||
|  |             CopyStringToBuffer(&IndexBuffers->Master, "</span>"); | ||||||
|  | 
 | ||||||
|  |             CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Text, 0, PAGE_PLAYER); | ||||||
|  | 
 | ||||||
|  |             if(LocalTopics->ItemCount > 0) | ||||||
|  |             { | ||||||
|  |                 BuildCategoryIcons(0, &IndexBuffers->Master, LocalTopics, LocalMedia, DefaultMedium->ID, RequiresCineraJS, Timestamp); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return Result; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | rc | ||||||
|  | ProcessTimestampQuoting(buffer *SearchEntry, string Filepath, memory_book *Strings, | ||||||
|  |         menu_buffers *MenuBuffers, index_buffers *IndexBuffers, | ||||||
|  |         speakers *Speakers, speaker *Speaker, string Author, | ||||||
|  |         bool *HasQuoteMenu, int *QuoteIdentifier, | ||||||
|  |         bool *HasQuote, bool HasReference, | ||||||
|  |         HMML_Timestamp *Timestamp, v4 Timecode) | ||||||
|  | { | ||||||
|  |     rc Result = RC_SUCCESS; | ||||||
|  |     if(Timestamp->quote.present) | ||||||
|  |     { | ||||||
|  |         if(!*HasQuoteMenu) | ||||||
|  |         { | ||||||
|  |             CopyStringToBuffer(&MenuBuffers->Quote, | ||||||
|  |                     "            <div class=\"menu quotes\">\n" | ||||||
|  |                     "                <span>Quotes ▼</span>\n" | ||||||
|  |                     "                <div class=\"refs quotes_container\">\n"); | ||||||
|  | 
 | ||||||
|  |             *HasQuoteMenu = TRUE; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if(!HasReference) | ||||||
|  |         { | ||||||
|  |             CopyStringToBuffer(&IndexBuffers->Data, " data-ref=\"&#%d;", *QuoteIdentifier); | ||||||
|  |         } | ||||||
|  |         else | ||||||
|  |         { | ||||||
|  |             CopyStringToBuffer(&IndexBuffers->Data, ",&#%d;", *QuoteIdentifier); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         *HasQuote = TRUE; | ||||||
|  | 
 | ||||||
|  |         bool ShouldFetchQuotes = FALSE; | ||||||
|  |         if(Config->CacheDir.Length == 0 || time(0) - LastQuoteFetch > 60*60) | ||||||
|  |         { | ||||||
|  |             ShouldFetchQuotes = TRUE; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if(!Speaker && Speakers->Speakers.ItemCount > 0) | ||||||
|  |         { | ||||||
|  |             Speaker = GetPlaceInBook(&Speakers->Speakers, 0); | ||||||
|  |         } | ||||||
|  |         string QuoteUsername; | ||||||
|  |         if(Timestamp->quote.author) | ||||||
|  |         { | ||||||
|  |             QuoteUsername = Wrap0(Timestamp->quote.author); | ||||||
|  |         } | ||||||
|  |         else if(Speaker) | ||||||
|  |         { | ||||||
|  |             QuoteUsername = Speaker->Person->QuoteUsername; | ||||||
|  |         } | ||||||
|  |         else | ||||||
|  |         { | ||||||
|  |             QuoteUsername = Author; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         quote_info QuoteInfo = { }; | ||||||
|  |         /*      */ MEM_TEST_MID(); | ||||||
|  |         /* +MEM */ Result = BuildQuote(Strings, &QuoteInfo, | ||||||
|  |                 QuoteUsername, Timestamp->quote.id, ShouldFetchQuotes); | ||||||
|  |         /*      */ MEM_TEST_MID(); | ||||||
|  |         if(Result == RC_SUCCESS) | ||||||
|  |         { | ||||||
|  |             CopyStringToBuffer(&MenuBuffers->Quote, | ||||||
|  |                     "                    <a target=\"_blank\" data-id=\"&#%d;\" class=\"ref\" href=\"https://dev.abaines.me.uk/quotes/%.*s/%d\">\n" | ||||||
|  |                     "                        <span>\n" | ||||||
|  |                     "                            <span class=\"ref_content\">\n" | ||||||
|  |                     "                                <div class=\"source\">Quote %d</div>\n" | ||||||
|  |                     "                                <div class=\"ref_title\">", | ||||||
|  |                     *QuoteIdentifier, | ||||||
|  |                     (int)QuoteUsername.Length, QuoteUsername.Base, | ||||||
|  |                     Timestamp->quote.id, | ||||||
|  |                     Timestamp->quote.id); | ||||||
|  | 
 | ||||||
|  |             CopyStringToBufferHTMLSafe(&MenuBuffers->Quote, QuoteInfo.Text); | ||||||
|  | 
 | ||||||
|  |             string DateString = UnixTimeToDateString(Strings, QuoteInfo.Date); | ||||||
|  |             CopyStringToBuffer(&MenuBuffers->Quote, "</div>\n" | ||||||
|  |                     "                                <div class=\"quote_byline\">—%.*s, %.*s</div>\n" | ||||||
|  |                     "                            </span>\n" | ||||||
|  |                     "                            <div class=\"ref_indices\">\n" | ||||||
|  |                     "                                <span data-timestamp=\"%.3f\" class=\"timecode\"><span class=\"ref_index\">[&#%d;]</span><span class=\"time\">", | ||||||
|  |                     (int)QuoteUsername.Length, QuoteUsername.Base, | ||||||
|  |                     (int)DateString.Length, DateString.Base, // TODO(matt): Convert Unixtime to date-string
 | ||||||
|  |                     TimecodeToDottedSeconds(Timecode), | ||||||
|  |                     *QuoteIdentifier); | ||||||
|  |             CopyTimecodeToBuffer(&MenuBuffers->Quote, Timecode); | ||||||
|  |             CopyStringToBuffer(&MenuBuffers->Quote, "</span></span>\n" | ||||||
|  |                     "                            </div>\n" | ||||||
|  |                     "                        </span>\n" | ||||||
|  |                     "                    </a>\n"); | ||||||
|  |             if(!Timestamp->text[0]) | ||||||
|  |             { | ||||||
|  |                 // .index
 | ||||||
|  |                 CopyStringToBuffer(SearchEntry, "\u201C"); | ||||||
|  |                 CopyStringToBufferNoFormat(SearchEntry, QuoteInfo.Text); | ||||||
|  |                 CopyStringToBuffer(SearchEntry, "\u201D"); | ||||||
|  | 
 | ||||||
|  |                 // .html
 | ||||||
|  |                 CopyStringToBuffer(&IndexBuffers->Text, "“"); | ||||||
|  |                 CopyStringToBufferHTMLSafe(&IndexBuffers->Text, QuoteInfo.Text); | ||||||
|  |                 CopyStringToBuffer(&IndexBuffers->Text, "”"); | ||||||
|  |             } | ||||||
|  |             else | ||||||
|  |             { | ||||||
|  |                 // NOTE(matt): Here accounting for ProcessTimestampText() not writing to .index
 | ||||||
|  | 
 | ||||||
|  |                 // .index
 | ||||||
|  |                 CopyStringToBufferNoFormat(SearchEntry, Wrap0(Timestamp->text)); | ||||||
|  |             } | ||||||
|  |             CopyStringToBuffer(&IndexBuffers->Text, "<sup>&#%d;</sup>", *QuoteIdentifier); | ||||||
|  |             ++*QuoteIdentifier; | ||||||
|  |         } | ||||||
|  |         else if(Result == RC_UNFOUND) | ||||||
|  |         { | ||||||
|  |             IndexingQuoteError(&Filepath, Timestamp->line, QuoteUsername, Timestamp->quote.id); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     else | ||||||
|  |     { | ||||||
|  |         // .index
 | ||||||
|  |         CopyStringToBufferNoFormat(SearchEntry, Wrap0(Timestamp->text)); | ||||||
|  |     } | ||||||
|  |     return Result; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | rc | ||||||
|  | ProcessTimestampText(neighbourhood *N, string Filepath, memory_book *Strings, | ||||||
|  |         menu_buffers *MenuBuffers, index_buffers *IndexBuffers, | ||||||
|  |         _memory_book(ref_info) *ReferencesArray, | ||||||
|  |         bool *HasReferenceMenu, bool *HasFilterMenu, | ||||||
|  |         int *RefIdentifier, | ||||||
|  |         _memory_book(category_info) *Topics, _memory_book(category_info) *LocalTopics, | ||||||
|  |         _memory_book(category_info) *Media, _memory_book(category_info) *LocalMedia, | ||||||
|  |         HMML_Timestamp *Timestamp, | ||||||
|  |         bool *HasReference, v4 Timecode, int *MarkerIndex) | ||||||
|  | { | ||||||
|  |     // NOTE(matt):  While this function processes the Timestamp->text for the benefit of both the .html and .index sides, it
 | ||||||
|  |     //              only writes out .html, meaning we need to write the .index file elsewhere in ProcessTimestampQuoting().
 | ||||||
|  |     rc Result = RC_SUCCESS; | ||||||
|  | 
 | ||||||
|  |     int RefIndex = 0; | ||||||
|  |     char *InPtr = Timestamp->text; | ||||||
|  |     while(*InPtr || RefIndex < Timestamp->reference_count) | ||||||
|  |     { | ||||||
|  |         if(*MarkerIndex < Timestamp->marker_count && | ||||||
|  |                 InPtr - Timestamp->text == Timestamp->markers[*MarkerIndex].offset) | ||||||
|  |         { | ||||||
|  |             char *Readable = Timestamp->markers[*MarkerIndex].parameter | ||||||
|  |                 ? Timestamp->markers[*MarkerIndex].parameter | ||||||
|  |                 : Timestamp->markers[*MarkerIndex].marker; | ||||||
|  |             HMML_MarkerType Type = Timestamp->markers[*MarkerIndex].type; | ||||||
|  |             if(Type == HMML_CATEGORY) | ||||||
|  |             { | ||||||
|  |                 hsl_colour TopicColour = {}; | ||||||
|  |                 Result = GenerateTopicColours(N, Wrap0(Timestamp->markers[*MarkerIndex].marker), &TopicColour); | ||||||
|  |                 if(Result == RC_SUCCESS) | ||||||
|  |                 { | ||||||
|  |                     if(!*HasFilterMenu) | ||||||
|  |                     { | ||||||
|  |                         *HasFilterMenu = TRUE; | ||||||
|  |                     } | ||||||
|  |                     InsertCategory(Topics, LocalTopics, Media, LocalMedia, Wrap0(Timestamp->markers[*MarkerIndex].marker), &TopicColour); | ||||||
|                     CopyStringToBuffer(&IndexBuffers->Text, "%.*s", (int)StringLength(Readable), InPtr); |                     CopyStringToBuffer(&IndexBuffers->Text, "%.*s", (int)StringLength(Readable), InPtr); | ||||||
|                 } |                 } | ||||||
|                 else |                 else | ||||||
|  | @ -10985,16 +11221,16 @@ ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, m | ||||||
|             { |             { | ||||||
|                 // TODO(matt): That EDITION_NETWORK site database API-polling stuff
 |                 // TODO(matt): That EDITION_NETWORK site database API-polling stuff
 | ||||||
|                 hsl_colour Colour; |                 hsl_colour Colour; | ||||||
|                     StringToColourHash(&Colour, Wrap0(Timestamp->markers[MarkerIndex].marker)); |                 StringToColourHash(&Colour, Wrap0(Timestamp->markers[*MarkerIndex].marker)); | ||||||
|                 CopyStringToBuffer(&IndexBuffers->Text, |                 CopyStringToBuffer(&IndexBuffers->Text, | ||||||
|                         "<span class=\"%s\" data-hue=\"%d\" data-saturation=\"%d%%\">%.*s</span>", |                         "<span class=\"%s\" data-hue=\"%d\" data-saturation=\"%d%%\">%.*s</span>", | ||||||
|                             Timestamp->markers[MarkerIndex].type == HMML_MEMBER ? "member" : "project", |                         Timestamp->markers[*MarkerIndex].type == HMML_MEMBER ? "member" : "project", | ||||||
|                         Colour.Hue, Colour.Saturation, |                         Colour.Hue, Colour.Saturation, | ||||||
|                         (int)StringLength(Readable), InPtr); |                         (int)StringLength(Readable), InPtr); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             InPtr += StringLength(Readable); |             InPtr += StringLength(Readable); | ||||||
|                 ++MarkerIndex; |             ++*MarkerIndex; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if(Result == RC_SUCCESS) |         if(Result == RC_SUCCESS) | ||||||
|  | @ -11035,7 +11271,7 @@ ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, m | ||||||
|                                 IndexingError(Filepath, Timestamp->line, S_ERROR, |                                 IndexingError(Filepath, Timestamp->line, S_ERROR, | ||||||
|                                         "Cannot process new combination of reference info\n" |                                         "Cannot process new combination of reference info\n" | ||||||
|                                         "\n" |                                         "\n" | ||||||
|                                             "Either tweak your timestamp, or contact miblodelcarpio@gmail.com\n" |                                         "Either tweak your timestamp, or write to contact@miblo.net\n" | ||||||
|                                         "mentioning the ref node you want to write and how you want it to\n" |                                         "mentioning the ref node you want to write and how you want it to\n" | ||||||
|                                         "appear in the references menu", |                                         "appear in the references menu", | ||||||
|                                         0); |                                         0); | ||||||
|  | @ -11045,8 +11281,8 @@ ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, m | ||||||
|                     break; // NOTE(matt): Out of the while()
 |                     break; // NOTE(matt): Out of the while()
 | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                     CopyStringToBuffer(&IndexBuffers->Data, "%s%s", !HasReference ? " data-ref=\"" : "," , This->ID); |                 CopyStringToBuffer(&IndexBuffers->Data, "%s%s", !*HasReference ? " data-ref=\"" : "," , This->ID); | ||||||
|                     HasReference = TRUE; |                 *HasReference = TRUE; | ||||||
|                 CopyStringToBuffer(&IndexBuffers->Text, "<sup>%s%d</sup>", |                 CopyStringToBuffer(&IndexBuffers->Text, "<sup>%s%d</sup>", | ||||||
|                         RefIndex > 0 && Timestamp->references[RefIndex].offset == Timestamp->references[RefIndex-1].offset ? "," : "", |                         RefIndex > 0 && Timestamp->references[RefIndex].offset == Timestamp->references[RefIndex-1].offset ? "," : "", | ||||||
|                         *RefIdentifier); |                         *RefIdentifier); | ||||||
|  | @ -11089,217 +11325,171 @@ ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, m | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|         if(Result == RC_SUCCESS) |     return Result; | ||||||
|         { | } | ||||||
|             if(Timestamp->quote.present) |  | ||||||
|             { |  | ||||||
|                 if(!*HasQuoteMenu) |  | ||||||
|                 { |  | ||||||
|                     CopyStringToBuffer(&MenuBuffers->Quote, |  | ||||||
|                             "            <div class=\"menu quotes\">\n" |  | ||||||
|                             "                <span>Quotes ▼</span>\n" |  | ||||||
|                             "                <div class=\"refs quotes_container\">\n"); |  | ||||||
| 
 | 
 | ||||||
|                     *HasQuoteMenu = TRUE; | speaker * | ||||||
|  | ProcessTimestampAuthoring(buffer *SearchEntry, index_buffers *IndexBuffers, | ||||||
|  |         speakers *Speakers, string Author, bool *HasFilterMenu, | ||||||
|  |         _memory_book(category_info) *Topics, _memory_book(category_info) *LocalTopics, | ||||||
|  |         _memory_book(category_info) *Media, _memory_book(category_info) *LocalMedia, | ||||||
|  |         HMML_Timestamp *Timestamp) | ||||||
|  | { | ||||||
|  |     speaker *Speaker = GetSpeaker(&Speakers->Speakers, Author); | ||||||
|  | 
 | ||||||
|  |     if(!IsCategorisedAFK(Timestamp)) | ||||||
|  |     { | ||||||
|  |         // NOTE(matt): I reckon it's fair to only cite the speaker when there are a multiple of them
 | ||||||
|  |         if(Speakers->Speakers.ItemCount > 1 && Speaker && !IsCategorisedAuthored(Timestamp)) | ||||||
|  |         { | ||||||
|  |             // .index
 | ||||||
|  |             CopyStringToBufferNoFormat(SearchEntry, Speaker->Person->Name); | ||||||
|  |             CopyStringToBuffer(SearchEntry, ": "); | ||||||
|  | 
 | ||||||
|  |             // .html
 | ||||||
|  |             string DisplayName = !Speaker->Seen ? Speaker->Person->Name : Speaker->Person->Abbreviations[Speakers->AbbrevScheme]; | ||||||
|  | 
 | ||||||
|  |             CopyStringToBuffer(&IndexBuffers->Text, | ||||||
|  |                     "<span class=\"author\" data-hue=\"%d\" data-saturation=\"%d%%\"", | ||||||
|  |                     Speaker->Colour.Hue, | ||||||
|  |                     Speaker->Colour.Saturation); | ||||||
|  | 
 | ||||||
|  |             if(Speaker->Seen) | ||||||
|  |             { | ||||||
|  |                 CopyStringToBuffer(&IndexBuffers->Text, " title=\""); | ||||||
|  |                 CopyStringToBufferHTMLSafe(&IndexBuffers->Text, Speaker->Person->Name); | ||||||
|  |                 CopyStringToBuffer(&IndexBuffers->Text, "\""); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|                 if(!HasReference) |             CopyStringToBuffer(&IndexBuffers->Text, | ||||||
|                 { |                     ">%.*s</span>: ", (int)DisplayName.Length, DisplayName.Base); | ||||||
|                     CopyStringToBuffer(&IndexBuffers->Data, " data-ref=\"&#%d;", *QuoteIdentifier); |  | ||||||
|                 } |  | ||||||
|                 else |  | ||||||
|                 { |  | ||||||
|                     CopyStringToBuffer(&IndexBuffers->Data, ",&#%d;", *QuoteIdentifier); |  | ||||||
|                 } |  | ||||||
| 
 | 
 | ||||||
|                 HasQuote = TRUE; |             Speaker->Seen = TRUE; | ||||||
|  |         } | ||||||
|  |         else if(Author.Length > 0) | ||||||
|  |         { | ||||||
|  |             // .index
 | ||||||
|  |             CopyStringToBuffer(SearchEntry, "@"); | ||||||
|  |             CopyStringToBufferNoFormat(SearchEntry, Author); | ||||||
|  |             CopyStringToBuffer(SearchEntry, ": "); | ||||||
| 
 | 
 | ||||||
|                 bool ShouldFetchQuotes = FALSE; |             // .html
 | ||||||
|                 if(Config->CacheDir.Length == 0 || time(0) - LastQuoteFetch > 60*60) |  | ||||||
|                 { |  | ||||||
|                     ShouldFetchQuotes = TRUE; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 if(!Speaker && Speakers->Speakers.ItemCount > 0) |  | ||||||
|                 { |  | ||||||
|                     Speaker = GetPlaceInBook(&Speakers->Speakers, 0); |  | ||||||
|                 } |  | ||||||
|                 string QuoteUsername; |  | ||||||
|                 if(Timestamp->quote.author) |  | ||||||
|                 { |  | ||||||
|                     QuoteUsername = Wrap0(Timestamp->quote.author); |  | ||||||
|                 } |  | ||||||
|                 else if(Speaker) |  | ||||||
|                 { |  | ||||||
|                     QuoteUsername = Speaker->Person->QuoteUsername; |  | ||||||
|                 } |  | ||||||
|                 else |  | ||||||
|                 { |  | ||||||
|                     QuoteUsername = Author; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 /*      */ MEM_TEST_MID(); |  | ||||||
|                 /* +MEM */ Result = BuildQuote(Strings, &QuoteInfo, |  | ||||||
|                         QuoteUsername, Timestamp->quote.id, ShouldFetchQuotes); |  | ||||||
|                 /*      */ MEM_TEST_MID(); |  | ||||||
|                 if(Result == RC_SUCCESS) |  | ||||||
|                 { |  | ||||||
|                     CopyStringToBuffer(&MenuBuffers->Quote, |  | ||||||
|                             "                    <a target=\"_blank\" data-id=\"&#%d;\" class=\"ref\" href=\"https://dev.abaines.me.uk/quotes/%.*s/%d\">\n" |  | ||||||
|                             "                        <span>\n" |  | ||||||
|                             "                            <span class=\"ref_content\">\n" |  | ||||||
|                             "                                <div class=\"source\">Quote %d</div>\n" |  | ||||||
|                             "                                <div class=\"ref_title\">", |  | ||||||
|                             *QuoteIdentifier, |  | ||||||
|                             (int)QuoteUsername.Length, QuoteUsername.Base, |  | ||||||
|                             Timestamp->quote.id, |  | ||||||
|                             Timestamp->quote.id); |  | ||||||
| 
 |  | ||||||
|                     CopyStringToBufferHTMLSafe(&MenuBuffers->Quote, QuoteInfo.Text); |  | ||||||
| 
 |  | ||||||
|                     string DateString = UnixTimeToDateString(Strings, QuoteInfo.Date); |  | ||||||
|                     CopyStringToBuffer(&MenuBuffers->Quote, "</div>\n" |  | ||||||
|                             "                                <div class=\"quote_byline\">—%.*s, %.*s</div>\n" |  | ||||||
|                             "                            </span>\n" |  | ||||||
|                             "                            <div class=\"ref_indices\">\n" |  | ||||||
|                             "                                <span data-timestamp=\"%.3f\" class=\"timecode\"><span class=\"ref_index\">[&#%d;]</span><span class=\"time\">", |  | ||||||
|                             (int)QuoteUsername.Length, QuoteUsername.Base, |  | ||||||
|                             (int)DateString.Length, DateString.Base, // TODO(matt): Convert Unixtime to date-string
 |  | ||||||
|                             TimecodeToDottedSeconds(Timecode), |  | ||||||
|                             *QuoteIdentifier); |  | ||||||
|                     CopyTimecodeToBuffer(&MenuBuffers->Quote, Timecode); |  | ||||||
|                     CopyStringToBuffer(&MenuBuffers->Quote, "</span></span>\n" |  | ||||||
|                             "                            </div>\n" |  | ||||||
|                             "                        </span>\n" |  | ||||||
|                             "                    </a>\n"); |  | ||||||
|                     if(!Timestamp->text[0]) |  | ||||||
|                     { |  | ||||||
|                         CopyStringToBuffer(&IndexBuffers->Text, "“"); |  | ||||||
|                         CopyStringToBufferHTMLSafe(&IndexBuffers->Text, QuoteInfo.Text); |  | ||||||
|                         CopyStringToBuffer(&IndexBuffers->Text, "”"); |  | ||||||
|                     } |  | ||||||
|                     CopyStringToBuffer(&IndexBuffers->Text, "<sup>&#%d;</sup>", *QuoteIdentifier); |  | ||||||
|                     ++*QuoteIdentifier; |  | ||||||
|                 } |  | ||||||
|                 else if(Result == RC_UNFOUND) |  | ||||||
|                 { |  | ||||||
|                     IndexingQuoteError(&Filepath, Timestamp->line, QuoteUsername, Timestamp->quote.id); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if(Result == RC_SUCCESS) |  | ||||||
|             { |  | ||||||
|                 CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"%.3f\": \"", TimecodeToDottedSeconds(Timecode)); |  | ||||||
|                 if(Timestamp->quote.present && !Timestamp->text[0]) |  | ||||||
|                 { |  | ||||||
|                     CopyStringToBuffer(&CollationBuffers->SearchEntry, "\u201C"); |  | ||||||
|                     CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, QuoteInfo.Text); |  | ||||||
|                     CopyStringToBuffer(&CollationBuffers->SearchEntry, "\u201D"); |  | ||||||
|                 } |  | ||||||
|                 else |  | ||||||
|                 { |  | ||||||
|                     CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, Wrap0(Timestamp->text)); |  | ||||||
|                 } |  | ||||||
|                 CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\n"); |  | ||||||
| 
 |  | ||||||
|                 while(MarkerIndex < Timestamp->marker_count) |  | ||||||
|                 { |  | ||||||
|                     hsl_colour TopicColour = {}; |  | ||||||
|                     Result = GenerateTopicColours(N, Wrap0(Timestamp->markers[MarkerIndex].marker), &TopicColour); |  | ||||||
|                     if(Result == RC_SUCCESS) |  | ||||||
|                     { |  | ||||||
|             if(!*HasFilterMenu) |             if(!*HasFilterMenu) | ||||||
|             { |             { | ||||||
|                 *HasFilterMenu = TRUE; |                 *HasFilterMenu = TRUE; | ||||||
|             } |             } | ||||||
|                         InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0(Timestamp->markers[MarkerIndex].marker), &TopicColour); |             InsertCategory(Topics, LocalTopics, Media, LocalMedia, Wrap0("authored"), NULL); | ||||||
|                         ++MarkerIndex; |             hsl_colour AuthorColour; | ||||||
|  |             StringToColourHash(&AuthorColour, Author); | ||||||
|  |             // TODO(matt): That EDITION_NETWORK site database API-polling stuff
 | ||||||
|  |             CopyStringToBuffer(&IndexBuffers->Text, | ||||||
|  |                     "<span class=\"author\" data-hue=\"%d\" data-saturation=\"%d%%\">@%.*s</span> ", | ||||||
|  |                     AuthorColour.Hue, AuthorColour.Saturation, | ||||||
|  |                     (int)Author.Length, Author.Base); | ||||||
|         } |         } | ||||||
|                     else |     } | ||||||
|  | 
 | ||||||
|  |     return Speaker; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void | ||||||
|  | ProcessTimestampTiming(buffer *SearchEntry, index_buffers *IndexBuffers, v4 Timecode) | ||||||
|  | { | ||||||
|  |     float Time = TimecodeToDottedSeconds(Timecode); | ||||||
|  | 
 | ||||||
|  |     // .index
 | ||||||
|  |     CopyStringToBuffer(SearchEntry, "\"%.3f\": \"", Time); | ||||||
|  | 
 | ||||||
|  |     // .html
 | ||||||
|  |     CopyStringToBuffer(&IndexBuffers->Header, | ||||||
|  |             "                <div data-timestamp=\"%.3f\"", Time); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | rc | ||||||
|  | ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, memory_book *Strings, | ||||||
|  |         menu_buffers *MenuBuffers, index_buffers *IndexBuffers, player_buffers *PlayerBuffers, | ||||||
|  |         medium *DefaultMedium, speakers *Speakers, string Author, | ||||||
|  |         _memory_book(ref_info) *ReferencesArray, | ||||||
|  |         bool *HasQuoteMenu, bool *HasReferenceMenu, bool *HasFilterMenu, bool *RequiresCineraJS, | ||||||
|  |         int *QuoteIdentifier, int *RefIdentifier, | ||||||
|  |         _memory_book(category_info) *Topics, _memory_book(category_info) *Media, | ||||||
|  |         HMML_Timestamp *Timestamp, v4 *PreviousTimecode) | ||||||
|  | { | ||||||
|  |     MEM_TEST_TOP(); | ||||||
|  |     // TODO(matt): Introduce and use a SystemError() in here
 | ||||||
|  |     rc Result = RC_SUCCESS; | ||||||
|  | 
 | ||||||
|  |     v4 Timecode = V4(Timestamp->h, Timestamp->m, Timestamp->s, Timestamp->ms); | ||||||
|  |     if(TimecodeToDottedSeconds(Timecode) >= TimecodeToDottedSeconds(*PreviousTimecode)) | ||||||
|     { |     { | ||||||
|                         break; |         *PreviousTimecode = Timecode; | ||||||
|                     } | 
 | ||||||
|  |         memory_book LocalTopics = InitBook(sizeof(category_info), 8); | ||||||
|  |         memory_book LocalMedia = InitBook(sizeof(category_info), 8); | ||||||
|  | 
 | ||||||
|  |         bool HasQuote = FALSE; | ||||||
|  |         bool HasReference = FALSE; | ||||||
|  | 
 | ||||||
|  |         RewindBuffer(&IndexBuffers->Master); | ||||||
|  |         RewindBuffer(&IndexBuffers->Header); | ||||||
|  |         RewindBuffer(&IndexBuffers->Class); | ||||||
|  |         RewindBuffer(&IndexBuffers->Data); | ||||||
|  |         RewindBuffer(&IndexBuffers->Text); | ||||||
|  |         RewindBuffer(&IndexBuffers->CategoryIcons); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         ProcessTimestampTiming(&CollationBuffers->SearchEntry, IndexBuffers, Timecode); | ||||||
|  | 
 | ||||||
|  |         CopyStringToBuffer(&IndexBuffers->Class, | ||||||
|  |                 " class=\"marker"); | ||||||
|  | 
 | ||||||
|  |         speaker *Speaker = ProcessTimestampAuthoring(&CollationBuffers->SearchEntry, IndexBuffers, | ||||||
|  |                 Speakers, Author, HasFilterMenu, | ||||||
|  |                 Topics, &LocalTopics, | ||||||
|  |                 Media, &LocalMedia, | ||||||
|  |                 Timestamp); | ||||||
|  | 
 | ||||||
|  |         int MarkerIndex = 0; | ||||||
|  |         Result = ProcessTimestampText(N, Filepath, Strings, | ||||||
|  |                 MenuBuffers, IndexBuffers, | ||||||
|  |                 ReferencesArray, | ||||||
|  |                 HasReferenceMenu, HasFilterMenu, | ||||||
|  |                 RefIdentifier, | ||||||
|  |                 Topics, &LocalTopics, | ||||||
|  |                 Media, &LocalMedia, | ||||||
|  |                 Timestamp, | ||||||
|  |                 &HasReference, Timecode, &MarkerIndex); | ||||||
|  | 
 | ||||||
|  |         if(Result == RC_SUCCESS) | ||||||
|  |         { | ||||||
|  |             Result = ProcessTimestampQuoting(&CollationBuffers->SearchEntry, Filepath, Strings, | ||||||
|  |                     MenuBuffers, IndexBuffers, | ||||||
|  |                     Speakers, Speaker, Author, | ||||||
|  |                     HasQuoteMenu, QuoteIdentifier, | ||||||
|  |                     &HasQuote, HasReference, | ||||||
|  |                     Timestamp, Timecode); | ||||||
|  | 
 | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if(Result == RC_SUCCESS) |         if(Result == RC_SUCCESS) | ||||||
|         { |         { | ||||||
|                     if(LocalTopics.ItemCount == 0) |             Result = ProcessTimestampCategories(N, | ||||||
|                     { |                 &CollationBuffers->SearchEntry, IndexBuffers, | ||||||
|                         hsl_colour TopicColour = {}; |                 DefaultMedium, | ||||||
|                         Result = GenerateTopicColours(N, Wrap0("nullTopic"), &TopicColour); |                 HasFilterMenu, | ||||||
|                         if(Result == RC_SUCCESS) |                 RequiresCineraJS, | ||||||
|                         { |                 Topics, &LocalTopics, | ||||||
|                             InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0("nullTopic"), &TopicColour); |                 Media, &LocalMedia, | ||||||
|                         } |                 Timestamp, Timecode, | ||||||
|  |                 &MarkerIndex, HasQuote, HasReference); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if(Result == RC_SUCCESS) |         if(Result == RC_SUCCESS) | ||||||
|         { |         { | ||||||
|                         if(LocalMedia.ItemCount == 0) |             ProcessTimestampCoda(&CollationBuffers->SearchEntry, IndexBuffers); | ||||||
|                         { |  | ||||||
|                             InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, DefaultMedium->ID, NULL); |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         BuildTimestampClass(&IndexBuffers->Class, &LocalTopics, &LocalMedia, DefaultMedium->ID); |  | ||||||
|                         CopyLandmarkedBuffer(&IndexBuffers->Header, &IndexBuffers->Class, 0, PAGE_PLAYER); |  | ||||||
| 
 |  | ||||||
|                         if(HasQuote || HasReference) |  | ||||||
|                         { |  | ||||||
|                             CopyStringToBuffer(&IndexBuffers->Data, "\""); |  | ||||||
|                             CopyLandmarkedBuffer(&IndexBuffers->Header, &IndexBuffers->Data, 0, PAGE_PLAYER); |  | ||||||
|                         } |  | ||||||
|                         CopyStringToBuffer(&IndexBuffers->Header, ">\n"); |  | ||||||
| 
 |  | ||||||
|                         CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Header, 0, PAGE_PLAYER); |  | ||||||
|                         CopyStringToBuffer(&IndexBuffers->Master, |  | ||||||
|                                 "                    <div class=\"cineraContent\"><span class=\"timecode\">"); |  | ||||||
|                         CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode); |  | ||||||
|                         CopyStringToBuffer(&IndexBuffers->Master, "</span>"); |  | ||||||
| 
 |  | ||||||
|                         CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Text, 0, PAGE_PLAYER); |  | ||||||
| 
 |  | ||||||
|                         if(LocalTopics.ItemCount > 0) |  | ||||||
|                         { |  | ||||||
|                             BuildCategoryIcons(&IndexBuffers->Master, &LocalTopics, &LocalMedia, DefaultMedium->ID, RequiresCineraJS); |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         CopyStringToBuffer(&IndexBuffers->Master, "</div>\n" |  | ||||||
|                                 "                    <div class=\"progress faded\">\n" |  | ||||||
|                                 "                        <div class=\"cineraContent\"><span class=\"timecode\">"); |  | ||||||
|                         CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode); |  | ||||||
|                         CopyStringToBuffer(&IndexBuffers->Master, "</span>"); |  | ||||||
| 
 |  | ||||||
|                         CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Text, 0, PAGE_PLAYER); |  | ||||||
| 
 |  | ||||||
|                         if(LocalTopics.ItemCount > 0) |  | ||||||
|                         { |  | ||||||
|                             BuildCategoryIcons(&IndexBuffers->Master, &LocalTopics, &LocalMedia, DefaultMedium->ID, RequiresCineraJS); |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         CopyStringToBuffer(&IndexBuffers->Master, "</div>\n" |  | ||||||
|                                 "                    </div>\n" |  | ||||||
|                                 "                    <div class=\"progress main\">\n" |  | ||||||
|                                 "                        <div class=\"cineraContent\"><span class=\"timecode\">"); |  | ||||||
|                         CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode); |  | ||||||
|                         CopyStringToBuffer(&IndexBuffers->Master, "</span>"); |  | ||||||
| 
 |  | ||||||
|                         CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Text, 0, PAGE_PLAYER); |  | ||||||
| 
 |  | ||||||
|                         if(LocalTopics.ItemCount > 0) |  | ||||||
|                         { |  | ||||||
|                             //CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->CategoryIcons, PAGE_PLAYER);
 |  | ||||||
|                             BuildCategoryIcons(&IndexBuffers->Master, &LocalTopics, &LocalMedia, DefaultMedium->ID, RequiresCineraJS); |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         CopyStringToBuffer(&IndexBuffers->Master, "</div>\n" |  | ||||||
|                                 "                    </div>\n" |  | ||||||
|                                 "                </div>\n"); |  | ||||||
| 
 | 
 | ||||||
|             CopyLandmarkedBuffer(&PlayerBuffers->Main, &IndexBuffers->Master, 0, PAGE_PLAYER); |             CopyLandmarkedBuffer(&PlayerBuffers->Main, &IndexBuffers->Master, 0, PAGE_PLAYER); | ||||||
|         } |         } | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         FreeBook(&LocalTopics); |         FreeBook(&LocalTopics); | ||||||
|         FreeBook(&LocalMedia); |         FreeBook(&LocalMedia); | ||||||
|  | @ -18318,7 +18508,7 @@ main(int ArgC, char **Args) | ||||||
|         if(ClaimBuffer(&CollationBuffers.Player, BID_COLLATION_BUFFERS_PLAYER, Kilobytes(552)) == RC_ARENA_FULL) { Exit(); }; |         if(ClaimBuffer(&CollationBuffers.Player, BID_COLLATION_BUFFERS_PLAYER, Kilobytes(552)) == RC_ARENA_FULL) { Exit(); }; | ||||||
| 
 | 
 | ||||||
|         if(ClaimBuffer(&CollationBuffers.IncludesSearch, BID_COLLATION_BUFFERS_INCLUDES_SEARCH, Kilobytes(4)) == RC_ARENA_FULL) { Exit(); }; |         if(ClaimBuffer(&CollationBuffers.IncludesSearch, BID_COLLATION_BUFFERS_INCLUDES_SEARCH, Kilobytes(4)) == RC_ARENA_FULL) { Exit(); }; | ||||||
|         if(ClaimBuffer(&CollationBuffers.SearchEntry, BID_COLLATION_BUFFERS_SEARCH_ENTRY, Kilobytes(32)) == RC_ARENA_FULL) { Exit(); }; |         if(ClaimBuffer(&CollationBuffers.SearchEntry, BID_COLLATION_BUFFERS_SEARCH_ENTRY, Kilobytes(64)) == RC_ARENA_FULL) { Exit(); }; | ||||||
| 
 | 
 | ||||||
|         CollationBuffers.Search.ID = BID_COLLATION_BUFFERS_SEARCH; // NOTE(matt): Allocated by SearchToBuffer()
 |         CollationBuffers.Search.ID = BID_COLLATION_BUFFERS_SEARCH; // NOTE(matt): Allocated by SearchToBuffer()
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue