540 lines
15 KiB
C
540 lines
15 KiB
C
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdarg.h>
|
|
#include <string.h>
|
|
#include <dirent.h>
|
|
#include <math.h>
|
|
#include <ctype.h>
|
|
#include <cmark.h>
|
|
|
|
// 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;
|
|
}
|
|
|
|
int main(int argc, char *argv[]) {
|
|
char pagesLocation[256] = "./pages";
|
|
int initialInboundCapacity = 2;
|
|
char templateFileName[256] = "./template.html";
|
|
char outputDirectoryName[256] = "./build";
|
|
|
|
/*** Argument Parsing ***/
|
|
for (int i = 1; i < argc; i++) {
|
|
if (strcmp(argv[i], "--pages") == 0) {
|
|
// We only want to do this if a directory was actually supplied
|
|
if (i + 1 < argc) {
|
|
i++;
|
|
strcpy(pagesLocation, argv[i]);
|
|
}
|
|
} else if (strcmp(argv[i], "--template") == 0) {
|
|
if (i + 1 < argc) {
|
|
i++;
|
|
strcpy(templateFileName, argv[i]);
|
|
}
|
|
} else if (strcmp(argv[i], "--output-dir") == 0) {
|
|
if (i + 1 < argc) {
|
|
i++;
|
|
strcpy(outputDirectoryName, argv[i]);
|
|
}
|
|
} else if (strcmp(argv[i], "--incoming-cap") == 0) {
|
|
if (i + 1 < argc) {
|
|
i++;
|
|
initialInboundCapacity = atoi(argv[i]);
|
|
}
|
|
} else {
|
|
printf("Unknown argument: %s\n", argv[i]);
|
|
}
|
|
}
|
|
|
|
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,
|
|
"<a class=\"calathea-404\" href=\"#\">",
|
|
linkTitle,
|
|
"</a>"
|
|
);
|
|
} 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; // <ul class="calathea-incoming">\n</ul>\n
|
|
for (int i = 0; i < currentPage->incoming->length; i++) {
|
|
// ` <li><a href=\"[permalink]\">[title]</a></li>\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, "<ul class=\"calathea-incoming\">\n");
|
|
for (int i = 0; i < currentPage->incoming->length; i++) {
|
|
struct Page *page = currentPage->incoming->pages[i];
|
|
char *link = concat_strings(5,
|
|
" <li><a href=\"",
|
|
page->permalink,
|
|
"\">",
|
|
page->title,
|
|
"</a></li>\n"
|
|
);
|
|
strcat(incomingLinksList, link);
|
|
free(link);
|
|
}
|
|
strcat(incomingLinksList, "</ul>\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;
|
|
}
|