cinera.c: Make @author and :categories searchable

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.
This commit is contained in:
Matt Mascarenhas 2025-04-16 18:56:26 +01:00
parent a67a5ee34b
commit e5ffec7a55
1 changed files with 606 additions and 411 deletions

View File

@ -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, "<span class=\"cineraCategories\">");
}
@ -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,
"></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)
{
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, "</div>");
}
Underway = TRUE;
}
}
if(CategoriesSpan)
if(Underway)
{
// .index
if(SearchEntry)
{
CopyStringToBuffer(SearchEntry, "]");
}
// .html
CopyStringToBuffer(CategoryIcons, "</span>");
}
}
@ -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,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;
}
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)
void
ProcessTimestampCoda(buffer *SearchEntry, index_buffers *IndexBuffers)
{
// .index
CopyStringToBuffer(SearchEntry, "\"\n");
// .html
CopyStringToBuffer(&IndexBuffers->Master, "</div>\n"
" </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;
v4 Timecode = V4(Timestamp->h, Timestamp->m, Timestamp->s, Timestamp->ms);
if(TimecodeToDottedSeconds(Timecode) >= TimecodeToDottedSeconds(*PreviousTimecode))
{
*PreviousTimecode = Timecode;
memory_book LocalTopics = InitBook(sizeof(category_info), 8);
memory_book LocalMedia = InitBook(sizeof(category_info), 8);
quote_info QuoteInfo = { };
bool HasQuote = FALSE;
bool HasReference = FALSE;
RewindBuffer(&IndexBuffers->Master);
RewindBuffer(&IndexBuffers->Header);
RewindBuffer(&IndexBuffers->Class);
RewindBuffer(&IndexBuffers->Data);
RewindBuffer(&IndexBuffers->Text);
RewindBuffer(&IndexBuffers->CategoryIcons);
CopyStringToBuffer(&IndexBuffers->Header,
" <div data-timestamp=\"%.3f\"",
TimecodeToDottedSeconds(Timecode));
CopyStringToBuffer(&IndexBuffers->Class,
" class=\"marker");
speaker *Speaker = GetSpeaker(&Speakers->Speakers, Author);
if(!IsCategorisedAFK(Timestamp))
{
// NOTE(matt): I reckon it's fair to only cite the speaker when there are a multiple of them
if(Speakers->Speakers.ItemCount > 1 && Speaker && !IsCategorisedAuthored(Timestamp))
{
string DisplayName = !Speaker->Seen ? Speaker->Person->Name : Speaker->Person->Abbreviations[Speakers->AbbrevScheme];
CopyStringToBuffer(&IndexBuffers->Text,
"<span class=\"author\" data-hue=\"%d\" data-saturation=\"%d%%\"",
Speaker->Colour.Hue,
Speaker->Colour.Saturation);
if(Speaker->Seen)
{
CopyStringToBuffer(&IndexBuffers->Text, " title=\"");
CopyStringToBufferHTMLSafe(&IndexBuffers->Text, Speaker->Person->Name);
CopyStringToBuffer(&IndexBuffers->Text, "\"");
}
CopyStringToBuffer(&IndexBuffers->Text,
">%.*s</span>: ", (int)DisplayName.Length, DisplayName.Base);
Speaker->Seen = TRUE;
}
else if(Author.Length > 0)
{
if(!*HasFilterMenu)
{
*HasFilterMenu = TRUE;
}
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0("authored"), NULL);
hsl_colour AuthorColour;
StringToColourHash(&AuthorColour, Author);
// TODO(matt): That EDITION_NETWORK site database API-polling stuff
CopyStringToBuffer(&IndexBuffers->Text,
"<span class=\"author\" data-hue=\"%d\" data-saturation=\"%d%%\">%.*s</span> ",
AuthorColour.Hue, AuthorColour.Saturation,
(int)Author.Length, Author.Base);
}
}
char *InPtr = Timestamp->text;
int MarkerIndex = 0, RefIndex = 0;
while(*InPtr || RefIndex < Timestamp->reference_count)
{
if(MarkerIndex < Timestamp->marker_count &&
InPtr - Timestamp->text == Timestamp->markers[MarkerIndex].offset)
{
char *Readable = Timestamp->markers[MarkerIndex].parameter
? Timestamp->markers[MarkerIndex].parameter
: Timestamp->markers[MarkerIndex].marker;
HMML_MarkerType Type = Timestamp->markers[MarkerIndex].type;
if(Type == HMML_CATEGORY)
while(*MarkerIndex < Timestamp->marker_count)
{
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(!*HasFilterMenu)
{
*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 &#9660;</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\">&mdash;%.*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, "&#8220;");
CopyStringToBufferHTMLSafe(&IndexBuffers->Text, QuoteInfo.Text);
CopyStringToBuffer(&IndexBuffers->Text, "&#8221;");
}
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);
}
else
@ -10980,16 +11221,16 @@ ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, m
{
// TODO(matt): That EDITION_NETWORK site database API-polling stuff
hsl_colour Colour;
StringToColourHash(&Colour, Wrap0(Timestamp->markers[MarkerIndex].marker));
StringToColourHash(&Colour, Wrap0(Timestamp->markers[*MarkerIndex].marker));
CopyStringToBuffer(&IndexBuffers->Text,
"<span class=\"%s\" data-hue=\"%d\" data-saturation=\"%d%%\">%.*s</span>",
Timestamp->markers[MarkerIndex].type == HMML_MEMBER ? "member" : "project",
Timestamp->markers[*MarkerIndex].type == HMML_MEMBER ? "member" : "project",
Colour.Hue, Colour.Saturation,
(int)StringLength(Readable), InPtr);
}
InPtr += StringLength(Readable);
++MarkerIndex;
++*MarkerIndex;
}
if(Result == RC_SUCCESS)
@ -11030,7 +11271,7 @@ ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, m
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"
"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);
@ -11040,8 +11281,8 @@ ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, m
break; // NOTE(matt): Out of the while()
}
CopyStringToBuffer(&IndexBuffers->Data, "%s%s", !HasReference ? " data-ref=\"" : "," , This->ID);
HasReference = TRUE;
CopyStringToBuffer(&IndexBuffers->Data, "%s%s", !*HasReference ? " data-ref=\"" : "," , This->ID);
*HasReference = TRUE;
CopyStringToBuffer(&IndexBuffers->Text, "<sup>%s%d</sup>",
RefIndex > 0 && Timestamp->references[RefIndex].offset == Timestamp->references[RefIndex-1].offset ? "," : "",
*RefIdentifier);
@ -11084,217 +11325,171 @@ ProcessTimestamp(buffers *CollationBuffers, neighbourhood *N, string Filepath, m
}
}
if(Result == RC_SUCCESS)
{
if(Timestamp->quote.present)
{
if(!*HasQuoteMenu)
{
CopyStringToBuffer(&MenuBuffers->Quote,
" <div class=\"menu quotes\">\n"
" <span>Quotes &#9660;</span>\n"
" <div class=\"refs quotes_container\">\n");
return Result;
}
*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->Data, " data-ref=\"&#%d;", *QuoteIdentifier);
}
else
{
CopyStringToBuffer(&IndexBuffers->Data, ",&#%d;", *QuoteIdentifier);
}
CopyStringToBuffer(&IndexBuffers->Text,
">%.*s</span>: ", (int)DisplayName.Length, DisplayName.Base);
HasQuote = TRUE;
Speaker->Seen = TRUE;
}
else if(Author.Length > 0)
{
// .index
CopyStringToBuffer(SearchEntry, "@");
CopyStringToBufferNoFormat(SearchEntry, Author);
CopyStringToBuffer(SearchEntry, ": ");
bool ShouldFetchQuotes = FALSE;
if(Config->CacheDir.Length == 0 || time(0) - LastQuoteFetch > 60*60)
{
ShouldFetchQuotes = TRUE;
}
if(!Speaker && Speakers->Speakers.ItemCount > 0)
{
Speaker = GetPlaceInBook(&Speakers->Speakers, 0);
}
string QuoteUsername;
if(Timestamp->quote.author)
{
QuoteUsername = Wrap0(Timestamp->quote.author);
}
else if(Speaker)
{
QuoteUsername = Speaker->Person->QuoteUsername;
}
else
{
QuoteUsername = Author;
}
/* */ MEM_TEST_MID();
/* +MEM */ Result = BuildQuote(Strings, &QuoteInfo,
QuoteUsername, Timestamp->quote.id, ShouldFetchQuotes);
/* */ MEM_TEST_MID();
if(Result == RC_SUCCESS)
{
CopyStringToBuffer(&MenuBuffers->Quote,
" <a target=\"_blank\" data-id=\"&#%d;\" class=\"ref\" href=\"https://dev.abaines.me.uk/quotes/%.*s/%d\">\n"
" <span>\n"
" <span class=\"ref_content\">\n"
" <div class=\"source\">Quote %d</div>\n"
" <div class=\"ref_title\">",
*QuoteIdentifier,
(int)QuoteUsername.Length, QuoteUsername.Base,
Timestamp->quote.id,
Timestamp->quote.id);
CopyStringToBufferHTMLSafe(&MenuBuffers->Quote, QuoteInfo.Text);
string DateString = UnixTimeToDateString(Strings, QuoteInfo.Date);
CopyStringToBuffer(&MenuBuffers->Quote, "</div>\n"
" <div class=\"quote_byline\">&mdash;%.*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, "&#8220;");
CopyStringToBufferHTMLSafe(&IndexBuffers->Text, QuoteInfo.Text);
CopyStringToBuffer(&IndexBuffers->Text, "&#8221;");
}
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)
{
// .html
if(!*HasFilterMenu)
{
*HasFilterMenu = TRUE;
}
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0(Timestamp->markers[MarkerIndex].marker), &TopicColour);
++MarkerIndex;
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);
}
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(LocalTopics.ItemCount == 0)
{
hsl_colour TopicColour = {};
Result = GenerateTopicColours(N, Wrap0("nullTopic"), &TopicColour);
if(Result == RC_SUCCESS)
{
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, Wrap0("nullTopic"), &TopicColour);
}
Result = ProcessTimestampCategories(N,
&CollationBuffers->SearchEntry, IndexBuffers,
DefaultMedium,
HasFilterMenu,
RequiresCineraJS,
Topics, &LocalTopics,
Media, &LocalMedia,
Timestamp, Timecode,
&MarkerIndex, HasQuote, HasReference);
}
if(Result == RC_SUCCESS)
{
if(LocalMedia.ItemCount == 0)
{
InsertCategory(Topics, &LocalTopics, Media, &LocalMedia, DefaultMedium->ID, NULL);
}
BuildTimestampClass(&IndexBuffers->Class, &LocalTopics, &LocalMedia, DefaultMedium->ID);
CopyLandmarkedBuffer(&IndexBuffers->Header, &IndexBuffers->Class, 0, PAGE_PLAYER);
if(HasQuote || HasReference)
{
CopyStringToBuffer(&IndexBuffers->Data, "\"");
CopyLandmarkedBuffer(&IndexBuffers->Header, &IndexBuffers->Data, 0, PAGE_PLAYER);
}
CopyStringToBuffer(&IndexBuffers->Header, ">\n");
CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Header, 0, PAGE_PLAYER);
CopyStringToBuffer(&IndexBuffers->Master,
" <div class=\"cineraContent\"><span class=\"timecode\">");
CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode);
CopyStringToBuffer(&IndexBuffers->Master, "</span>");
CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Text, 0, PAGE_PLAYER);
if(LocalTopics.ItemCount > 0)
{
BuildCategoryIcons(&IndexBuffers->Master, &LocalTopics, &LocalMedia, DefaultMedium->ID, RequiresCineraJS);
}
CopyStringToBuffer(&IndexBuffers->Master, "</div>\n"
" <div class=\"progress faded\">\n"
" <div class=\"cineraContent\"><span class=\"timecode\">");
CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode);
CopyStringToBuffer(&IndexBuffers->Master, "</span>");
CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Text, 0, PAGE_PLAYER);
if(LocalTopics.ItemCount > 0)
{
BuildCategoryIcons(&IndexBuffers->Master, &LocalTopics, &LocalMedia, DefaultMedium->ID, RequiresCineraJS);
}
CopyStringToBuffer(&IndexBuffers->Master, "</div>\n"
" </div>\n"
" <div class=\"progress main\">\n"
" <div class=\"cineraContent\"><span class=\"timecode\">");
CopyTimecodeToBuffer(&IndexBuffers->Master, Timecode);
CopyStringToBuffer(&IndexBuffers->Master, "</span>");
CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->Text, 0, PAGE_PLAYER);
if(LocalTopics.ItemCount > 0)
{
//CopyLandmarkedBuffer(&IndexBuffers->Master, &IndexBuffers->CategoryIcons, PAGE_PLAYER);
BuildCategoryIcons(&IndexBuffers->Master, &LocalTopics, &LocalMedia, DefaultMedium->ID, RequiresCineraJS);
}
CopyStringToBuffer(&IndexBuffers->Master, "</div>\n"
" </div>\n"
" </div>\n");
ProcessTimestampCoda(&CollationBuffers->SearchEntry, IndexBuffers);
CopyLandmarkedBuffer(&PlayerBuffers->Main, &IndexBuffers->Master, 0, PAGE_PLAYER);
}
}
}
}
FreeBook(&LocalTopics);
FreeBook(&LocalMedia);
@ -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()