From e5ffec7a5583a1c563149a43be4a0c7206150c51 Mon Sep 17 00:00:00 2001 From: Matt Mascarenhas Date: Wed, 16 Apr 2025 18:56:26 +0100 Subject: [PATCH] cinera.c: Make @author and :categories searchable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a stop-gap gap solution pending CINERA_DB_VERSION 6. It simply augments the .index files, prepending "@author: " to timestamps bearing an author – e.g. audience questions – and appending " [:each :category]" to those bearing topic or medium categorisation. --- cinera/cinera.c | 1017 ++++++++++++++++++++++++++++------------------- 1 file changed, 606 insertions(+), 411 deletions(-) diff --git a/cinera/cinera.c b/cinera/cinera.c index 475c16f..1dfe024 100644 --- a/cinera/cinera.c +++ b/cinera/cinera.c @@ -23,7 +23,7 @@ typedef struct version CINERA_APP_VERSION = { .Major = 0, .Minor = 10, - .Patch = 30 + .Patch = 31 }; #define __USE_XOPEN2K8 // NOTE(matt): O_NOFOLLOW @@ -8350,16 +8350,70 @@ BuildTimestampClass(buffer *TimestampClass, _memory_book(category_info) *LocalTo CopyStringToBuffer(TimestampClass, "\""); } -void -BuildCategoryIcons(buffer *CategoryIcons, _memory_book(category_info) *LocalTopics, _memory_book(category_info) *LocalMedia, string DefaultMedium, bool *RequiresCineraJS) +bool +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 *FirstLocalMedium = GetPlaceInBook(LocalMedia, 0); 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, ""); } @@ -8368,6 +8422,14 @@ BuildCategoryIcons(buffer *CategoryIcons, _memory_book(category_info) *LocalTopi for(int i = 0; i < LocalTopics->ItemCount; ++i) { category_info *This = GetPlaceInBook(LocalTopics, i); + + // .index + if(SearchEntry) + { + PushCategorySearchEntry(SearchEntry, Timestamp, This, Underway); + } + + // .html // NOTE(matt): Stack-string char SanitisedMarker[This->Marker.Length + 1]; 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, ">"); + + 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) { category_info *This = GetPlaceInBook(LocalMedia, i); + + // .index + if(SearchEntry) + { + PushCategorySearchEntry(SearchEntry, Timestamp, This, Underway); + } + + // .html // NOTE(matt): Stack-string char SanitisedMarker[This->Marker.Length + 1]; 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, ""); } + + Underway = TRUE; } } - if(CategoriesSpan) + if(Underway) { + // .index + if(SearchEntry) + { + CopyStringToBuffer(SearchEntry, "]"); + } + + // .html CopyStringToBuffer(CategoryIcons, ""); } } @@ -9220,21 +9301,6 @@ StripSurroundingSlashes(char *String) // NOTE(matt): For relative paths return Ptr; } -bool -IsWhitespace(char C) -{ - return (C == ' ' || C == '\t' || C == '\n'); -} - -void -ConsumeWhitespace(buffer *B) -{ - while(B->Ptr - B->Location < B->Size && IsWhitespace(*B->Ptr)) - { - ++B->Ptr; - } -} - string StripPWDIndicators(string Path) { @@ -10862,6 +10928,483 @@ TimecodeIs(v4 Timecode, int Hours, int Minutes, int Seconds, int Milliseconds) return Timecode.Hours == Hours && Timecode.Minutes == Minutes && Timecode.Seconds == Seconds && Timecode.Milliseconds == Milliseconds; } +void +ProcessTimestampCoda(buffer *SearchEntry, index_buffers *IndexBuffers) +{ + // .index + CopyStringToBuffer(SearchEntry, "\"\n"); + + // .html + CopyStringToBuffer(&IndexBuffers->Master, "\n" + " \n" + " \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) +{ + rc Result = RC_SUCCESS; + + while(*MarkerIndex < Timestamp->marker_count) + { + hsl_colour TopicColour = {}; + Result = GenerateTopicColours(N, Wrap0(Timestamp->markers[*MarkerIndex].marker), &TopicColour); + if(Result == RC_SUCCESS) + { + if(!*HasFilterMenu) + { + *HasFilterMenu = TRUE; + } + InsertCategory(Topics, LocalTopics, Media, LocalMedia, Wrap0(Timestamp->markers[*MarkerIndex].marker), &TopicColour); + ++*MarkerIndex; + } + else + { + break; + } + } + + if(Result == RC_SUCCESS) + { + if(LocalTopics->ItemCount == 0) + { + hsl_colour TopicColour = {}; + Result = GenerateTopicColours(N, Wrap0("nullTopic"), &TopicColour); + if(Result == RC_SUCCESS) + { + InsertCategory(Topics, LocalTopics, Media, LocalMedia, Wrap0("nullTopic"), &TopicColour); + } + } + + if(Result == RC_SUCCESS) + { + if(LocalMedia->ItemCount == 0) + { + InsertCategory(Topics, LocalTopics, Media, LocalMedia, DefaultMedium->ID, NULL); + } + + BuildTimestampClass(&IndexBuffers->Class, LocalTopics, LocalMedia, DefaultMedium->ID); + CopyLandmarkedBuffer(&IndexBuffers->Header, &IndexBuffers->Class, 0, PAGE_PLAYER); + + if(HasQuote || HasReference) + { + CopyStringToBuffer(&IndexBuffers->Data, "\""); + CopyLandmarkedBuffer(&IndexBuffers->Header, &IndexBuffers->Data, 0, PAGE_PLAYER); + } + CopyStringToBuffer(&IndexBuffers->Header, ">\n"); + + CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Header, 0, PAGE_PLAYER); + CopyStringToBuffer(&IndexBuffers->Master, + "
"); + CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode); + CopyStringToBuffer(&IndexBuffers->Master, ""); + + 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, "
\n" + "
\n" + "
"); + CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode); + CopyStringToBuffer(&IndexBuffers->Master, ""); + + 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, "
\n" + "
\n" + "
\n" + "
"); + CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode); + CopyStringToBuffer(&IndexBuffers->Master, ""); + + 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, + "
\n" + " Quotes ▼\n" + "
\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, + " \n" + " \n" + " \n" + "
Quote %d
\n" + "
", + *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, "
\n" + " \n" + "
\n" + "
\n" + " [&#%d;]", + (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, "\n" + "
\n" + "
\n" + "
\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, "&#%d;", *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); + } + else + { + break; + } + } + else // NOTE(matt): HMML_MEMBER / HMML_PROJECT + { + // TODO(matt): That EDITION_NETWORK site database API-polling stuff + hsl_colour Colour; + StringToColourHash(&Colour, Wrap0(Timestamp->markers[*MarkerIndex].marker)); + CopyStringToBuffer(&IndexBuffers->Text, + "%.*s", + Timestamp->markers[*MarkerIndex].type == HMML_MEMBER ? "member" : "project", + Colour.Hue, Colour.Saturation, + (int)StringLength(Readable), InPtr); + } + + InPtr += StringLength(Readable); + ++*MarkerIndex; + } + + if(Result == RC_SUCCESS) + { + while(RefIndex < Timestamp->reference_count && + InPtr - Timestamp->text == Timestamp->references[RefIndex].offset) + { + HMML_Reference *CurrentRef = Timestamp->references + RefIndex; + if(!*HasReferenceMenu) + { + CopyStringToBuffer(&MenuBuffers->Reference, + "
\n" + " References ▼\n" + "
\n"); + + *HasReferenceMenu = TRUE; + } + + ref_info *This = GetOrBuildReference(Strings, ReferencesArray, CurrentRef, &Result); + if(This) + { + identifier *New = MakeSpaceInBook(&This->Identifier); + New->Timecode = Timecode; + New->Identifier = *RefIdentifier; + } + else + { + switch(Result) + { + case RC_INVALID_REFERENCE: + { + IndexingError(Filepath, Timestamp->line, S_ERROR, + "Invalid [ref]. Please set either a \"url\" or \"isbn\"", + 0); + } break; + case RC_UNHANDLED_REF_COMBO: + { + IndexingError(Filepath, Timestamp->line, S_ERROR, + "Cannot process new combination of reference info\n" + "\n" + "Either tweak your timestamp, or write to contact@miblo.net\n" + "mentioning the ref node you want to write and how you want it to\n" + "appear in the references menu", + 0); + } break; + default: break; + } + break; // NOTE(matt): Out of the while() + } + + CopyStringToBuffer(&IndexBuffers->Data, "%s%s", !*HasReference ? " data-ref=\"" : "," , This->ID); + *HasReference = TRUE; + CopyStringToBuffer(&IndexBuffers->Text, "%s%d", + RefIndex > 0 && Timestamp->references[RefIndex].offset == Timestamp->references[RefIndex-1].offset ? "," : "", + *RefIdentifier); + + ++RefIndex; + ++*RefIdentifier; + } + + if(Result != RC_SUCCESS) + { + break; + } + + if(*InPtr) + { + switch(*InPtr) + { + case '<': + CopyStringToBuffer(&IndexBuffers->Text, "<"); + break; + case '>': + CopyStringToBuffer(&IndexBuffers->Text, ">"); + break; + case '&': + CopyStringToBuffer(&IndexBuffers->Text, "&"); + break; + case '\"': + CopyStringToBuffer(&IndexBuffers->Text, """); + break; + case '\'': + CopyStringToBuffer(&IndexBuffers->Text, "'"); + break; + default: + *IndexBuffers->Text.Ptr++ = *InPtr; + *IndexBuffers->Text.Ptr = '\0'; + break; + } + ++InPtr; + } + } + } + + return Result; +} + +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, + "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: ", (int)DisplayName.Length, DisplayName.Base); + + Speaker->Seen = TRUE; + } + else if(Author.Length > 0) + { + // .index + CopyStringToBuffer(SearchEntry, "@"); + CopyStringToBufferNoFormat(SearchEntry, Author); + CopyStringToBuffer(SearchEntry, ": "); + + // .html + 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, + "@%.*s ", + AuthorColour.Hue, AuthorColour.Saturation, + (int)Author.Length, Author.Base); + } + } + + return Speaker; +} + +void +ProcessTimestampTiming(buffer *SearchEntry, index_buffers *IndexBuffers, v4 Timecode) +{ + float Time = TimecodeToDottedSeconds(Timecode); + + // .index + CopyStringToBuffer(SearchEntry, "\"%.3f\": \"", Time); + + // .html + CopyStringToBuffer(&IndexBuffers->Header, + "
CategoryIcons); - CopyStringToBuffer(&IndexBuffers->Header, - "
SearchEntry, IndexBuffers, Timecode); CopyStringToBuffer(&IndexBuffers->Class, " class=\"marker"); - speaker *Speaker = GetSpeaker(&Speakers->Speakers, Author); - if(!IsCategorisedAFK(Timestamp)) + 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) { - // 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]; + Result = ProcessTimestampQuoting(&CollationBuffers->SearchEntry, Filepath, Strings, + MenuBuffers, IndexBuffers, + Speakers, Speaker, Author, + HasQuoteMenu, QuoteIdentifier, + &HasQuote, HasReference, + Timestamp, Timecode); - CopyStringToBuffer(&IndexBuffers->Text, - "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: ", (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, - "%.*s ", - AuthorColour.Hue, AuthorColour.Saturation, - (int)Author.Length, Author.Base); - } - } - - char *InPtr = Timestamp->text; - - int MarkerIndex = 0, RefIndex = 0; - while(*InPtr || RefIndex < Timestamp->reference_count) - { - if(MarkerIndex < Timestamp->marker_count && - InPtr - Timestamp->text == Timestamp->markers[MarkerIndex].offset) - { - char *Readable = Timestamp->markers[MarkerIndex].parameter - ? Timestamp->markers[MarkerIndex].parameter - : Timestamp->markers[MarkerIndex].marker; - HMML_MarkerType Type = Timestamp->markers[MarkerIndex].type; - if(Type == HMML_CATEGORY) - { - hsl_colour TopicColour = {}; - Result = GenerateTopicColours(N, Wrap0(Timestamp->markers[MarkerIndex].marker), &TopicColour); - if(Result == RC_SUCCESS) - { - if(!*HasFilterMenu) - { - *HasFilterMenu = TRUE; - } - InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0(Timestamp->markers[MarkerIndex].marker), &TopicColour); - CopyStringToBuffer(&IndexBuffers->Text, "%.*s", (int)StringLength(Readable), InPtr); - } - else - { - break; - } - } - else // NOTE(matt): HMML_MEMBER / HMML_PROJECT - { - // TODO(matt): That EDITION_NETWORK site database API-polling stuff - hsl_colour Colour; - StringToColourHash(&Colour, Wrap0(Timestamp->markers[MarkerIndex].marker)); - CopyStringToBuffer(&IndexBuffers->Text, - "%.*s", - Timestamp->markers[MarkerIndex].type == HMML_MEMBER ? "member" : "project", - Colour.Hue, Colour.Saturation, - (int)StringLength(Readable), InPtr); - } - - InPtr += StringLength(Readable); - ++MarkerIndex; - } - - if(Result == RC_SUCCESS) - { - while(RefIndex < Timestamp->reference_count && - InPtr - Timestamp->text == Timestamp->references[RefIndex].offset) - { - HMML_Reference *CurrentRef = Timestamp->references + RefIndex; - if(!*HasReferenceMenu) - { - CopyStringToBuffer(&MenuBuffers->Reference, - "
\n" - " References ▼\n" - "
\n"); - - *HasReferenceMenu = TRUE; - } - - ref_info *This = GetOrBuildReference(Strings, ReferencesArray, CurrentRef, &Result); - if(This) - { - identifier *New = MakeSpaceInBook(&This->Identifier); - New->Timecode = Timecode; - New->Identifier = *RefIdentifier; - } - else - { - switch(Result) - { - case RC_INVALID_REFERENCE: - { - IndexingError(Filepath, Timestamp->line, S_ERROR, - "Invalid [ref]. Please set either a \"url\" or \"isbn\"", - 0); - } break; - case RC_UNHANDLED_REF_COMBO: - { - IndexingError(Filepath, Timestamp->line, S_ERROR, - "Cannot process new combination of reference info\n" - "\n" - "Either tweak your timestamp, or contact miblodelcarpio@gmail.com\n" - "mentioning the ref node you want to write and how you want it to\n" - "appear in the references menu", - 0); - } break; - default: break; - } - break; // NOTE(matt): Out of the while() - } - - CopyStringToBuffer(&IndexBuffers->Data, "%s%s", !HasReference ? " data-ref=\"" : "," , This->ID); - HasReference = TRUE; - CopyStringToBuffer(&IndexBuffers->Text, "%s%d", - RefIndex > 0 && Timestamp->references[RefIndex].offset == Timestamp->references[RefIndex-1].offset ? "," : "", - *RefIdentifier); - - ++RefIndex; - ++*RefIdentifier; - } - - if(Result != RC_SUCCESS) - { - break; - } - - if(*InPtr) - { - switch(*InPtr) - { - case '<': - CopyStringToBuffer(&IndexBuffers->Text, "<"); - break; - case '>': - CopyStringToBuffer(&IndexBuffers->Text, ">"); - break; - case '&': - CopyStringToBuffer(&IndexBuffers->Text, "&"); - break; - case '\"': - CopyStringToBuffer(&IndexBuffers->Text, """); - break; - case '\'': - CopyStringToBuffer(&IndexBuffers->Text, "'"); - break; - default: - *IndexBuffers->Text.Ptr++ = *InPtr; - *IndexBuffers->Text.Ptr = '\0'; - break; - } - ++InPtr; - } - } } if(Result == RC_SUCCESS) { - if(Timestamp->quote.present) - { - if(!*HasQuoteMenu) - { - CopyStringToBuffer(&MenuBuffers->Quote, - "
\n" - " Quotes ▼\n" - "
\n"); + Result = ProcessTimestampCategories(N, + &CollationBuffers->SearchEntry, IndexBuffers, + DefaultMedium, + HasFilterMenu, + RequiresCineraJS, + Topics, &LocalTopics, + Media, &LocalMedia, + Timestamp, Timecode, + &MarkerIndex, HasQuote, HasReference); + } - *HasQuoteMenu = TRUE; - } + if(Result == RC_SUCCESS) + { + ProcessTimestampCoda(&CollationBuffers->SearchEntry, IndexBuffers); - if(!HasReference) - { - CopyStringToBuffer(&IndexBuffers->Data, " data-ref=\"&#%d;", *QuoteIdentifier); - } - else - { - CopyStringToBuffer(&IndexBuffers->Data, ",&#%d;", *QuoteIdentifier); - } - - HasQuote = TRUE; - - bool ShouldFetchQuotes = FALSE; - if(Config->CacheDir.Length == 0 || time(0) - LastQuoteFetch > 60*60) - { - ShouldFetchQuotes = TRUE; - } - - if(!Speaker && Speakers->Speakers.ItemCount > 0) - { - Speaker = GetPlaceInBook(&Speakers->Speakers, 0); - } - string QuoteUsername; - if(Timestamp->quote.author) - { - QuoteUsername = Wrap0(Timestamp->quote.author); - } - else if(Speaker) - { - QuoteUsername = Speaker->Person->QuoteUsername; - } - else - { - QuoteUsername = Author; - } - - /* */ MEM_TEST_MID(); - /* +MEM */ Result = BuildQuote(Strings, &QuoteInfo, - QuoteUsername, Timestamp->quote.id, ShouldFetchQuotes); - /* */ MEM_TEST_MID(); - if(Result == RC_SUCCESS) - { - CopyStringToBuffer(&MenuBuffers->Quote, - " \n" - " \n" - " \n" - "
Quote %d
\n" - "
", - *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, "
\n" - " \n" - "
\n" - "
\n" - " [&#%d;]", - (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, "\n" - "
\n" - "
\n" - "
\n"); - if(!Timestamp->text[0]) - { - CopyStringToBuffer(&IndexBuffers->Text, "“"); - CopyStringToBufferHTMLSafe(&IndexBuffers->Text, QuoteInfo.Text); - CopyStringToBuffer(&IndexBuffers->Text, "”"); - } - CopyStringToBuffer(&IndexBuffers->Text, "&#%d;", *QuoteIdentifier); - ++*QuoteIdentifier; - } - else if(Result == RC_UNFOUND) - { - IndexingQuoteError(&Filepath, Timestamp->line, QuoteUsername, Timestamp->quote.id); - } - } - - if(Result == RC_SUCCESS) - { - CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"%.3f\": \"", TimecodeToDottedSeconds(Timecode)); - if(Timestamp->quote.present && !Timestamp->text[0]) - { - CopyStringToBuffer(&CollationBuffers->SearchEntry, "\u201C"); - CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, QuoteInfo.Text); - CopyStringToBuffer(&CollationBuffers->SearchEntry, "\u201D"); - } - else - { - CopyStringToBufferNoFormat(&CollationBuffers->SearchEntry, Wrap0(Timestamp->text)); - } - CopyStringToBuffer(&CollationBuffers->SearchEntry, "\"\n"); - - while(MarkerIndex < Timestamp->marker_count) - { - hsl_colour TopicColour = {}; - Result = GenerateTopicColours(N, Wrap0(Timestamp->markers[MarkerIndex].marker), &TopicColour); - if(Result == RC_SUCCESS) - { - if(!*HasFilterMenu) - { - *HasFilterMenu = TRUE; - } - InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0(Timestamp->markers[MarkerIndex].marker), &TopicColour); - ++MarkerIndex; - } - else - { - break; - } - } - - if(Result == RC_SUCCESS) - { - if(LocalTopics.ItemCount == 0) - { - hsl_colour TopicColour = {}; - Result = GenerateTopicColours(N, Wrap0("nullTopic"), &TopicColour); - if(Result == RC_SUCCESS) - { - InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0("nullTopic"), &TopicColour); - } - } - - if(Result == RC_SUCCESS) - { - if(LocalMedia.ItemCount == 0) - { - InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, DefaultMedium->ID, NULL); - } - - BuildTimestampClass(&IndexBuffers->Class, &LocalTopics, &LocalMedia, DefaultMedium->ID); - CopyLandmarkedBuffer(&IndexBuffers->Header, &IndexBuffers->Class, 0, PAGE_PLAYER); - - if(HasQuote || HasReference) - { - CopyStringToBuffer(&IndexBuffers->Data, "\""); - CopyLandmarkedBuffer(&IndexBuffers->Header, &IndexBuffers->Data, 0, PAGE_PLAYER); - } - CopyStringToBuffer(&IndexBuffers->Header, ">\n"); - - CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Header, 0, PAGE_PLAYER); - CopyStringToBuffer(&IndexBuffers->Master, - "
"); - CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode); - CopyStringToBuffer(&IndexBuffers->Master, ""); - - CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Text, 0, PAGE_PLAYER); - - if(LocalTopics.ItemCount > 0) - { - BuildCategoryIcons(&IndexBuffers->Master, &LocalTopics, &LocalMedia, DefaultMedium->ID, RequiresCineraJS); - } - - CopyStringToBuffer(&IndexBuffers->Master, "
\n" - "
\n" - "
"); - CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode); - CopyStringToBuffer(&IndexBuffers->Master, ""); - - CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Text, 0, PAGE_PLAYER); - - if(LocalTopics.ItemCount > 0) - { - BuildCategoryIcons(&IndexBuffers->Master, &LocalTopics, &LocalMedia, DefaultMedium->ID, RequiresCineraJS); - } - - CopyStringToBuffer(&IndexBuffers->Master, "
\n" - "
\n" - "
\n" - "
"); - CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode); - CopyStringToBuffer(&IndexBuffers->Master, ""); - - 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, "
\n" - "
\n" - "
\n"); - - CopyLandmarkedBuffer(&PlayerBuffers->Main, &IndexBuffers->Master, 0, PAGE_PLAYER); - } - } - } + CopyLandmarkedBuffer(&PlayerBuffers->Main, &IndexBuffers->Master, 0, PAGE_PLAYER); } FreeBook(&LocalTopics); @@ -18313,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.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()