#include #include #include #include #include #include #include #include #include // Structure defining the content and metadata of a single page struct Page { char title[80]; char *permalink; struct Page *next; struct PageList *incoming; char *content; }; /*** Helper functions ***/ int min(int x, int y) { return x > y ? y : x; } char * read_file(char *filename) { FILE *file = fopen(filename, "rb"); if (file == NULL) { printf("Warning: Failed to open %s\n", filename); return NULL; } // First, we calculate the length of the file fseek(file, 0, SEEK_END); // Traverse to the end of the file int fileLength = ftell(file); // Get the current position in the file rewind(file); // Go back to the beginning of the file // Allocate enough space in our buffer to hold the entire file. char *buffer = malloc(fileLength + 1); if (buffer == NULL) { printf("Warning: Failed to allocate enough memory for %s\n", filename); fclose(file); return NULL; } fread(buffer, 1, fileLength, file); buffer[fileLength] = 0; fclose(file); return buffer; } /*** Hash map implementation ***/ struct PageMap { struct Page **pages; int capacity; int size; }; int helper_hash_polynomial( char string[], int i, int length, int tableSize, unsigned long long acc ) { if (i == length) { return acc % tableSize; } else { return helper_hash_polynomial(string, i + 1, length, tableSize, acc + string[i] * pow(33, length-i-1)); } } int hash_polynomial(int capacity, char key[]) { return helper_hash_polynomial(key, 0, strlen(key), capacity, 0); } char * to_lower_case(char str[]) { char * lower = malloc((strlen(str) + 1) * sizeof(char)); int i = 0; for (; str[i] != '\0'; i++) { lower[i] = tolower(str[i]); } lower[i] = 0; return lower; } struct PageMap * map(int capacity) { struct PageMap *map = malloc(sizeof(struct PageMap)); map->pages = calloc(capacity, sizeof(struct Page *)); map->capacity = capacity; map->size = 0; return map; } void map_free(struct PageMap *map) { free(map->pages); free(map); } void map_put(struct PageMap *map, char title[], struct Page *page) { char *lowercased = to_lower_case(title); int index = hash_polynomial(map->capacity, lowercased); while (map->pages[index] != NULL) index++; map->pages[index] = page; map->size++; // Resize the map if we're running low on space if (map->size/map->capacity > 0.75) { map->pages = realloc(map->pages, map->capacity * 2); for (int i = map->capacity; i < map->capacity * 2; i++) { map->pages[i] = 0; } map->capacity *= 2; } free(lowercased); } struct Page * map_get(struct PageMap *map, char title[]) { char *lowercased = to_lower_case(title); int index = hash_polynomial(map->capacity, lowercased); struct Page *page = map->pages[index]; char *lowercasedFoundTitle = to_lower_case(page->title); while (strcmp(lowercasedFoundTitle, lowercased) != 0) { page = map->pages[++index]; free(lowercasedFoundTitle); lowercasedFoundTitle = to_lower_case(page->title); } free(lowercased); free(lowercasedFoundTitle); return page; } /*** Dynamic Array Implementation ***/ struct PageList { struct Page **pages; int length; int capacity; }; struct PageList * page_list(int capacity) { struct PageList *pageList = malloc(sizeof(struct PageList)); pageList->pages = malloc(sizeof(struct Page *) * capacity); pageList->capacity = capacity; pageList->length = 0; return pageList; } void page_list_insert(struct PageList *list, struct Page *page) { // If the page is already present in the list, we don't bother adding it again for (int i = 0; i < list->length; i++) { if (page == list->pages[i]) { return; } } // Increase the length of the array if necessary if (list->length >= list->capacity) { list->pages = realloc(list->pages, (list->capacity + 1)*sizeof(struct Page *)); } list->pages[list->length] = page; list->length++; } void page_list_free(struct PageList *list) { free(list->pages); free(list); } /*** Templating ***/ char * concat_strings(int n, ...) { va_list list; va_start(list, n); int length = 0; char **strings = malloc(n * sizeof(char *)); memset(strings, 0, n * sizeof(char *)); for (int i = 0; i < n; i++) { strings[i] = va_arg(list, char *); length += strlen(strings[i]); } char * joinedString = malloc((length + 1) * sizeof(char *)); memset(joinedString, 0, (length + 1) * sizeof(char *)); strcpy(joinedString, strings[0]); for (int i = 1; i < n; i++) { strcat(joinedString, strings[i]); } free(strings); va_end(list); return joinedString; } char * substitute_string(char dest[], char sub[], char *start, char *end) { int startIndex = start - dest; int newLength = strlen(dest) - (end - start) + strlen(sub) + 1; char *compiled = malloc(newLength * sizeof(char)); memset(compiled, 0, newLength * sizeof(char)); strncpy(compiled, dest, startIndex); strcat(compiled, sub); strcat(compiled, end); return compiled; } static struct argp_option options[] = { { "src", 's', "dir", 0, "Source directory of pages", 0 }, { "template", 't', "file", 0, "Template file path", 0 }, { "output", 'o', "dir", 0, "Output directory", 0 }, { 0 } }; static char *pagesLocation = "./pages"; static char *templateFileName = "./template.html"; static char *outputDirectoryName = "./build"; static int parse_opt(int key, char *arg, struct argp_state *state) { switch (key) { case 's': { pagesLocation = arg; break; } case 't': { templateFileName = arg; break; } case 'o': { outputDirectoryName = arg; break; } } return 0; } int main(int argc, char *argv[]) { int initialInboundCapacity = 2; struct argp argp = {options, parse_opt, 0, 0 }; argp_parse(&argp, argc, argv, 0, 0, 0); char *templateContent = read_file(templateFileName); if (templateContent == NULL) { // If no template is given, we'll just dump the rendered markdown into a // plaintext file templateContent = malloc(sizeof(char) * 12); strcpy(templateContent, "{{content}}\0"); } DIR *pagesDir = opendir(pagesLocation); if (pagesDir == NULL) { fprintf(stderr, "Unable to open directory: %s\n", pagesLocation); return 1; } struct PageMap *pageMap = map(100); // Contains some information about the current file picked from pagesDir struct dirent *fileEntry = readdir(pagesDir); struct Page *currentPage = malloc(sizeof(struct Page)); struct Page *firstPage = currentPage; while (fileEntry != NULL) { // Ignore hidden files, ".", and ".." on Unix if (fileEntry->d_name[0] == '.') { fileEntry = readdir(pagesDir); continue; } // Determine the base name of the file int filenameLength = strlen(fileEntry->d_name); char fileBasename[filenameLength]; memset(fileBasename, 0, filenameLength); unsigned char foundPoint = 0; for (int i = filenameLength - 1; i >= 0; i--) { if (foundPoint) { // Start writing the base name into the string fileBasename[i] = fileEntry->d_name[i]; } else if (fileEntry->d_name[i] == '.') { // Start writing on the next iteration foundPoint = 1; } else { // Write zeros where the extension would have been fileBasename[i] = 0; } } currentPage->incoming = page_list(initialInboundCapacity); // Build the page's permalink currentPage->permalink = concat_strings(2, fileBasename, ".html"); // Construct the relative path // The two accounts for the slash and the terminal zero char *relativePath = concat_strings(3, pagesLocation, "/", fileEntry->d_name); char *buffer = read_file(relativePath); free(relativePath); if (buffer == NULL) { fileEntry = readdir(pagesDir); continue; } // Get a pointer to the start of the content part of the page char *endOfFirstLine = strchr(buffer, '\n'); if (endOfFirstLine == NULL) { printf("Warning: First line in %s/%s must be the title\n", pagesLocation, fileEntry->d_name); free(buffer); fileEntry = readdir(pagesDir); continue; } // We subtract the buffer pointer from the pointer to the end of the // first line to get the length of the title int titleLength = (endOfFirstLine - buffer)/sizeof(char); // Save the content string for later by mallocing it char *contentBuffer = endOfFirstLine; currentPage->content = calloc(strlen(buffer), sizeof(char)); strcpy(currentPage->content, contentBuffer); // Copy the first line (title) into its respective field strncpy(currentPage->title, buffer, min(titleLength, 80)); currentPage->title[min(titleLength, 80)] = 0; // Insert it into the hash map for lookup later map_put(pageMap, currentPage->title, currentPage); // Get ready to process the next page fileEntry = readdir(pagesDir); if (fileEntry != NULL) { struct Page *nextPage = malloc(sizeof(struct Page)); // Swap the pages currentPage->next = nextPage; currentPage = nextPage; } else { currentPage->next = NULL; } free(buffer); } // Create the directory if it doesn't exist char *createOutputDir = concat_strings(3, "mkdir ", outputDirectoryName, " 2> /dev/null"); system(createOutputDir); free(createOutputDir); /*** Link Processing ***/ currentPage = firstPage; while (currentPage != NULL) { // Scan the file for links // This pointer is updated upon each iteration char *nextLinkStart = strstr(currentPage->content, "[["); while (nextLinkStart != NULL) { char *nextLinkEnd = strstr(nextLinkStart, "]]"); if (nextLinkEnd == NULL) { // This link is broken printf("Warning: \"%s\" contains a broken link", currentPage->title); break; } int linkLength = nextLinkEnd - nextLinkStart; char *nextVerticalBar = strchr(nextLinkStart, '|'); char *linkTitle = calloc(linkLength - 3, sizeof(char)); char *linkPageTitle = NULL; if (nextVerticalBar != NULL && nextVerticalBar < nextLinkEnd) { // The link is in the from [[link title|page title]] strncpy(linkTitle, nextLinkStart + 2, (nextVerticalBar - 1) - (nextLinkStart + 1)); linkPageTitle = calloc((nextLinkEnd - 2) - nextVerticalBar, sizeof(char)); strncpy(linkPageTitle, nextVerticalBar + 1, (nextLinkEnd - 1) - nextVerticalBar); } else { // The link is of the form [[page title]] linkPageTitle = linkTitle; strncpy(linkTitle, nextLinkStart + 2, (linkLength - 2)*sizeof(char)); } struct Page *linkedPage = map_get(pageMap, linkPageTitle); char *compiledLink = NULL; if (linkedPage == NULL) { // i.e. the page does not exist compiledLink = concat_strings(3, "", linkTitle, "" ); } else { page_list_insert(linkedPage->incoming, currentPage); compiledLink = concat_strings(5, "[", linkTitle, "](", linkedPage->permalink, ")"); } char *newContent = substitute_string( currentPage->content, compiledLink, nextLinkStart, nextLinkEnd + 2); free(currentPage->content); currentPage->content = newContent; free(compiledLink); if (linkTitle != linkPageTitle) { free(linkPageTitle); } free(linkTitle); // Move to the next chunk of the file // NOTE: This is suboptimal, because we search for "[[" from the // beginning of the content at each iteration. We could start from // the nextLinkEnd pointer, but since the memory is reallocated, we'd // need to take into account that this pointer now points to a chunk // of deallocated memory nextLinkStart = strstr(currentPage->content, "[["); } currentPage = currentPage->next; } /*** Rendering ***/ currentPage = firstPage; while (currentPage != NULL) { // Compile the markdown currentPage->content = cmark_markdown_to_html( currentPage->content, strlen(currentPage->content), (1 << 17) // Disables safe mode, allowing raw HTML ); // Insert the content into the template char *renderedPage = NULL; char *templateTagStart = strstr(templateContent, "{{content}}"); char *renderedWithContent = substitute_string( templateContent, currentPage->content, templateTagStart, templateTagStart + 11 ); renderedPage = renderedWithContent; char *incomingTagStart = strstr(renderedPage, "{{incoming}}"); if (incomingTagStart != NULL) { // Build the incoming links list int incomingListSize = 37; //
    \n
\n for (int i = 0; i < currentPage->incoming->length; i++) { // `
  • [title]
  • \n` struct Page *page = currentPage->incoming->pages[i]; incomingListSize += 27 + strlen(page->title) + strlen(page->permalink); } char *incomingLinksList = malloc((incomingListSize + 1) * sizeof(char)); memset(incomingLinksList, 0, (incomingListSize + 1) * sizeof(char)); strcpy(incomingLinksList, "
      \n"); for (int i = 0; i < currentPage->incoming->length; i++) { struct Page *page = currentPage->incoming->pages[i]; char *link = concat_strings(5, "
    • permalink, "\">", page->title, "
    • \n" ); strcat(incomingLinksList, link); free(link); } strcat(incomingLinksList, "
    \n"); char *renderedWithInbound = substitute_string( renderedPage, incomingLinksList, incomingTagStart, incomingTagStart + 12 ); renderedPage = renderedWithInbound; free(renderedWithContent); } // Output the page to the file char *outputFileName = concat_strings(3, outputDirectoryName, "/", currentPage->permalink); FILE *outputFile = fopen(outputFileName, "w"); if (outputFile != NULL) { fputs(renderedPage, outputFile); fclose(outputFile); } else { printf("Warning: failed to create %s\n", outputFileName); } free(outputFileName); free(renderedPage); currentPage = currentPage->next; } printf("Pages built successfully in %s\n", outputDirectoryName); /*** Deallocation and whatnot ***/ currentPage = firstPage; while (currentPage != NULL) { free(currentPage->content); free(currentPage->permalink); page_list_free(currentPage->incoming); struct Page *next = currentPage->next; free(currentPage); currentPage = next; } closedir(pagesDir); free(templateContent); map_free(pageMap); return 0; }