#if 0 ctime -begin ${0%.*}.ctm gcc -g -Wall -fsanitize=address -std=c99 $0 -o ${0%.*} hmml.a ctime -end ${0%.*}.ctm exit #endif typedef unsigned int bool; #define TRUE 1 #define FALSE 0 #include // NOTE(matt): varargs #include // NOTE(matt): printf, sprintf, vsprintf, fprintf, perror #include // NOTE(matt): calloc, malloc, free #include // NOTE(matt): strncmp, memset #include "hmmlib.h" typedef struct { char *Location; char *Ptr; int Size; } buffer; typedef struct { char Timecode[8]; int Identifier; } identifier; typedef struct { char RefTitle[620]; char ID[512]; char URL[512]; char Source[256]; identifier Identifier[12]; int IdentifierCount; } ref_info; #define ArrayCount(A) sizeof(A)/sizeof(*(A)) void ClaimBuffer(char *MemoryArena, int *ClaimedMemory, buffer *Buffer, int Size) { Buffer->Location = MemoryArena + *ClaimedMemory; Buffer->Size = Size; *ClaimedMemory += Buffer->Size; Buffer->Ptr = Buffer->Location; } #if 0 //TODO(matt): Rewrite me ref_info ParseRef(HMML_Reference RefInput) { ref_info Info; if(RefInput.author) { Info.Source = RefInput.author; Info.RefTitle = RefInput.title; return Info; } else if(RefInput.page) { Info.Source = RefInput.site; Info.RefTitle = RefInput.page; return Info; } else { Info.Source = ""; Info.RefTitle = RefInput.site; return Info; } } #endif // TODO(matt): MakeReference() int TimecodeToSeconds(char *Timecode) { int HMS[3] = { 0, 0, 0 }; // 0 == Seconds; 1 == Minutes; 2 == Hours int Colons = 0; while(*Timecode) { //if((*Timecode < '0' || *Timecode > '9') && *Timecode != ':') { return FALSE; } if(*Timecode == ':') { ++Colons; //if(Colons > 2) { return FALSE; } for(int i = 0; i < Colons; ++i) { HMS[Colons - i] = HMS[Colons - (i + 1)]; } HMS[0] = 0; } else { HMS[0] = HMS[0] * 10 + *Timecode - '0'; } ++Timecode; } //if(HMS[0] > 59 || HMS[1] > 59 || Timecode[-1] == ':') { return FALSE; } return HMS[2] * 60 * 60 + HMS[1] * 60 + HMS[0]; } void CopyBuffer(buffer *Dest, buffer *Src) { Src->Ptr = Src->Location; while(*Src->Ptr) { *Dest->Ptr++ = *Src->Ptr++; } } void CopyString(char Dest[], char *Src) { int i = 0; while(*Src) { Dest[i] = *Src++; ++i; } Dest[i] = '\0'; } __attribute__ ((format (printf, 2, 3))) void CopyStringToBuffer(buffer *Dest, char *Format, ...) { va_list Args; va_start(Args, Format); int Length = vsprintf(Dest->Ptr, Format, Args); va_end(Args); Dest->Ptr += Length; } int StringsDiffer(char *A, char *B) { while(*A && *B && *A == *B) { ++A, ++B; } return *A - *B; } int CharToColour(char Char) { if(Char >= 'a' && Char <= 'z') { return (((float)Char - 'a') / ('z' - 'a') * 0xFFFFFF); } else if(Char >= 'A' && Char <= 'Z') { return (((float)Char - 'A') / ('Z' - 'A') * 0xFFFFFF); } else if(Char >= '0' && Char <= '9') { return (((float)Char - '0') / ('9' - '0') * 0xFFFFFF); } else { return 0x777777; } } int StringToColourHash(char *String) { int Result = 0; int i; for(i = 0; String[i]; ++i) { Result += CharToColour(String[i]); } return Result / i; } int StringLength(char *String) { int i = 0; while(String[i]) { ++i; } return i; } char * SanitisePunctuation(char *String) { char *Ptr = String; while(*Ptr) { if(*Ptr == ' ') { *Ptr = '_'; } if((*Ptr < '0' || *Ptr > '9') && (*Ptr < 'a' || *Ptr > 'z') && (*Ptr < 'A' || *Ptr > 'Z')) { *Ptr = '-'; } ++Ptr; } return String; } char *CategoryMedium[] = { "blackboard", "owl", "rant", "research", "run", }; void BuildCategories(buffer *AnnotationClass, buffer *Category, int *MarkerIndex, bool *HasCategory, char *Marker) { for(int i = 0; i < ArrayCount(CategoryMedium); ++i) { if(!StringsDiffer(CategoryMedium[i], Marker)) { CopyStringToBuffer(AnnotationClass, " %s", SanitisePunctuation(Marker)); ++*MarkerIndex; return; } } if(*HasCategory == FALSE) { CopyStringToBuffer(Category, ""); *HasCategory = TRUE; } CopyStringToBuffer(Category, "
", SanitisePunctuation(Marker)); CopyStringToBuffer(AnnotationClass, " cat_%s", SanitisePunctuation(Marker)); ++*MarkerIndex; return; } void GenerateTopicColours(char *Topic) { for(int i = 0; i < ArrayCount(CategoryMedium); ++i) { if(!StringsDiffer(Topic, CategoryMedium[i])) { return; } } FILE *TopicsFile; char *TopicsBuffer; if((TopicsFile = fopen("topics.css", "a+"))) { fseek(TopicsFile, 0, SEEK_END); int TopicsLength = ftell(TopicsFile); fseek(TopicsFile, 0, SEEK_SET); if(!(TopicsBuffer = malloc(TopicsLength))) { perror("hmml_to_html"); return; } fread(TopicsBuffer, TopicsLength, 1, TopicsFile); char *TopicsPtr = TopicsBuffer; while(TopicsPtr - TopicsBuffer < TopicsLength) { TopicsPtr += 39; if(!strncmp(SanitisePunctuation(Topic), TopicsPtr, StringLength(Topic))) { free(TopicsBuffer); return; } while(TopicsPtr - TopicsBuffer < TopicsLength && *TopicsPtr != '\n') { ++TopicsPtr; } ++TopicsPtr; } fprintf(TopicsFile, ".marker .content .categories .category.%s { border-color: #%X; background: #%X; }\n", SanitisePunctuation(Topic), StringToColourHash(Topic), StringToColourHash(Topic)); fclose(TopicsFile); free(TopicsBuffer); } else { perror("hmml_to_html"); return; } } int main(int ArgC, char **Args) { if(ArgC < 2) { fprintf(stderr, "Usage: %s filename(s)\n", Args[0]); return 1; } // NOTE(matt): Init MemoryArena char *MemoryArena; int ArenaSize = 1024 * 1024; if(!(MemoryArena = calloc(ArenaSize, 1))) { perror(Args[0]); return 1; } int ClaimedMemory = 0; // NOTE(matt): Setup buffers and ptrs char *InPtr; buffer Title; buffer QuoteMenu; buffer ReferenceMenu; buffer Player; buffer Annotation; buffer AnnotationHeader; buffer AnnotationClass; buffer AnnotationData; buffer Text; buffer Category; buffer Master; for(int FileIndex = 1; FileIndex < ArgC; ++FileIndex) { FILE *InFile; if(!(InFile = fopen(Args[FileIndex], "r"))) { perror(Args[0]); free(MemoryArena); return 1; } HMML_Output HMML = hmml_parse_file(InFile); fclose(InFile); if(HMML.well_formed) { ClaimBuffer(MemoryArena, &ClaimedMemory, &Title, 1024 * 16); ref_info ReferencesArray[200]; memset(ReferencesArray, 0, sizeof(ReferencesArray)); ClaimBuffer(MemoryArena, &ClaimedMemory, &Player, 1024 * 256); bool HasQuoteMenu = FALSE; bool HasReferenceMenu = FALSE; int QuoteIdentifier = 0x3b1; int RefIdentifier = 1; int UniqueRefs = 0; CopyStringToBuffer(&Title, "
\n" " %s\n", HMML.metadata.project, HMML.metadata.title); CopyStringToBuffer(&Player, "
\n" "
\n" "
\n", HMML.metadata.id, HMML.metadata.project); for(int AnnotationIndex = 0; AnnotationIndex < HMML.annotation_count; ++AnnotationIndex) { HMML_Annotation *Anno = HMML.annotations + AnnotationIndex; bool HasCategory = FALSE; bool HasQuote = FALSE; bool HasReference = FALSE; ClaimBuffer(MemoryArena, &ClaimedMemory, &AnnotationHeader, 512); ClaimBuffer(MemoryArena, &ClaimedMemory, &Category, 256); ClaimBuffer(MemoryArena, &ClaimedMemory, &AnnotationClass, 256); ClaimBuffer(MemoryArena, &ClaimedMemory, &Text, 1024 * 4); CopyStringToBuffer(&AnnotationHeader, "
time)); CopyStringToBuffer(&AnnotationClass, " class=\"marker"); if(Anno->author) { CopyStringToBuffer(&AnnotationClass, " authored"); CopyStringToBuffer(&Text, "%s ", StringToColourHash(Anno->author), Anno->author); } InPtr = Anno->text; int MarkerIndex = 0, RefIndex = 0; while(*InPtr || RefIndex < Anno->reference_count) { if(MarkerIndex < Anno->marker_count && InPtr - Anno->text == Anno->markers[MarkerIndex].offset) { char *Readable = Anno->markers[MarkerIndex].parameter ? Anno->markers[MarkerIndex].parameter : Anno->markers[MarkerIndex].marker; if(Anno->markers[MarkerIndex].type == HMML_MEMBER) { CopyStringToBuffer(&Text, "%.*s", Anno->markers[MarkerIndex].marker, StringToColourHash(Anno->markers[MarkerIndex].marker), StringLength(Readable), InPtr); InPtr += StringLength(Readable); ++MarkerIndex; } else if(Anno->markers[MarkerIndex].type == HMML_PROJECT) { CopyStringToBuffer(&Text, "%s", Anno->markers[MarkerIndex].marker, StringToColourHash(Anno->markers[MarkerIndex].marker), Readable); InPtr += StringLength(Readable); ++MarkerIndex; } else if(Anno->markers[MarkerIndex].type == HMML_CATEGORY) { GenerateTopicColours(Anno->markers[MarkerIndex].marker); BuildCategories(&AnnotationClass, &Category, &MarkerIndex, &HasCategory, Anno->markers[MarkerIndex].marker); } } if(RefIndex < Anno->reference_count && InPtr - Anno->text == Anno->references[RefIndex].offset) { HMML_Reference *CurrentRef = Anno->references + RefIndex; if(!HasReferenceMenu) { ClaimBuffer(MemoryArena, &ClaimedMemory, &ReferenceMenu, 1024 * 16); CopyStringToBuffer(&ReferenceMenu, "
\n" " References ▼\n" "
\n" "
\n"); CopyString(ReferencesArray[RefIdentifier - 1].ID, CurrentRef->editor); CopyString(ReferencesArray[RefIdentifier - 1].RefTitle, "Title"); CopyString(ReferencesArray[RefIdentifier - 1].URL, "http://example.com/"); CopyString(ReferencesArray[RefIdentifier - 1].Source, "Source"); CopyString(ReferencesArray[RefIdentifier - 1].Identifier[ReferencesArray[RefIdentifier - 1].IdentifierCount].Timecode, Anno->time); ReferencesArray[RefIdentifier - 1].Identifier[ReferencesArray[RefIdentifier - 1].IdentifierCount].Identifier = RefIdentifier; ++ReferencesArray[RefIdentifier - 1].IdentifierCount; ++UniqueRefs; HasReferenceMenu = TRUE; } else { for(int i = 0; i < UniqueRefs; ++i) { if(!StringsDiffer(CurrentRef->editor, ReferencesArray[i].ID)) { CopyString(ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Timecode, Anno->time); ReferencesArray[i].Identifier[ReferencesArray[i].IdentifierCount].Identifier = RefIdentifier; ++ReferencesArray[i].IdentifierCount; goto AppendedIdentifier; } } CopyString(ReferencesArray[UniqueRefs].ID, CurrentRef->editor); CopyString(ReferencesArray[UniqueRefs].RefTitle, "Title"); CopyString(ReferencesArray[UniqueRefs].URL, "http://example.com/"); CopyString(ReferencesArray[UniqueRefs].Source, "Source"); CopyString(ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Timecode, Anno->time); ReferencesArray[UniqueRefs].Identifier[ReferencesArray[UniqueRefs].IdentifierCount].Identifier = RefIdentifier; ++ReferencesArray[UniqueRefs].IdentifierCount; ++UniqueRefs; } AppendedIdentifier: if(!HasReference) { ClaimBuffer(MemoryArena, &ClaimedMemory, &AnnotationData, 128); CopyStringToBuffer(&AnnotationData, " data-ref=\"%s", CurrentRef->editor); HasReference = TRUE; } else { CopyStringToBuffer(&AnnotationData, ",%s", CurrentRef->editor); } CopyStringToBuffer(&Text, "%d", RefIdentifier); ++RefIndex; ++RefIdentifier; } if(*InPtr) { *Text.Ptr++ = *InPtr++; } } if(Anno->is_quote) { if(!HasQuoteMenu) { ClaimBuffer(MemoryArena, &ClaimedMemory, &QuoteMenu, 1024 * 16); CopyStringToBuffer(&QuoteMenu, "
\n" " Quotes ▼\n" "
\n" "
\n"); HasQuoteMenu = TRUE; } if(!HasReference) { ClaimBuffer(MemoryArena, &ClaimedMemory, &AnnotationData, 32); CopyStringToBuffer(&AnnotationData, " data-ref=\"&#%d;", QuoteIdentifier); } else { CopyStringToBuffer(&AnnotationData, ",&#%d;", QuoteIdentifier); } HasQuote = TRUE; CopyStringToBuffer(&QuoteMenu, " \n" " \n" "
#%d • %s
\n" "
%s
\n" "
\n" "
\n" " [&#%d;]%s\n" "
\n" "
\n", QuoteIdentifier, Anno->quote.id, "Quote date", "Quote text", TimecodeToSeconds(Anno->time), QuoteIdentifier, Anno->time); if(!Anno->text[0]) { CopyStringToBuffer(&Text, "“Quote text”"); } CopyStringToBuffer(&Text, "&#%d;", QuoteIdentifier); ++QuoteIdentifier; } while(MarkerIndex < Anno->marker_count) { GenerateTopicColours(Anno->markers[MarkerIndex].marker); BuildCategories(&AnnotationClass, &Category, &MarkerIndex, &HasCategory, Anno->markers[MarkerIndex].marker); } CopyStringToBuffer(&AnnotationClass, "\""); CopyBuffer(&AnnotationHeader, &AnnotationClass); if(HasQuote || HasReference) { CopyStringToBuffer(&AnnotationData, "\""); CopyBuffer(&AnnotationHeader, &AnnotationData); } CopyStringToBuffer(&AnnotationHeader, ">\n"); ClaimBuffer(MemoryArena, &ClaimedMemory, &Annotation, 1024 * 4); CopyBuffer(&Annotation, &AnnotationHeader); CopyStringToBuffer(&Annotation, "
%s", Anno->time); if(HasCategory) { CopyStringToBuffer(&Category, ""); CopyBuffer(&Text, &Category); } *Text.Ptr = '\0'; CopyBuffer(&Annotation, &Text); CopyStringToBuffer(&Annotation, "
\n" "
\n" "
%s", Anno->time); CopyBuffer(&Annotation, &Text); CopyStringToBuffer(&Annotation, "
\n" "
\n" "
\n" "
%s", Anno->time); CopyBuffer(&Annotation, &Text); CopyStringToBuffer(&Annotation, "
\n" "
\n" "
\n"); CopyBuffer(&Player, &Annotation); ClaimedMemory -= Text.Size; ClaimedMemory -= AnnotationHeader.Size; ClaimedMemory -= Category.Size; ClaimedMemory -= AnnotationClass.Size; ClaimedMemory -= Annotation.Size; } if(HasQuoteMenu) { CopyStringToBuffer(&QuoteMenu, "
\n" "
\n"); CopyBuffer(&Title, &QuoteMenu); } if(HasReferenceMenu) { for(int i = 0; i < UniqueRefs; ++i) { CopyStringToBuffer(&ReferenceMenu, " \n" " \n" "
%s
\n" "
%s
\n" "
\n" // TODO(matt): Fill the div class="ref_indices" with <= 3 span // class="ref_index" and ensure to put these <=3 spans on the same line without // a space between them "
\n ", ReferencesArray[i].ID, "http://example.com/", "Source", "Title"); for(int j = 0; j < ReferencesArray[i].IdentifierCount; ++j) { CopyStringToBuffer(&ReferenceMenu, "[%d]%s", TimecodeToSeconds(ReferencesArray[i].Identifier[j].Timecode), ReferencesArray[i].Identifier[j].Identifier, ReferencesArray[i].Identifier[j].Timecode); } CopyStringToBuffer(&ReferenceMenu, "\n" "
\n" "
\n"); } CopyStringToBuffer(&ReferenceMenu, "
\n" "
\n"); CopyBuffer(&Title, &ReferenceMenu); } CopyStringToBuffer(&Title, " Annotator: %s\n" "
\n", HMML.metadata.annotator); CopyStringToBuffer(&Player, "
\n" "
\n"); //NOTE(matt): Collate the buffers! ClaimBuffer(MemoryArena, &ClaimedMemory, &Master, 1024 * 512); CopyStringToBuffer(&Master, "\n" " \n" " \n" "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n", HMML.metadata.project); //NOTE(matt): Here is where we do all our CopyBuffer() calls CopyBuffer(&Master, &Title); CopyBuffer(&Master, &Player); // CopyStringToBuffer(&Master, " \n" " \n" "\n"); FILE *OutFile; if(!(OutFile = fopen("out.html", "w"))) { perror(Args[0]); return 1; } fwrite(Master.Location, Master.Ptr - Master.Location, 1, OutFile); fclose(OutFile); ClaimedMemory -= AnnotationData.Size; ClaimedMemory -= QuoteMenu.Size; ClaimedMemory -= ReferenceMenu.Size; ClaimedMemory -= Title.Size; ClaimedMemory -= Master.Size; } else { fprintf(stderr, "%s:%d: %s\n", Args[FileIndex], HMML.error.line, HMML.error.message); } hmml_free(&HMML); } free(MemoryArena); }