Skip to content

Instantly share code, notes, and snippets.

@rlapz
Last active January 8, 2024 13:42
Show Gist options
  • Save rlapz/afb549b6792f655da08e444a4cebbc1e to your computer and use it in GitHub Desktop.
Save rlapz/afb549b6792f655da08e444a4cebbc1e to your computer and use it in GitHub Desktop.
A simple static site generator for blog
/*
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>
*/
#define _GNU_SOURCE
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <error.h>
#include <fcntl.h>
#include <libgen.h>
#include <regex.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>
/* Blog tree:
*
* blog/ [root]
* |-> index.html
* |-> style.css
* |-> posts/
* | |-> 0000_00_00_post1.html
* | `-> 0000_00_00_post2.html
* |-> contents/
* | |-> post1.html
* | `-> post2.html
* `-> .contents_tmp/
* `-> post3.html
*/
/* Editable Section -------------------------------------------------- */
#define DIR_POSTS "posts"
#define DIR_CONTENTS "contents"
#define DIR_CONTENTS_TMP ".contents_tmp"
#define STR_INDEX_TITLE "Blog"
#define STR_INDEX_SUBTITLE "Berusaha Menulis Blog"
#define STR_INDEX_TITLE_HEAD "My Personal Blog"
#define FILE_INDEX "index.html"
#define FILE_STYLE "style.css"
/* ------------------------------------------------------------------- */
#define SIZE_BUFFER (1024 * 1024)
#define SIZE_STR_INIT (1024 * 512)
#define SIZE_FILENAME (256)
#define SIZE_ERROR_CONTEXT (32)
#define SIZE_ERROR_MESSAGE (64)
#define SIZE_POST_DATE (sizeof("2000/01/01"))
#define SIZE_POST_DATE_LONG (sizeof("Wednesday, 01 September 2023"))
#define SIZE_POST_TITLE (64)
#define SIZE_POST_CONTENTS (8192)
#define SIZE_POST_FILENAME_MIN (sizeof("2000_01_01_x.html"))
#define SIZE_POST_FILENAME_MAX (((SIZE_POST_TITLE - 1) + (SIZE_POST_FILENAME_MIN)))
#define REGEX_POST_DATE\
"^\\(" \
"[0-9]\\{4\\}/" \
"[0-9]\\{2\\}/" \
"[0-9]\\{2\\}" \
"\\)$"
#define REGEX_POST_FILENAME\
"^\\(" \
"[0-9]\\{4\\}_" \
"[0-9]\\{2\\}_" \
"[0-9]\\{2\\}_" \
"[_a-z0-9]\\{1,63\\}" \
"\\)" \
"\\." \
"\\(html\\)$"
#define HTML_STYLE\
"html {\n" \
" background-color: #fff;\n" \
" font-family: Sans-Serif;\n" \
"}\n\n" \
"body {\n" \
" max-width: 800px;\n" \
" padding: 10px;\n" \
" margin: 0 auto;\n" \
"}\n\n" \
"p {\n" \
" text-align: justify;\n" \
" text-justify: auto;\n" \
" line-height: 1.4;\n" \
"}\n\n" \
"a {\n" \
" text-decoration: none;\n" \
"}\n\n" \
"pre {\n" \
" white-space: pre-wrap;\n" \
"}\n\n" \
"blockquote {\n" \
" background-color: #f5f5f5;\n" \
" padding: 5px;\n" \
" margin: 10px 10px;\n" \
" border-radius: 5px;\n" \
"}\n"
#define HTML_HEAD\
"<!DOCTYPE html>\n" \
"<html lang=\"en\">\n" \
"<head>\n" \
" <meta charset=\"UTF-8\">\n" \
" <meta name=\"viewport\" " \
"content=\"width=device-width, " \
"initial-scale=1.0\">\n" \
" <link rel=\"stylesheet\" " \
"href=\"/%s/" FILE_STYLE "\">\n\n" \
" <title>%s</title>\n" \
"</head>\n" \
"<body>\n" \
" <header>\n" \
" <h1>%s</h1>\n" \
" <sub>%s</sub>\n" \
" </header>\n" \
" <hr>\n\n" \
" <div id=\"contents\">\n"
#define HTML_TAIL\
" </div>\n\n" \
" <hr>\n" \
"</body>\n" \
"</html>\n"
#define HTML_INDEX_LINK\
"\t\t<p>%s - <a href=\"/%s/" DIR_POSTS "/%s\">%s</a></p>\n"
/*===================================================================*/
/* UTILS */
/*===================================================================*/
/*
* cstr
*/
static char *cstr_copy(char dest[], size_t size, const char src[]);
static char *cstr_copy_trim(char dest[], size_t size, const char src[], size_t len);
static char *cstr_replace_all(char dest[], size_t size, int hy, int c, const char src[]);
static char *cstr_title(char dest[]);
/*
* date
*/
static int date_fmt(char buffer[], size_t size, const char date[]);
static int date_now_fmt(char buffer[], size_t size);
/*
* str
*/
struct str {
size_t len;
size_t size;
char *cstr;
};
static int str_init(struct str *self, size_t size);
static void str_deinit(struct str *self);
static void str_reset(struct str *self);
static int str_append(struct str *self, const char cstr[]);
static int str_append_fmt(struct str *self, const char fmt[], ...);
/*
* dir
*/
static int dir_open_create(const char name[]);
static int dir_open_create_at(int parent, const char name[]);
/*
* file
*/
static int file_open_read_all(int dir_fd, const char name[], char buffer[],
size_t *size, int check_size);
static int file_open_write_all(int dir_fd, const char name[], const char buffer[],
size_t size, int flags);
/*
* html
*/
static int html_title_get(const char self[], char dest[], size_t size);
/*
* error
*/
struct error {
int lpad;
char context[SIZE_ERROR_CONTEXT];
char message[SIZE_ERROR_MESSAGE];
};
static void error_init(struct error *self, int lpad, const char fmt[], ...);
static void error_print(struct error *self, int number, const char fmt[], ...);
/*===================================================================*/
/* BLOG */
/*===================================================================*/
/*
* post
*/
struct blog_post {
char title[SIZE_POST_TITLE];
char date[SIZE_POST_DATE];
char contents[SIZE_POST_CONTENTS];
};
static int blog_post_parse(struct blog_post *self, const char html[]);
/*
* blog
*/
struct blog {
int dir_root;
int dir_posts;
int dir_contents;
int dir_contents_tmp;
struct str str;
struct blog_post post;
regex_t regex;
char root_dir_name[SIZE_FILENAME];
char buffer[SIZE_BUFFER];
};
static int blog_init(struct blog *self, const char root_dir[]);
static void blog_deinit(struct blog *self);
static int blog_build_index(struct blog *self);
static int blog_build_posts(struct blog *self);
/*===================================================================*/
/* UTILS: IMPL */
/*===================================================================*/
/*
* cstr
*/
static char *
cstr_copy(char dest[], size_t size, const char src[])
{
size_t i = 0;
const char *const s = src;
char *const d = dest;
for (; (i < size) && (s[i] != '\0'); i++)
d[i] = s[i];
d[i] = '\0';
return d;
}
static char *
cstr_copy_trim(char dest[], size_t size, const char src[], size_t len)
{
const char *s = src;
char *const d = dest;
if (size == 0)
return NULL;
if (len == 0) {
d[0] = '\0';
return d;
}
/* left */
len--;
while ((len > 0) && (*s != '\0') && isspace(*s)) {
s++;
len--;
}
/* right */
while ((len > 0) && (s[len] != '\0')) {
if (!isspace(s[len])) {
len++;
break;
}
len--;
}
if (len >= size)
len -= (len - size);
memcpy(d, s, len);
d[len] = '\0';
return d;
}
static char *
cstr_replace_all(char dest[], size_t size, int hy, int c, const char src[])
{
char *d = dest;
const char *s = src;
size_t i = 0;
while ((i < size) && (s[i] != '\0')) {
if (s[i] == hy)
d[i] = c;
else
d[i] = s[i];
i++;
}
d[i] = '\0';
return d;
}
static char *
cstr_title(char dest[])
{
int first = 1;
char *s = dest;
for (; *s != '\0'; s++) {
if (isspace(*s)) {
first = 1;
continue;
}
if (first == 0) {
*s = tolower(*s);
} else {
*s = toupper(*s);
first = 0;
}
}
return dest;
}
/*
* date
*/
static int
date_fmt(char buffer[], size_t size, const char date[])
{
int yyyy, mm, dd;
struct tm tm = { 0 };
if (sscanf(date, "%d/%d/%d", &yyyy, &mm, &dd) != 3)
return -EINVAL;
if (dd > 31)
return -EINVAL;
if (strptime(date, "%Y/%m/%d", &tm) == NULL)
return -EINVAL;
if (strftime(buffer, size, "%A, %d %B %Y", &tm) == 0)
return -errno;
return 0;
}
static int
date_now_fmt(char buffer[], size_t size)
{
const time_t now = time(NULL);
if (now < 0)
return -errno;
if (strftime(buffer, size, "%Y/%m/%d", localtime(&now)) == 0)
return -EINVAL;
return 0;
}
/*
* str
*/
static int
str_init(struct str *self, size_t size)
{
if (size == 0)
return -EINVAL;
char *const cstr = malloc(size);
if (cstr == NULL)
return -errno;
cstr[0] = '\0';
self->len = 0;
self->size = size;
self->cstr = cstr;
return 0;
}
static void
str_deinit(struct str *self)
{
self->len = 0;
self->size = 0;
free(self->cstr);
}
static void
str_reset(struct str *self)
{
self->len = 0;
self->cstr[0] = '\0';
}
static int
__str_resize(struct str *self, size_t src_len)
{
const size_t len = self->len;
const size_t size = self->size;
size_t remn_size = 0;
if (size > len)
remn_size = size - len;
if (src_len >= remn_size) {
const size_t _req_size = (src_len - remn_size) + size + 1;
char *const new_cstr = realloc(self->cstr, _req_size);
if (new_cstr == NULL)
return -errno;
self->size = _req_size;
self->cstr = new_cstr;
}
return 0;
}
static int
str_append(struct str *self, const char cstr[])
{
size_t len = self->len;
const size_t cstr_len = strlen(cstr);
if (cstr_len == 0)
return 0;
const int ret = __str_resize(self, cstr_len);
if (ret < 0)
return ret;
memcpy(self->cstr + len, cstr, cstr_len);
len += cstr_len;
self->len = len;
self->cstr[len] = '\0';
return 0;
}
static int
str_append_fmt(struct str *self, const char fmt[], ...)
{
int ret;
size_t len = self->len;
va_list va;
/* Determine required size. */
va_start(va, fmt);
ret = vsnprintf(NULL, 0, fmt, va);
va_end(va);
if (ret < 0)
return -errno;
const size_t cstr_len = (size_t)ret;
if (cstr_len == 0)
return 0;
if ((ret = __str_resize(self, cstr_len)) < 0)
return ret;
va_start(va, fmt);
ret = vsnprintf(self->cstr + len, cstr_len + 1, fmt, va);
va_end(va);
if (ret < 0)
return ret;
len += (size_t)ret;
self->len = len;
self->cstr[len] = '\0';
return 0;
}
/*
* dir
*/
static int
dir_open_create(const char name[])
{
int ret = 0;
while (1) {
ret = open(name, O_DIRECTORY, 0);
if (ret > 0)
break;
if (errno == ENOENT) {
ret = mkdir(name, 0755);
if (ret == 0)
continue;
}
ret = -errno;
break;
}
return ret;
}
static int
dir_open_create_at(int parent, const char name[])
{
int ret = 0;
while (1) {
ret = openat(parent, name, O_DIRECTORY, 0);
if (ret > 0)
break;
if (errno == ENOENT) {
ret = mkdirat(parent, name, 0755);
if (ret == 0)
continue;
}
ret = -errno;
break;
}
return ret;
}
/*
* file
*/
static int
file_open_read_all(int dir_fd, const char name[], char buffer[], size_t *size,
int check_size)
{
int ret = 0, fd;
size_t rd = 0;
size_t _size = *size;
if ((fd = openat(dir_fd, name, O_RDONLY, 0)) < 0)
return -errno;
if (check_size != 0) {
struct stat _stat;
if ((ret = fstat(fd, &_stat)) < 0) {
ret = -errno;
goto out0;
}
if ((size_t)_stat.st_size >= _size) {
ret = -ENOMEM;
rd = (size_t)_stat.st_size - _size;
goto out1;
}
}
while (rd < _size) {
const ssize_t r = read(fd, buffer + rd, _size - rd);
if (r < 0) {
ret = -errno;
break;
}
if (r == 0)
break;
rd += (size_t)r;
}
buffer[rd - 1] = '\0';
out1:
(*size) = rd;
out0:
close(fd);
return ret;
}
static int
file_open_write_all(int dir_fd, const char name[], const char buffer[], size_t len,
int flags)
{
int ret = 0, fd;
size_t wr = 0;
if ((fd = openat(dir_fd, name, flags, 0644)) < 0)
return -errno;
while (wr < len) {
const ssize_t w = write(fd, buffer + wr, len - wr);
if (w < 0) {
ret = -errno;
break;
}
if (w == 0)
break;
wr += (size_t)w;
}
close(fd);
return ret;
}
/*
* html
*/
static int
html_title_get(const char self[], char dest[], size_t size)
{
const char *const head_start = strstr(self, "<head>");
if (head_start == NULL)
return -EINVAL;
const char *const head_end = strstr(head_start, "</head>");
if (head_end == NULL)
return -EINVAL;
const char *title_start = strstr(head_start, "<title>");
if (title_start == NULL)
return -EINVAL;
const char *const title_end = strstr(title_start, "</title>");
if ((title_end == NULL) || (title_end >= head_end))
return -EINVAL;
// + strlen("<title>")
title_start += 7;
cstr_copy_trim(dest, size, title_start, (title_end - title_start));
return 0;
}
/*
* error
*/
static void
error_init(struct error *self, int lpad, const char fmt[], ...)
{
int ret;
va_list va;
const size_t size = sizeof(self->context);
va_start(va, fmt);
ret = vsnprintf(self->context, size, fmt, va);
va_end(va);
if (ret < 0)
self->context[0] = '\0';
if ((size_t)ret >= size) {
cstr_copy(&self->context[size - 4], 3, "...");
self->context[size - 1] = '\0';
}
self->message[0] = '\0';
self->lpad = lpad;
}
static void
error_print(struct error *self, int number, const char fmt[], ...)
{
int ret;
va_list va;
const size_t size = sizeof(self->message);
va_start(va, fmt);
ret = vsnprintf(self->message, size, fmt, va);
va_end(va);
if (ret < 0)
self->message[0] = '\0';
if ((size_t)ret >= size) {
cstr_copy(&self->message[size - 4], 3, "...");
self->message[size - 1] = '\0';
}
if (number == 0) {
fprintf(stderr, "%*s: %s: %s\n", self->lpad, "Error", self->context,
self->message);
} else {
fprintf(stderr, "%*s: %s: %s: %s\n", self->lpad, "Error",
self->context, self->message, strerror(abs(number)));
}
}
/*===================================================================*/
/* BLOG: IMPL */
/*===================================================================*/
/*
* post
*/
static int
blog_post_parse(struct blog_post *self, const char html[])
{
const char *title_end = strchr(html, '\n');
if (title_end == NULL)
return -EINVAL;
const char *const date_end = strstr(title_end, "\n\n");
if (date_end == NULL)
return -EINVAL;
/* title */
size_t len = title_end - html;
cstr_copy_trim(self->title, SIZE_POST_TITLE, html, len);
cstr_title(self->title);
/* date */
title_end++;
len = 0;
if (date_end > title_end)
len = date_end - title_end;
cstr_copy_trim(self->date, SIZE_POST_DATE, title_end, len);
/* contents */
cstr_copy(self->contents, SIZE_POST_CONTENTS, date_end + 2);
return 0;
}
/*
* blog
*/
static int
blog_init(struct blog *self, const char root_dir[])
{
int ret, dir_root, dir_posts, dir_contents, dir_contents_tmp;
struct error error;
error_init(&error, 0, "blog_init");
if (SIZE_BUFFER <= sizeof(struct blog_post)) {
error_print(&error, 0, "no enough memory: buffer too small");
return -1;
}
ret = str_init(&self->str, SIZE_STR_INIT);
if (ret < 0) {
error_print(&error, ret, "str_init");
return -1;
}
/* /root */
dir_root = dir_open_create(root_dir);
if (dir_root < 0) {
error_print(&error, dir_root, "open: \"%s\"", root_dir);
goto err0;
}
/* /root/posts */
dir_posts = dir_open_create_at(dir_root, DIR_POSTS);
if (dir_posts < 0) {
error_print(&error, dir_posts, "open: \"%s/%s\"", root_dir, DIR_POSTS);
goto err1;
}
/* /root/contents */
dir_contents = dir_open_create_at(dir_root, DIR_CONTENTS);
if (dir_contents < 0) {
error_print(&error, dir_contents, "open: \"%s/%s\"", root_dir, DIR_CONTENTS);
goto err2;
}
/* /root/.contents_tmp */
dir_contents_tmp = dir_open_create_at(dir_root, DIR_CONTENTS_TMP);
if (dir_contents_tmp < 0) {
error_print(&error, dir_contents_tmp, "open: \"%s/%s\"", root_dir,
DIR_CONTENTS_TMP);
goto err3;
}
/* /root/style.css */
if (faccessat(dir_root, FILE_STYLE, F_OK, AT_EACCESS) < 0) {
if (errno != ENOENT) {
error_print(&error, errno, "check: \"%s/%s\"", root_dir, FILE_STYLE);
goto err4;
}
const int _ret = file_open_write_all(dir_root, FILE_STYLE, HTML_STYLE,
sizeof(HTML_STYLE) - 1,
O_CREAT | O_WRONLY);
if (_ret < 0) {
error_print(&error, errno, "write: \"%s/%s\"", root_dir, FILE_STYLE);
goto err4;
}
}
/* get cwd name */
if ((strlen(root_dir) == 1) && (root_dir[0] == '.')) {
char *const _name = getcwd(self->buffer, sizeof(self->buffer));
if (_name == NULL) {
if (errno == ERANGE)
error_print(&error, 0, "getcwd: not enough memory");
else
error_print(&error, errno, "getcwd: %s", root_dir);
goto err4;
}
root_dir = basename(_name);
}
if (strlen(root_dir) >= sizeof(self->root_dir_name)) {
error_print(&error, ret, "root_dir: no enough memory");
goto err4;
}
cstr_copy(self->root_dir_name, sizeof(self->root_dir_name), root_dir);
self->dir_root = dir_root;
self->dir_posts = dir_posts;
self->dir_contents = dir_contents;
self->dir_contents_tmp = dir_contents_tmp;
return 0;
err4:
close(dir_contents_tmp);
err3:
close(dir_contents);
err2:
close(dir_posts);
err1:
close(dir_root);
err0:
str_deinit(&self->str);
return -1;
}
static void
blog_deinit(struct blog *self)
{
close(self->dir_contents_tmp);
close(self->dir_contents);
close(self->dir_posts);
close(self->dir_root);
str_deinit(&self->str);
}
static void
__blog_build_index_post_read(struct blog *self, const char name[])
{
int ret;
char *buffer = self->buffer;
size_t buffer_size = sizeof(self->buffer);
size_t read_size = sizeof(HTML_HEAD) + SIZE_POST_TITLE;
struct str *const str = &self->str;
struct error error;
error_init(&error, 9, "add post: \"%s\"", name);
if (read_size >= buffer_size) {
error_print(&error, 0, "read: not enough memory");
return;
}
/* get post title */
ret = file_open_read_all(self->dir_posts, name, buffer, &read_size, 0);
if (ret < 0) {
error_print(&error, ret, "read");
return;
}
char *const title = buffer + read_size;
const size_t title_size = buffer_size - read_size;
if (title_size <= 1) {
error_print(&error, 0, "read: not enough memory");
return;
}
ret = html_title_get(buffer, title, title_size);
if (ret < 0) {
error_print(&error, 0, "no title");
return;
}
const char *const date = cstr_replace_all(buffer, SIZE_POST_DATE - 1, '_', '/', name);
ret = str_append_fmt(str, HTML_INDEX_LINK, date, self->root_dir_name, name, title);
if (ret < 0)
error_print(&error, 0, "add: post link");
}
static int
__blog_build_index_posts_add(struct blog *self)
{
int len;
const regex_t *const regex = &self->regex;
struct dirent **dirent;
struct error error;
len = scandirat(self->dir_posts, ".", &dirent, NULL, alphasort);
if (len < 0) {
error_print(&error, errno, "scan dir: %s/%s", self->root_dir_name, DIR_POSTS);
return -1;
}
while (len--) {
const char *const _name = dirent[len]->d_name;
if (_name[0] != '.') {
if (regexec(regex, _name, 0, NULL, 0) == 0) {
printf(" -> post: add: \"%s\"\n", _name);
__blog_build_index_post_read(self, _name);
} else {
printf(" -> post: ignore: \"%s\"\n", _name);
}
}
free(dirent[len]);
}
free(dirent);
return 0;
}
static int
blog_build_index(struct blog *self)
{
int ret;
struct str *const str = &self->str;
const char *const root = self->root_dir_name;
struct error error;
error_init(&error, 0, "blog_build_index");
ret = regcomp(&self->regex, REGEX_POST_FILENAME, 0);
if (ret != 0) {
regerror(ret, &self->regex, self->buffer, sizeof(self->buffer));
error_print(&error, 0, "regcomp: %s", self->buffer);
return -1;
}
str_reset(str);
/* add HTML HEAD */
ret = str_append_fmt(str, HTML_HEAD, root, STR_INDEX_TITLE_HEAD, STR_INDEX_TITLE,
STR_INDEX_SUBTITLE);
if (ret < 0) {
error_print(&error, ret, "add: HTML HEAD");
goto out0;
}
/* add HTML CONTENTS */
ret = __blog_build_index_posts_add(self);
if (ret < 0)
goto out0;
/* add HTML TAIL */
ret = str_append(str, HTML_TAIL);
if (ret < 0) {
error_print(&error, ret, "add: HTML TAIL");
goto out0;
}
/* write all */
ret = file_open_write_all(self->dir_root, FILE_INDEX, str->cstr, str->len,
O_CREAT | O_WRONLY | O_TRUNC);
if (ret < 0)
error_print(&error, ret, "write: HTML");
out0:
regfree(&self->regex);
return ret;
}
static int
__blog_post_filename_from(char buffer[], size_t size, const char date[],
const char title[])
{
char *buf;
const char *p = title;
size_t i = 0;
buf = cstr_replace_all(buffer, SIZE_POST_DATE, '/', '_', date);
buf += (SIZE_POST_DATE - 1);
*(buf++) = '_';
size -= (SIZE_POST_DATE - 1);
while ((*p != '\0') && (i < size)) {
int c = *p;
if (isspace(c))
c = '_';
if (isalnum(c) || (c == '_'))
buf[i++] = tolower(c);
p++;
}
size -= i;
if (size <= 6)
return -ENOMEM;
cstr_copy(buf + i, size, ".html");
return 0;
}
static int
__blog_build_posts_content_build(struct blog *self, const char name[])
{
int ret;
struct str *const str = &self->str;
struct blog_post *const post = &self->post;
char date[SIZE_POST_DATE_LONG];
struct error error;
error_init(&error, 9, "build content: \"%s\"", name);
ret = date_fmt(date, sizeof(date), post->date);
if (ret < 0) {
error_print(&error, ret, "invalid date format");
return -1;
}
/* add HTML HEAD */
ret = str_append_fmt(str, HTML_HEAD, self->root_dir_name, post->title,
post->title, date);
if (ret < 0) {
error_print(&error, ret, "add: HTML HEAD");
return -1;
}
/* add HTML CONTENTS */
if (str_append_fmt(str, "%s\n", post->contents) < 0) {
error_print(&error, 0, "add: HTML CONTENTS");
return -1;
}
/* add HTML TAIL */
if (str_append(str, HTML_TAIL) < 0) {
error_print(&error, 0, "add: HTML TAIL");
return -1;
}
return 0;
}
static int
__blog_build_posts_content_parse(struct blog *self, const char content[], const char name[])
{
int ret;
struct blog_post *const post = &self->post;
struct error error;
error_init(&error, 9, "parse content: \"%s\"", name);
if (blog_post_parse(post, content) < 0) {
error_print(&error, 0, "invalid content file");
return -1;
}
if (post->title[0] == '\0') {
error_print(&error, 0, "empty title");
return -1;
}
if (post->contents[0] == '\0') {
error_print(&error, 0, "empty content");
return -1;
}
if (post->date[0] == '\0') {
ret = date_now_fmt(post->date, sizeof(post->date));
if (ret < 0) {
error_print(&error, ret, "failed to get current date");
return -1;
}
}
if (regexec(&self->regex, post->date, 0, NULL, 0) != 0) {
error_print(&error, 0, "invalid date format");
return -1;
}
return 0;
}
static void
__blog_build_posts_content_add(struct blog *self, const char name[])
{
int ret;
char *const buffer = self->buffer;
size_t buffer_size = sizeof(self->buffer);
char trg_name[SIZE_POST_FILENAME_MAX];
struct blog_post *const post = &self->post;
struct error error;
error_init(&error, 9, "add content: \"%s\"", name);
ret = file_open_read_all(self->dir_contents, name, buffer, &buffer_size, 1);
if (ret < 0) {
if (ret == -ENOMEM)
error_print(&error, 0, "read: not enough memory: "
"need %zu bytes more", buffer_size);
else
error_print(&error, ret, "read");
return;
}
if (buffer_size == 0) {
error_print(&error, 0, "empty file");
return;
}
/* parse the content */
if (__blog_build_posts_content_parse(self, buffer, name) < 0)
return;
/* build the content */
if (__blog_build_posts_content_build(self, name) < 0)
return;
/* create target file name */
ret = __blog_post_filename_from(trg_name, sizeof(trg_name), post->date, post->title);
if (ret < 0) {
error_print(&error, ret, "filename: %s", trg_name);
return;
}
/* write content */
ret = file_open_write_all(self->dir_posts, trg_name, self->str.cstr, self->str.len,
O_TRUNC | O_CREAT | O_WRONLY);
if (ret < 0) {
error_print(&error, ret, "write: %s", trg_name);
return;
}
/* move/rename content file */
if (renameat(self->dir_contents, name, self->dir_contents_tmp, name) < 0)
error_print(&error, errno, "move into %s", DIR_CONTENTS_TMP);
}
static int
blog_build_posts(struct blog *self)
{
int ret, len;
struct str *const str = &self->str;
struct dirent **dirent;
struct error error;
error_init(&error, 0, "blog_build_posts");
ret = regcomp(&self->regex, REGEX_POST_DATE, 0);
if (ret != 0) {
regerror(ret, &self->regex, self->buffer, sizeof(self->buffer));
error_print(&error, 0, "regcomp: %s", self->buffer);
return -1;
}
len = scandirat(self->dir_contents, ".", &dirent, NULL, alphasort);
if (len < 0) {
error_print(&error, errno, "scan dir: %s/%s", self->root_dir_name,
DIR_CONTENTS);
ret = -1;
goto err0;
}
while (len--) {
const char *const _name = dirent[len]->d_name;
if (_name[0] != '.') {
str_reset(str);
printf(" -> content: add: \"%s\"\n", _name);
__blog_build_posts_content_add(self, _name);
}
free(dirent[len]);
}
free(dirent);
err0:
regfree(&self->regex);
return ret;
}
/*===================================================================*/
/* ENTRY POINT */
/*===================================================================*/
int
main(int argc, char *argv[])
{
int ret;
struct blog blog;
const char *cwd = ".";
if (argc > 1)
cwd = argv[1];
if ((ret = blog_init(&blog, cwd)) < 0)
goto out0;
printf("root dir: \"%s\"\n", blog.root_dir_name);
puts("\nBuilding posts...");
if ((ret = blog_build_posts(&blog)) < 0)
goto out1;
puts("\nBuilding index file...");
if ((ret = blog_build_index(&blog)) < 0)
goto out1;
puts("\nDone.");
out1:
blog_deinit(&blog);
out0:
return -ret;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment