#if 0 cc "$0" -g -std=c99 -D_POSIX_SOURCE -o "${0%.c}" exit #endif #include #include #include #include // access #include // strerror #include // open #include // open, mkdir #include // 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, invalid quoting in %.*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 stream_platform=twitch stream_username=handmade_hero project=hero title=\"%s\" vod_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; }