ccalathea/calathea.c

389 lines
11 KiB
C

#include <stdio.h>
#include <stdlib.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;
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);
if (buffer == NULL) {
printf("Warning: Failed to allocate enough memory for %s\n", filename);
fclose(file);
return NULL;
}
fread(buffer, 1, fileLength, file);
fclose(file);
return buffer;
}
/*** Hash map implementation ***/
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 mapSize, char key[]) {
return helper_hash_polynomial(key, 0, strlen(key), mapSize, 0);
}
void to_lower_case(char dest[], char str[]) {
int i = 0;
for (; str[i] != '\0'; i++) {
dest[i] = tolower(str[i]);
}
dest[i] = 0;
}
void map_put(struct Page **map, int mapSize, char title[], struct Page *page) {
char lowercased[80];
to_lower_case(lowercased, title);
int index = hash_polynomial(mapSize, lowercased);
map[index] = page;
}
struct Page * map_get(struct Page **map, int mapSize, char title[]) {
char lowercased[80];
to_lower_case(lowercased, title);
int index = hash_polynomial(mapSize, lowercased);
struct Page *page = map[index];
return page;
}
/*** Templating ***/
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));
compiled[0] = 0;
strncpy(compiled, dest, startIndex);
strcat(compiled, sub);
strcat(compiled, end);
return compiled;
}
int main(int argc, char *argv[]) {
char pagesLocation[256] = "./pages";
int mapSize = 1000;
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], "--table-size") == 0) {
if (i + 1 < argc) {
i++;
mapSize = atoi(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 {
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 Page ** pageMap = malloc(mapSize * sizeof(struct Page *));
memset(pageMap, 0, mapSize * sizeof(struct Page *));
// 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;
}
}
// Build the page's permalink
currentPage->permalink = malloc(strlen(fileBasename) + 6);
strcpy(currentPage->permalink, fileBasename);
strcat(currentPage->permalink, ".html");
// Construct the relative path
// The two accounts for the slash and the terminal zero
char relativePath[strlen(pagesLocation) + filenameLength + 2];
memset(relativePath, 0, strlen(pagesLocation) + filenameLength + 2);
strcpy(relativePath, pagesLocation);
strcat(relativePath, "/");
strcat(relativePath,fileEntry->d_name);
relativePath[strlen(pagesLocation) + filenameLength + 1] = 0;
char *buffer = read_file(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 = malloc(
sizeof(char) * (strlen(buffer) - titleLength + 1)
);
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, mapSize, 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 = malloc(
(7 + strlen(outputDirectoryName)) * sizeof(char)
);
strcpy(createOutputDir, "mkdir \0");
strcat(createOutputDir, outputDirectoryName);
system(createOutputDir);
free(createOutputDir);
/*** Page 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;
// Determine the exact title of the link
char title[linkLength - 3];
strncpy(title, nextLinkStart + 2, (linkLength - 2)*sizeof(char));
title[linkLength - 2] = 0;
struct Page *linkedPage = map_get(pageMap, mapSize, title);
char *compiledLink;
if (linkedPage == NULL) {
// i.e. the page does not exist
compiledLink = malloc(strlen(title) + 38);
strcpy(compiledLink, "<a class=\"calathea-404\" href=\"#\">");
strcat(compiledLink, title);
strcat(compiledLink, "</a>\0");
} else {
compiledLink = malloc(strlen(title) + strlen(linkedPage->permalink) + 5);
strcpy(compiledLink, "[");
strcat(compiledLink, title);
strcat(compiledLink, "](");
strcat(compiledLink, linkedPage->permalink);
strcat(compiledLink, ")\0");
}
char *newContent = substitute_string(
currentPage->content, compiledLink, nextLinkStart, nextLinkEnd + 2);
free(currentPage->content);
currentPage->content = newContent;
free(compiledLink);
// 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, "[[");
}
// Compile the markdown
currentPage->content = cmark_markdown_to_html(
currentPage->content,
strlen(currentPage->content),
(1 << 17) // Disables safe mode, allowing raw HTML
);
char *templateTagStart = strstr(templateContent, "{{content}}");
char *renderedPageContent = substitute_string(
templateContent,
currentPage->content,
templateTagStart,
templateTagStart + 11
);
// Output the page to the file
char *outputFileName = malloc(
(strlen(outputDirectoryName)
+ strlen(currentPage->permalink)
+ 2));
strcpy(outputFileName, outputDirectoryName);
strcat(outputFileName, "/\0");
strcat(outputFileName, currentPage->permalink);
FILE *outputFile = fopen(outputFileName, "w");
if (outputFile != NULL) {
fputs(renderedPageContent, outputFile);
fclose(outputFile);
} else {
printf("Warning: failed to create %s\n", outputFileName);
}
free(outputFileName);
free(renderedPageContent);
currentPage = currentPage->next;
}
/*** Deallocation and whatnot ***/
currentPage = firstPage;
while (currentPage != NULL) {
free(currentPage->content);
free(currentPage->permalink);
struct Page *next = currentPage->next;
free(currentPage);
currentPage = next;
}
closedir(pagesDir);
free(templateContent);
// Deallocate all the pages
return 0;
}