Annotation-System/hmmlconv/hmmlconv.c

573 lines
14 KiB
C

#if 0
cc "$0" -g -std=c99 -D_POSIX_SOURCE -o "${0%.c}"
exit
#endif
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h> // access
#include <string.h> // strerror
#include <fcntl.h> // open
#include <sys/types.h> // open, mkdir
#include <sys/stat.h> // open, mkdir
#define LError(Format, ...) do { fprintf(stderr, " l.%d: " Format, LineNumber, ##__VA_ARGS__); } while(0)
typedef struct
{
char *Site;
char *Page;
char *URL;
} Resource;
static Resource Resources[64];
static int ResCount;
// Removes directories from a path, e.g.: /path/to/thing -> thing
char *BaseName(char *Path)
{
char *Base = Path;
for(char *c = Path; *c; ++c)
{
if(*c == '/')
{
Base = c + 1;
}
}
return Base;
}
int PromptOverwrite(char *Name, int* Always)
{
while(1){
printf("The file '%s' exists. Overwrite? [Yes|No|Always|eXit]\n> ", Name);
fflush(stdout);
int C;
switch((C = getchar()))
{
case 'a':
case 'A':
*Always = 1;
case 'y':
case 'Y':
while(getchar() != '\n');
return 1;
case 'n':
case 'N':
while(getchar() != '\n');
return 0;
case 'x':
case 'X':
case EOF:
exit(0);
}
printf("Unknown option '%c'\n", C);
if(C != '\n'){
while(getchar() != '\n');
}
}
}
char *ReadWholeFile(FILE *File)
{
fseek(File, 0, SEEK_END);
size_t Size = ftell(File);
fseek(File, 0, SEEK_SET);
char *Buffer = malloc(Size + 1);
if(!Buffer)
{
perror("malloc");
exit(1);
}
fread(Buffer, Size, 1, File);
Buffer[Size] = 0;
return Buffer;
}
void SkipWhitespace(char **Ptr)
{
while(**Ptr && **Ptr <= ' ')
{
++*Ptr;
}
}
char *InPlaceUnescape(char *In)
{
if(*In == '"') ++In;
char *Result = In;
char *Out = In;
while(*In && *In != '"')
{
if(*In == '\\')
{
++In;
}
*Out++ = *In++;
}
if(*In != '"' || In[1] != '\0')
{
return NULL;
}
*Out = 0;
return Result;
}
int IsAlNum(char C)
{
return (C >= '0' && C <= '9') || (C >= 'a' && C <= 'z') || (C >= 'A' && C <= 'Z');
}
Resource *LookupResource(char *Tag, char *Line)
{
Resource *Res = NULL;
for(int i = 0; i < ResCount; ++i)
{
if(strcmp(Tag, Resources[i].Site) == 0 || strcmp(Tag, Resources[i].Page) == 0)
{
Res = Resources + i;
if(strstr(Line, Res->Page))
{
break;
}
}
}
return Res;
}
enum
{
TC_NAN = 1,
TC_COLONS = 2,
TC_OUT_OF_RANGE = 3
};
int
ValidateTimecode(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 TC_NAN; }
if(*Timecode == ':')
{
++Colons;
if(Colons > 2) { return TC_COLONS; }
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 TC_OUT_OF_RANGE; }
return 0;
}
void ProcessAnnotation(char *Line, int LineNumber, FILE *OutFile)
{
SkipWhitespace(&Line);
if(*Line != '"') { LError("Syntax error, line must begin with a \": %.*s...\n", 8, Line); exit(1); }
fputc('[', OutFile);
char Timecode[9];
char *Ptr = Timecode;
while(*++Line != '"' && *Line != '\n')
{
*Ptr++ = *Line;
}
*Ptr = '\0';
switch(ValidateTimecode(Timecode))
{
case 0: break;
case TC_NAN: LError("Invalid timecode, not a number: %s\n", Timecode); exit(1);
case TC_COLONS: LError("Invalid timecode, too many colons: %s\n", Timecode); exit(1);
case TC_OUT_OF_RANGE: LError("Invalid timecode, not 0-59: %s\n", Timecode); exit(1);
}
fputs(Timecode, OutFile);
fputc(']', OutFile);
if(*++Line != ':') { LError("Syntax error, missing : before: %.*s...\n", 8, Line); exit(1); }
if(*++Line != ' ') { LError("Syntax error, missing space before: %.*s...\n", 8, Line); exit(1); }
if(*++Line != '"') { LError("Syntax error, missing \" before: %.*s...\n", 8, Line); exit(1); }
char *LinePtr = Line;
if(!(Line = InPlaceUnescape(Line)))
{
LError("Syntax error, missing closing \": %.*s\n", Line-LinePtr, LinePtr); exit(1);
}
// convert author
if(Line[0] == '@')
{
char *P = strchr(Line, ' ');
if(!P) { LError("Invalid annotation, cannot contain only a member: %.*s...\n", 8, Line); exit(1); }
if(P[-1] == ':')
{
--P;
}
fprintf(OutFile, "[@%.*s]", (int)(P - Line), Line);
Line = P+1;
}
char RefBuf[256];
char *RunStart = Line;
char *FirstSpace = NULL;
int ConsiderAuthored = 0;
fputc('[', OutFile);
int QuoteID = -1;
for(LinePtr = Line; *LinePtr; ++LinePtr)
{
int ScanBytes = 0;
int TmpQuoteID;
// convert Resource -> ref
// TODO(matt): Gather the whole Resources section, and prompt if we match more than one resource
// TODO(matt): (Maybe) Lookup individual words from the [see Resources] thing
if(LinePtr[0] == ' ' && LinePtr[1] == '[' && sscanf(LinePtr+1, "[see Resources, %255[^]]]%n", RefBuf, &ScanBytes) == 1 && ScanBytes)
{
//printf("Find Resource [%s]\n", RefBuf);
Resource *Res = LookupResource(RefBuf, Line);
if(Res){
fprintf(OutFile,
"%.*s[ref\n site=\"%s\"\n page=\"%s\"\n url=\"%s\"]",
(int)(LinePtr - RunStart),
RunStart,
Res->Site,
Res->Page,
Res->URL);
LinePtr += ScanBytes;
RunStart = LinePtr+1;
}
else
{
LError("WARNING: can't find resource: %s :(\n", RefBuf);
}
}
// convert quotes
else if(LinePtr[0] == ' ' && LinePtr[1] == '(' && sscanf(LinePtr+1, "(!quote %d)%n", &TmpQuoteID, &ScanBytes) == 1 && ScanBytes)
{
QuoteID = TmpQuoteID;
fprintf(OutFile, "%.*s", (int)(LinePtr - RunStart), RunStart);
LinePtr += ScanBytes;
RunStart = LinePtr+1;
}
// Used for getting name for Q:'s
else if(*LinePtr == ' ')
{
if(!FirstSpace)
{
FirstSpace = LinePtr;
ConsiderAuthored = 1;
}
else
{
ConsiderAuthored = 0;
}
}
// Q:
else if(LinePtr[0] == 'Q' && LinePtr[1] == ':' && ConsiderAuthored)
{
LinePtr += 2;
RunStart = LinePtr+1;
fprintf(OutFile, "@%.*s][", (int)(FirstSpace - Line), Line);
}
// Escape stuff
else if(*LinePtr == ']' ||
*LinePtr == '[' ||
*LinePtr == '\\' ||
(LinePtr > Line && LinePtr[-1] == ' ' && strchr(":~@", LinePtr[0]) && !IsAlNum(LinePtr[1])))
{
fprintf(OutFile, "%.*s\\", (int)(LinePtr - RunStart), RunStart);
RunStart = LinePtr;
}
}
// write out remaining text
fprintf(OutFile, "%.*s]", (int)(LinePtr - RunStart), RunStart);
// write out quote node if applicable
if(QuoteID != -1)
{
fprintf(OutFile, "[quote %d]", QuoteID);
}
fputc('\n', OutFile);
}
int ProcessFile(char *InFileName, FILE *InFile, FILE *OutFile)
{
char *Contents = ReadWholeFile(InFile);
char *Ptr;
if(!(getenv("HERO")))
{
printf("Processing [%s]...\n", InFileName);
}
// Resources
ResCount = 0;
if((Ptr = strstr(Contents, "\n## Resources")))
{
char *LinePtrState;
char *LinePtr = strtok_r(Ptr+13, "\r\n", &LinePtrState);
for(; LinePtr; LinePtr = strtok_r(NULL, "\r\n", &LinePtrState))
{
if(LinePtr[0] == '#' && LinePtr[1] == '#')
{
break;
}
if(LinePtr[0] != '*' || LinePtr[1] != ' ')
{
continue;
}
LinePtr += 2;
// Site (seems to be optional [day 205])
{
int Inc = 2;
char *Separator = strstr(LinePtr, " [");
if(!Separator)
{
Separator = strstr(LinePtr, " '[");
Inc = 3;
}
if(!Separator)
{
Separator = strstr(LinePtr, "[");
Inc = 1;
}
if(!Separator) continue;
char *S = (Separator[-1] == ',' || Separator[-1] == ':') ? Separator - 1 : Separator;
*S = 0;
Resources[ResCount].Site = LinePtr;
LinePtr = Separator + Inc;
}
// Page
{
char *Separator = strstr(LinePtr, "](");
if(!Separator) continue;
// needed for day115, maybe others
{
char *S = Separator[-1] == '*' ? Separator - 1 : Separator;
*S = 0;
}
if(*LinePtr == '*') ++LinePtr;
Resources[ResCount].Page = LinePtr;
LinePtr = Separator + 2;
}
// URL
{
char *Separator = strstr(LinePtr, ")");
if(!Separator) continue;
*Separator = 0;
Resources[ResCount].URL = LinePtr;
LinePtr = Separator + 1;
}
if(!(getenv("HERO")))
{
printf("Add Res: %s\n", Resources[ResCount].Site);
}
++ResCount;
}
}
char *Title;
char *VideoID;
enum {
STATE_METADATA,
STATE_MARKERS,
} State = STATE_METADATA;
int LineNumber = 1;
char *LineState;
char *LinePtr = strtok_r(Contents, "\r\n", &LineState);
for(; LinePtr; LinePtr = strtok_r(NULL, "\r\n", &LineState))
{
switch(State)
{
case STATE_METADATA: {
if(strncmp(LinePtr, "title: ", 7) == 0)
{
Title = InPlaceUnescape(LinePtr + 7);
}
else if(strncmp(LinePtr, "videoId: ", 9) == 0)
{
VideoID = InPlaceUnescape(LinePtr + 9);
}
else if(strncmp(LinePtr, "markers:", 8) == 0)
{
fprintf(OutFile, "[video member=cmuratori project=hero twitch_username=handmade_hero title=\"%s\" platform=youtube id=%s annotator=Miblo]\n", Title, VideoID);
State = STATE_MARKERS;
}
} break;
case STATE_MARKERS: {
if(strncmp(LinePtr, "---", 3) == 0){
goto Done;
} else {
ProcessAnnotation(LinePtr, LineNumber, OutFile);
}
} break;
}
++LineNumber;
}
Done:
fputs("[/video]\n", OutFile);
free(Contents);
return 1;
}
int main(int ArgC, char **Args)
{
if(ArgC < 3)
{
fprintf(stderr, "Usage: %s [Files...] [Output Directory]\n", Args[0]);
return 1;
}
const char *OutDirName = Args[ArgC-1];
// Check if the directory exists and we can write to it.
// If it doesn't exist, create it.
if(access(OutDirName, R_OK | W_OK | X_OK) == -1)
{
if(errno == ENOENT)
{
if(mkdir(OutDirName, 0777) == -1)
{
fprintf(stderr, "Couldn't create directory [%s]: %s\n", OutDirName, strerror(errno));
}
}
else
{
fprintf(stderr, "Error accessing %s: %s\n", OutDirName, strerror(errno));
return 1;
}
}
// Loop through all the files and convert them.
int Errors = 0;
int AlwaysOverwrite = 0;
if(getenv("HERO"))
{
AlwaysOverwrite = 1;
}
for(int i = 1; i < ArgC-1; ++i)
{
char *FileNameBase = BaseName(Args[i]);
char OutNameBuf[strlen(OutDirName) + strlen(FileNameBase) + 7];
sprintf(OutNameBuf, "%s/%s.hmml", OutDirName, FileNameBase);
FILE *OutFile = NULL;
{
int OpenFlags = O_CREAT | O_EXCL | O_WRONLY;
// Use the POSIX open function for a change, and so we can more easily
// test for the file already existing.
OpenOutput:;
int OutFileDesc = open(OutNameBuf, OpenFlags, 0666);
if(OutFileDesc == -1)
{
if(errno == EEXIST)
{
if(AlwaysOverwrite || PromptOverwrite(OutNameBuf, &AlwaysOverwrite))
{
OpenFlags = (OpenFlags & ~O_EXCL) | O_TRUNC;
goto OpenOutput;
}
}
else
{
perror("open");
}
} else {
OutFile = fdopen(OutFileDesc, "w");
if(!OutFile)
{
fprintf(stderr, "Error opening %s: %s\n", OutNameBuf, strerror(errno));
}
}
}
FILE* InFile = fopen(Args[i], "r");
if(!InFile)
{
fprintf(stderr, "Error opening %s: %s\n", Args[i], strerror(errno));
}
if(InFile && OutFile && !ProcessFile(FileNameBase, InFile, OutFile))
{
++Errors;
}
if(InFile) fclose(InFile);
if(OutFile) fclose(OutFile);
}
if(Errors){
printf("There were errors processing %d files.\n", Errors);
}
return Errors != 0;
}