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 = { 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>");
} }
} }
@ -9220,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)
{ {
@ -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; 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 &#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); CopyStringToBuffer(&IndexBuffers->Text, "%.*s", (int)StringLength(Readable), InPtr);
} }
else else
@ -10980,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)
@ -11030,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);
@ -11040,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);
@ -11084,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 &#9660;</span>\n"
" <div class=\"refs quotes_container\">\n");
*HasQuoteMenu = TRUE;
} }
if(!HasReference) 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)
{ {
CopyStringToBuffer(&IndexBuffers->Data, " data-ref=\"&#%d;", *QuoteIdentifier); speaker *Speaker = GetSpeaker(&Speakers->Speakers, Author);
}
else if(!IsCategorisedAFK(Timestamp))
{ {
CopyStringToBuffer(&IndexBuffers->Data, ",&#%d;", *QuoteIdentifier); // 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, "\"");
} }
HasQuote = TRUE; CopyStringToBuffer(&IndexBuffers->Text,
">%.*s</span>: ", (int)DisplayName.Length, DisplayName.Base);
bool ShouldFetchQuotes = FALSE; Speaker->Seen = TRUE;
if(Config->CacheDir.Length == 0 || time(0) - LastQuoteFetch > 60*60)
{
ShouldFetchQuotes = TRUE;
} }
else if(Author.Length > 0)
{
// .index
CopyStringToBuffer(SearchEntry, "@");
CopyStringToBufferNoFormat(SearchEntry, Author);
CopyStringToBuffer(SearchEntry, ": ");
if(!Speaker && Speakers->Speakers.ItemCount > 0) // .html
{
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)
{
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)
{ {
break; 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))
{
*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);
@ -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.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()