/* $Id: clover.c 13 2024-09-06 22:37:46Z nishi $ */
/* --- START LICENSE --- */
/* -------------------------------------------------------------------------- */
/* HTML 2.0 compliant static site generator */
/* -------------------------------------------------------------------------- */
/* Copyright (c) 2024 Nishi. */
/* Redistribution and use in source and binary forms, with or without modific */
/* ation, are permitted provided that the following conditions are met: */
/* 1. Redistributions of source code must retain the above copyright noti */
/* ce, this list of conditions and the following disclaimer. */
/* 2. Redistributions in binary form must reproduce the above copyright n */
/* otice, this list of conditions and the following disclaimer in the documen */
/* tation and/or other materials provided with the distribution. */
/* 3. Neither the name of the copyright holder nor the names of its contr */
/* ibutors may be used to endorse or promote products derived from this softw */
/* are without specific prior written permission. */
/* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS */
/* " AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, TH */
/* E IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPO */
/* SE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS */
/* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CON */
/* SEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITU */
/* TE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPT */
/* ION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, S */
/* TRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN AN */
/* Y WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY */
/* OF SUCH DAMAGE. */
/* -------------------------------------------------------------------------- */
/* --- END LICENSE --- */
#include <dirent.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include <expat.h>
#define CLOVER_VERSION "1.0E"
char* concat_str(const char* str1, const char* str2) {
char* r = malloc(strlen(str1) + strlen(str2) + 1);
memcpy(r, str1, strlen(str1));
memcpy(r + strlen(str1), str2, strlen(str2));
r[strlen(str1) + strlen(str2)] = 0;
return r;
}
char* concat3_str(const char* str1, const char* str2, const char* str3) {
char* tmp = concat_str(str1, str2);
char* str = concat_str(tmp, str3);
free(tmp);
return str;
}
char* dup_str(const char* str) {
char* r = malloc(strlen(str) + 1);
memcpy(r, str, strlen(str));
r[strlen(str)] = 0;
return r;
}
char* input_dir;
char* output_dir;
char* title;
char* icon;
char* http_path;
char* static_dir;
int parse_config(const char* path) {
FILE* f = fopen(path, "r");
if(f == NULL) {
fprintf(stderr, "%s: could not open the file\n", path);
return 1;
}
char* bf = malloc(2);
bf[1] = 0;
char* str = malloc(1);
str[0] = 0;
while(1) {
fread(bf, 1, 1, f);
if(feof(f)) break;
if(bf[0] == '\n') {
int i;
bool found = false;
for(i = 0; str[i] != 0; i++) {
if(str[i] == ' ') {
for(; str[i] != 0 && (str[i] == ' ' || str[i] == '\t'); i++) str[i] = 0;
found = true;
break;
}
}
if(str[0] == '#') {
} else if(strcmp(str, "output") == 0) {
if(found) {
free(output_dir);
output_dir = dup_str(str + i);
mkdir(str + i, 0775);
}
} else if(strcmp(str, "input") == 0) {
if(found) {
free(input_dir);
input_dir = dup_str(str + i);
}
} else if(strcmp(str, "static") == 0) {
if(found) {
free(static_dir);
static_dir = dup_str(str + i);
}
} else if(strcmp(str, "title") == 0) {
if(found) {
free(title);
title = dup_str(str + i);
}
} else if(strcmp(str, "icon") == 0) {
if(found) {
free(icon);
icon = dup_str(str + i);
}
} else if(strcmp(str, "http-path") == 0) {
if(found) {
free(http_path);
http_path = dup_str(str + i);
}
}
free(str);
str = malloc(1);
str[0] = 0;
} else if(bf[0] != '\r') {
char* tmp = str;
str = concat_str(tmp, bf);
free(tmp);
}
}
free(str);
free(bf);
fclose(f);
return 0;
}
int typearr[1024];
int typearr2[1024];
int depth;
int depth2;
enum TYPES { ELEM_NONE, ELEM_METADATA, ELEM_TITLE, ELEM_PAGE, ELEM_ICON, ELEM_SHORTDESC };
char* page_title;
char* page_data;
char* page_icon;
char* page_shortdesc;
bool page_index;
bool page_do_not_index;
char* page_title2;
char* page_data2;
char* page_icon2;
char* page_shortdesc2;
bool page_index2;
bool page_do_not_index2;
static void XMLCALL xml_startelem(void* user_data, const XML_Char* el, const XML_Char* attr[]) {
typearr[depth] = ELEM_NONE;
if(strcmp(el, "metadata") == 0) typearr[depth] = ELEM_METADATA;
if(strcmp(el, "title") == 0) typearr[depth] = ELEM_TITLE;
if(strcmp(el, "page") == 0) typearr[depth] = ELEM_PAGE;
if(strcmp(el, "icon") == 0) typearr[depth] = ELEM_ICON;
if(strcmp(el, "short-description") == 0) typearr[depth] = ELEM_SHORTDESC;
if(strcmp(el, "index") == 0) page_index = true;
if(strcmp(el, "do-not-index") == 0) page_do_not_index = true;
if(depth == 0 && typearr[depth] != ELEM_METADATA) {
fprintf(stderr, "toplevel must be metadata\n");
exit(1);
}
depth++;
}
static void XMLCALL xml_startelem2(void* user_data, const XML_Char* el, const XML_Char* attr[]) {
typearr2[depth2] = ELEM_NONE;
if(strcmp(el, "metadata") == 0) typearr2[depth2] = ELEM_METADATA;
if(strcmp(el, "title") == 0) typearr2[depth2] = ELEM_TITLE;
if(strcmp(el, "page") == 0) typearr2[depth2] = ELEM_PAGE;
if(strcmp(el, "icon") == 0) typearr2[depth2] = ELEM_ICON;
if(strcmp(el, "index") == 0) page_index2 = true;
if(strcmp(el, "do-not-index") == 0) page_do_not_index2 = true;
if(strcmp(el, "short-description") == 0) typearr2[depth2] = ELEM_SHORTDESC;
if(depth2 == 0 && typearr2[depth2] != ELEM_METADATA) {
fprintf(stderr, "toplevel must be metadata\n");
exit(1);
}
depth2++;
}
static void XMLCALL xml_endelem(void* user_data, const XML_Char* el) { depth--; }
static void XMLCALL xml_endelem2(void* user_data, const XML_Char* el) { depth2--; }
static void XMLCALL xml_elemdata(void* user_data, const XML_Char* data, int data_size) {
if(data_size > 0) {
char* str = malloc(data_size + 1);
memcpy(str, data, data_size);
str[data_size] = 0;
if(typearr[depth - 1] == ELEM_TITLE) {
if(page_title != NULL) {
char* tmp = page_title;
page_title = concat_str(tmp, str);
free(tmp);
} else {
page_title = dup_str(str);
}
} else if(typearr[depth - 1] == ELEM_PAGE) {
if(page_data != NULL) {
char* tmp = page_data;
page_data = concat_str(tmp, str);
free(tmp);
} else {
page_data = dup_str(str);
}
} else if(typearr[depth - 1] == ELEM_ICON) {
if(page_icon != NULL) {
char* tmp = page_icon;
page_icon = concat_str(tmp, str);
free(tmp);
} else {
page_icon = dup_str(str);
}
} else if(typearr[depth - 1] == ELEM_SHORTDESC) {
if(page_shortdesc != NULL) {
char* tmp = page_shortdesc;
page_shortdesc = concat_str(tmp, str);
free(tmp);
} else {
page_shortdesc = dup_str(str);
}
}
free(str);
}
}
static void XMLCALL xml_elemdata2(void* user_data, const XML_Char* data, int data_size) {
if(data_size > 0) {
char* str = malloc(data_size + 1);
memcpy(str, data, data_size);
str[data_size] = 0;
if(typearr2[depth2 - 1] == ELEM_TITLE) {
if(page_title2 != NULL) {
char* tmp = page_title2;
page_title2 = concat_str(tmp, str);
free(tmp);
} else {
page_title2 = dup_str(str);
}
} else if(typearr2[depth2 - 1] == ELEM_PAGE) {
if(page_data2 != NULL) {
char* tmp = page_data2;
page_data2 = concat_str(tmp, str);
free(tmp);
} else {
page_data2 = dup_str(str);
}
} else if(typearr2[depth2 - 1] == ELEM_ICON) {
if(page_icon2 != NULL) {
char* tmp = page_icon2;
page_icon2 = concat_str(tmp, str);
free(tmp);
} else {
page_icon2 = dup_str(str);
}
} else if(typearr2[depth2 - 1] == ELEM_SHORTDESC) {
if(page_shortdesc2 != NULL) {
char* tmp = page_shortdesc2;
page_shortdesc2 = concat_str(tmp, str);
free(tmp);
} else {
page_shortdesc2 = dup_str(str);
}
}
free(str);
}
}
void scan_and_print(FILE* f, const char* path, const char* http){
DIR* dir = opendir(path);
if(dir != NULL){
struct dirent* d;
while((d = readdir(dir)) != NULL){
if(strcmp(d->d_name, ".") == 0 || strcmp(d->d_name, "..") == 0) continue;
char* p = concat3_str(path, "/", d->d_name);
struct stat s;
if(stat(p, &s) == 0){
if(S_ISDIR(s.st_mode)){
char* new_http = concat3_str(http, d->d_name, "/");
scan_and_print(f, p, new_http);
free(new_http);
}else if(strcmp(d->d_name, "metadata.xml") == 0){
page_title2 = NULL;
page_data2 = NULL;
page_icon2 = NULL;
page_shortdesc2 = NULL;
page_do_not_index2 = false;
FILE* fp = fopen(p, "r");
XML_Parser sparser = XML_ParserCreate(NULL);
XML_SetElementHandler(sparser, xml_startelem2, xml_endelem2);
XML_SetCharacterDataHandler(sparser, xml_elemdata2);
if(fp != NULL){
char* buf = malloc(1024);
int done;
do{
int len = fread(buf, 1, 1024, fp);
done = len < 1024;
if(XML_Parse(sparser, buf, len, done) == XML_STATUS_ERROR){
break;
}
}while(!done);
free(buf);
fclose(fp);
}
XML_ParserFree(sparser);
if(!page_do_not_index2){
fprintf(f, "<dt><a href=\"%s/%s\">", http_path, http);
if(page_icon2 != NULL) fprintf(f, "<img src=\"%s/static/%s\" alt=\"%s\">", http_path, page_icon2, page_title2);
if(page_icon2 != NULL) fprintf(f, "</a> ");
if(page_title2 != NULL) fprintf(f, "<b>%s </b>", page_title2);
if(page_icon2 == NULL) fprintf(f, "</a>");
fprintf(f, "</dt>\n");
if(page_shortdesc2 != NULL){
fprintf(f, "<dd>%s</dd>\n", page_shortdesc2);
}
}
if(page_title2 != NULL) free(page_title2);
if(page_data2 != NULL) free(page_data2);
if(page_icon2 != NULL) free(page_icon2);
if(page_shortdesc2 != NULL) free(page_shortdesc2);
}
}
free(p);
}
closedir(dir);
}
}
int parse_xml(const char* xml, const char* outdir) {
printf("%s...\n", xml);
depth = 0;
page_title = NULL;
page_data = NULL;
page_icon = NULL;
page_index = false;
page_shortdesc = NULL;
XML_Parser parser = XML_ParserCreate(NULL);
XML_SetElementHandler(parser, xml_startelem, xml_endelem);
XML_SetCharacterDataHandler(parser, xml_elemdata);
FILE* f = fopen(xml, "r");
char* buffer = malloc(1024);
int done = 0;
do {
int len = fread(buffer, 1, 1024, f);
done = len < 1024;
if(XML_Parse(parser, buffer, len, done) == XML_STATUS_ERROR) {
fprintf(stderr, "XML parse error\n");
XML_ParserFree(parser);
fclose(f);
free(buffer);
return 1;
}
} while(!done);
fclose(f);
free(buffer);
XML_ParserFree(parser);
char* html = concat_str(outdir, "/index.html");
f = fopen(html, "w");
if(f != NULL) {
char* lastmod = malloc(513);
struct stat s;
stat(xml, &s);
struct tm* tm = localtime(&s.st_mtime);
strftime(lastmod, 512, "%a %b %e %H:%M:%S %Z %Y", tm);
fprintf(f, "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n");
fprintf(f, "<html>\n");
fprintf(f, " <head>\n");
fprintf(f, " <meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n");
if(page_title != NULL) {
fprintf(f, " <title>%s - %s</title>\n", title, page_title);
} else {
fprintf(f, " <title>%s</title>\n", title);
}
fprintf(f, " <link rel=\"icon\" href=\"%s/static/%s\">\n", http_path, icon);
fprintf(f, " </head>\n");
fprintf(f, " <body>\n");
fprintf(f, " <a href=\"%s\"><img src=\"%s/static/%s\" alt=\"website icon\"></a>\n", http_path, http_path, icon);
if(page_icon != NULL){
char* from_path = concat3_str(static_dir, "/", page_icon);
char* to_path = concat3_str(output_dir, "/static/", page_icon);
FILE* from = fopen(from_path, "rb");
FILE* to = fopen(to_path, "wb");
if(from != NULL){
char* buf = malloc(1024);
int done = 0;
do{
int len = fread(buf, 1, 1024, from);
fwrite(buf, 1, len, to);
done = len < 1024;
}while(!done);
free(buf);
fclose(from);
}
fclose(to);
free(from_path);
free(to_path);
fprintf(f, " <img src=\"%s/static/%s\" alt=\"page icon\">\n", http_path, page_icon);
}
if(page_title != NULL) {
fprintf(f, " <h1>%s - %s</h1>\n", title, page_title);
} else {
fprintf(f, " <h1>%s</h1>\n", title);
}
if(page_data != NULL) {
fprintf(f, "%s\n", page_data);
}
if(page_index){
fprintf(f, " <hr>\n");
fprintf(f, " <dl>\n");
scan_and_print(f, input_dir, "");
fprintf(f, " </dl>\n");
}
fprintf(f, " <hr>\n");
fprintf(f, " <i>Last modified: %s</i><br>\n", lastmod);
fprintf(f, " <i>Generated by <a href=\"http://nishi.boats/clover\">Clover</a> %s</i>\n", CLOVER_VERSION);
fprintf(f, " </body>\n");
fprintf(f, "</html>\n");
fclose(f);
free(lastmod);
}
free(html);
if(page_title != NULL) free(page_title);
if(page_data != NULL) free(page_data);
if(page_icon != NULL) free(page_icon);
if(page_shortdesc != NULL) free(page_shortdesc);
return 0;
}
void generate(const char* input, const char* output) {
mkdir(output, 0775);
DIR* dir = opendir(input);
if(dir != NULL) {
struct dirent* d;
while((d = readdir(dir)) != NULL) {
if(strcmp(d->d_name, ".") == 0 || strcmp(d->d_name, "..") == 0) continue;
char* p = concat3_str(input, "/", d->d_name);
struct stat s;
if(stat(p, &s) == 0) {
if(S_ISDIR(s.st_mode)) {
char* op = concat3_str(output, "/", d->d_name);
generate(p, op);
free(op);
} else if(strcmp(d->d_name, "metadata.xml") == 0) {
char* xml = concat3_str(input, "/", d->d_name);
if(parse_xml(xml, output) == 1) {
free(xml);
free(p);
break;
}
free(xml);
}
}
free(p);
}
closedir(dir);
}
}
int main(int argc, char** argv) {
output_dir = dup_str("webroot");
input_dir = dup_str("input");
title = dup_str("Unnamed");
icon = dup_str("icon.png");
static_dir = dup_str("static");
http_path = dup_str("/");
int r;
if(argv[1] == NULL) {
r = parse_config("clover.conf");
} else {
r = parse_config(argv[1]);
}
printf("Clover %s Copyright (c) 2024 Nishi\n", CLOVER_VERSION);
printf("Using Expat %d.%d.%d\n", XML_MAJOR_VERSION, XML_MINOR_VERSION, XML_MICRO_VERSION);
printf(" Icon: %s\n", icon);
printf(" Title: %s\n", title);
printf(" Input: %s\n", input_dir);
printf(" Output: %s\n", output_dir);
printf(" HTTP Path: %s\n", http_path);
printf("Static Path: %s\n", static_dir);
char* staticp = concat_str(output_dir, "/static");
mkdir(staticp, 0775);
char* toicon = concat3_str(staticp, "/", icon);
FILE* ficon = fopen(toicon, "wb");
char* iconpath = concat3_str(static_dir, "/", icon);
FILE* from = fopen(iconpath, "rb");
if(from != NULL){
char* buf = malloc(1024);
int done = 0;
do{
int len = fread(buf, 1, 1024, from);
fwrite(buf, 1, len, ficon);
done = len < 1024;
}while(!done);
free(buf);
fclose(from);
}
fclose(ficon);
free(toicon);
free(staticp);
free(iconpath);
generate(input_dir, output_dir);
if(r != 0) return r;
}