/* See LICENSE file for copyright and license details.
*
* gen - static page generator for krisyotam.net
*
* Reads MDX files (YAML frontmatter + markdown) from the content repo,
* strips JSX components, renders a small markdown subset to plain HTML
* and writes one page per entry plus one listing page per type.
*
* cc -std=c99 -Os -Wall -Wextra -pedantic -o gen gen.c
* ./gen
*/
#define _DEFAULT_SOURCE
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include "config.h"
#define LEN(x) (sizeof(x) / sizeof((x)[0]))
#define NTYPES LEN(types)
#define MAXENTRIES 4096
typedef struct {
char type[32];
char slug[128];
char title[256];
char date[16];
char category[64];
char status[32];
char confidence[32];
char importance[16];
char preview[1024];
char tags[256];
long words;
char *body;
} Entry;
/* page layout families, chosen per type */
enum layout { LPLAIN, LSTAT, LACADEMIC, LWINDOW, LCHAPBOOK };
static enum layout
layoutof(const char *type)
{
if (!strcmp(type, "blog") || !strcmp(type, "news") ||
!strcmp(type, "diary"))
return LSTAT;
if (!strcmp(type, "papers") || !strcmp(type, "essays"))
return LACADEMIC;
if (!strcmp(type, "reviews") || !strcmp(type, "ocs"))
return LWINDOW;
if (!strcmp(type, "verse") || !strcmp(type, "prayers"))
return LCHAPBOOK;
return LPLAIN;
}
static long
wordcount(const char *s)
{
long n = 0;
int in = 0;
for (; *s; s++) {
if (isspace((unsigned char)*s)) {
in = 0;
} else if (!in) {
in = 1;
n++;
}
}
return n;
}
static Entry entries[MAXENTRIES];
static size_t nentries;
static void
die(const char *msg)
{
perror(msg);
exit(1);
}
static char *
readfile(const char *path)
{
FILE *f;
char *buf;
long n;
if (!(f = fopen(path, "r")))
return NULL;
fseek(f, 0, SEEK_END);
n = ftell(f);
fseek(f, 0, SEEK_SET);
if (!(buf = malloc(n + 1)))
die("malloc");
n = fread(buf, 1, n, f);
buf[n] = '\0';
fclose(f);
return buf;
}
/* write s to f, escaping &, <, > */
static void
esc(FILE *f, const char *s, size_t n)
{
size_t i;
for (i = 0; i < n && s[i]; i++) {
switch (s[i]) {
case '&': fputs("&", f); break;
case '<': fputs("<", f); break;
case '>': fputs(">", f); break;
default: fputc(s[i], f);
}
}
}
static void
escs(FILE *f, const char *s)
{
esc(f, s, strlen(s));
}
/* ------------------------------------------------------------ frontmatter */
/* copy a yaml scalar value, dropping surrounding quotes */
static void
yamlval(char *dst, size_t dstsz, const char *src)
{
size_t n;
while (*src == ' ')
src++;
n = strlen(src);
while (n && (src[n-1] == '\n' || src[n-1] == ' ' || src[n-1] == '\r'))
n--;
if (n >= 2 && (*src == '\'' || *src == '"') && src[n-1] == *src) {
src++;
n -= 2;
}
if (n >= dstsz)
n = dstsz - 1;
memcpy(dst, src, n);
dst[n] = '\0';
}
/* "[a, b, c]" -> "a b c" */
static void
yamllist(char *dst, size_t dstsz, const char *src)
{
size_t j = 0;
for (; *src && j < dstsz - 1; src++) {
if (*src == '[' || *src == ']' || *src == '\'' || *src == '"' || *src == '\n')
continue;
if (*src == ',') {
if (j && dst[j-1] != ' ')
dst[j++] = ' ';
continue;
}
if (*src == ' ' && (!j || dst[j-1] == ' '))
continue;
dst[j++] = *src;
}
while (j && dst[j-1] == ' ')
j--;
dst[j] = '\0';
}
/* parse frontmatter in buf, fill e, return pointer to body (in buf) */
static char *
frontmatter(char *buf, Entry *e)
{
char *line, *next, *colon;
if (strncmp(buf, "---\n", 4))
return NULL;
line = buf + 4;
while (line && *line) {
if (!strncmp(line, "---\n", 4))
return line + 4;
if (!strncmp(line, "---", 3) && (line[3] == '\0'))
return line + 3;
next = strchr(line, '\n');
if (next)
*next++ = '\0';
if ((colon = strchr(line, ':')) && line[0] != ' ' && line[0] != '#') {
*colon = '\0';
const char *k = line, *v = colon + 1;
while (*v == ' ')
v++;
if (*v == '>' || *v == '|') {
/* block scalar: consume indented lines */
char *p = next;
size_t j = 0;
char tmp[sizeof(e->preview)];
while (p && *p == ' ') {
char *eol = strchr(p, '\n');
size_t n = eol ? (size_t)(eol - p) : strlen(p);
while (*p == ' ') { p++; n--; }
if (j && j < sizeof(tmp) - 1)
tmp[j++] = ' ';
if (n > sizeof(tmp) - 1 - j)
n = sizeof(tmp) - 1 - j;
memcpy(tmp + j, p, n);
j += n;
p = eol ? eol + 1 : NULL;
}
tmp[j] = '\0';
next = p;
if (!strcmp(k, "preview"))
yamlval(e->preview, sizeof(e->preview), tmp);
} else if (!strcmp(k, "title")) {
yamlval(e->title, sizeof(e->title), v);
} else if (!strcmp(k, "slug")) {
yamlval(e->slug, sizeof(e->slug), v);
} else if (!strcmp(k, "type")) {
yamlval(e->type, sizeof(e->type), v);
} else if (!strcmp(k, "start_date")) {
yamlval(e->date, sizeof(e->date), v);
} else if (!strcmp(k, "category")) {
yamlval(e->category, sizeof(e->category), v);
} else if (!strcmp(k, "status")) {
yamlval(e->status, sizeof(e->status), v);
} else if (!strcmp(k, "confidence")) {
yamlval(e->confidence, sizeof(e->confidence), v);
} else if (!strcmp(k, "importance")) {
yamlval(e->importance, sizeof(e->importance), v);
} else if (!strcmp(k, "preview")) {
yamlval(e->preview, sizeof(e->preview), v);
} else if (!strcmp(k, "tags")) {
yamllist(e->tags, sizeof(e->tags), v);
}
}
line = next;
}
return NULL;
}
/* --------------------------------------------------------- jsx stripping */
/* remove jsx comments, component tags and template-literal wrappers
* in place; children of components are kept as plain text */
static void
stripjsx(char *s)
{
char *r = s, *w = s;
while (*r) {
/* jsx comment: brace slash-star ... star-slash brace */
if (r[0] == '{' && r[1] == '/' && r[2] == '*') {
char *end = strstr(r + 3, "*/}");
if (end) {
r = end + 3;
continue;
}
}
/* {String.raw` , {` , `} */
if (!strncmp(r, "{String.raw`", 12)) {
r += 12;
continue;
}
if (r[0] == '{' && r[1] == '`') {
r += 2;
continue;
}
if (r[0] == '`' && r[1] == '}') {
r += 2;
continue;
}
/* <Component ...> | <Component ... /> | </Component> */
if (r[0] == '<' && (isupper((unsigned char)r[1]) ||
(r[1] == '/' && isupper((unsigned char)r[2])))) {
char *p = r + 1;
char q = 0;
for (; *p; p++) {
if (q) {
if (*p == q)
q = 0;
} else if (*p == '"' || *p == '\'') {
q = *p;
} else if (*p == '>') {
break;
}
}
if (*p == '>') {
r = p + 1;
/* swallow one trailing newline to avoid gaps */
if (*r == '\n')
r++;
continue;
}
}
/* className= -> class= in raw html */
if (!strncmp(r, "className=", 10)) {
memcpy(w, "class=", 6);
w += 6;
r += 10;
continue;
}
*w++ = *r++;
}
*w = '\0';
}
/* ------------------------------------------------------------- markdown */
/* raw inline html tag at s? return its length (incl. <>), else 0 */
static size_t
rawtag(const char *s, size_t n)
{
size_t i = 1;
char q = 0;
if (n < 3 || s[0] != '<')
return 0;
if (!islower((unsigned char)s[1]) && s[1] != '/' && s[1] != '!')
return 0;
if (s[1] == '/' && !islower((unsigned char)s[2]))
return 0;
for (; i < n; i++) {
if (q) {
if (s[i] == q)
q = 0;
} else if (s[i] == '"' || s[i] == '\'') {
q = s[i];
} else if (s[i] == '>') {
return i + 1;
}
}
return 0;
}
/* render inline markdown: `code`, **strong**, *em*, ,
* [text](url); raw lowercase html tags pass through, the rest of
* & < > is escaped */
static void
inlinemd(FILE *f, const char *s, size_t n)
{
size_t i = 0;
while (i < n) {
if (s[i] == '`') {
const char *end = memchr(s + i + 1, '`', n - i - 1);
if (end) {
fputs("<code>", f);
esc(f, s + i + 1, end - (s + i + 1));
fputs("</code>", f);
i = end - s + 1;
continue;
}
}
if (i + 1 < n && s[i] == '*' && s[i+1] == '*') {
const char *end = NULL, *p;
for (p = s + i + 2; p + 1 < s + n; p++)
if (p[0] == '*' && p[1] == '*') { end = p; break; }
if (end) {
fputs("<strong>", f);
inlinemd(f, s + i + 2, end - (s + i + 2));
fputs("</strong>", f);
i = end - s + 2;
continue;
}
}
if (s[i] == '*' && i + 1 < n && s[i+1] != ' ') {
const char *end = memchr(s + i + 1, '*', n - i - 1);
if (end && end > s + i + 1) {
fputs("<em>", f);
inlinemd(f, s + i + 1, end - (s + i + 1));
fputs("</em>", f);
i = end - s + 1;
continue;
}
}
if (s[i] == '!' && i + 1 < n && s[i+1] == '[') {
const char *rb = memchr(s + i, ']', n - i);
if (rb && rb + 1 < s + n && rb[1] == '(') {
const char *rp = memchr(rb, ')', n - (rb - s));
if (rp) {
fputs("<img src=\"", f);
esc(f, rb + 2, rp - (rb + 2));
fputs("\" alt=\"", f);
esc(f, s + i + 2, rb - (s + i + 2));
fputs("\">", f);
i = rp - s + 1;
continue;
}
}
}
if (s[i] == '[') {
const char *rb = memchr(s + i, ']', n - i);
if (rb && rb + 1 < s + n && rb[1] == '(') {
const char *rp = memchr(rb, ')', n - (rb - s));
if (rp) {
fputs("<a href=\"", f);
esc(f, rb + 2, rp - (rb + 2));
fputs("\">", f);
inlinemd(f, s + i + 1, rb - (s + i + 1));
fputs("</a>", f);
i = rp - s + 1;
continue;
}
}
}
if (s[i] == '<') {
size_t tn = rawtag(s + i, n - i);
if (tn) {
fwrite(s + i, 1, tn, f);
i += tn;
continue;
}
}
switch (s[i]) {
case '&': fputs("&", f); break;
case '<': fputs("<", f); break;
case '>': fputs(">", f); break;
default: fputc(s[i], f);
}
i++;
}
}
static int
ishr(const char *s)
{
int n = 0;
while (*s == '-' || *s == '*') {
n++;
s++;
}
while (*s == ' ' || *s == '\n' || *s == '\r')
s++;
return n >= 3 && !*s;
}
static int
isblank_(const char *s)
{
while (*s == ' ' || *s == '\t' || *s == '\r')
s++;
return !*s;
}
/* trim trailing whitespace (incl. markdown double-space line breaks) */
static size_t
rtrim(const char *s)
{
size_t n = strlen(s);
while (n && (s[n-1] == ' ' || s[n-1] == '\t' || s[n-1] == '\r'))
n--;
return n;
}
/* render the markdown body, line based */
static void
mdrender(FILE *f, char *body)
{
char *line, *next;
int inpara = 0, inul = 0, inol = 0, inquote = 0, intable = 0;
for (line = body; line; line = next) {
next = strchr(line, '\n');
if (next)
*next++ = '\0';
/* fenced code block */
if (!strncmp(line, "```", 3)) {
if (inpara) { fputs("</p>\n", f); inpara = 0; }
if (inul) { fputs("</ul>\n", f); inul = 0; }
if (inol) { fputs("</ol>\n", f); inol = 0; }
if (inquote) { fputs("</blockquote>\n", f); inquote = 0; }
if (intable) { fputs("</table>\n", f); intable = 0; }
fputs("<pre><code>", f);
for (line = next; line; line = next) {
next = strchr(line, '\n');
if (next)
*next++ = '\0';
if (!strncmp(line, "```", 3))
break;
escs(f, line);
fputc('\n', f);
}
fputs("</code></pre>\n", f);
continue;
}
if (isblank_(line)) {
if (inpara) { fputs("</p>\n", f); inpara = 0; }
if (inul) { fputs("</ul>\n", f); inul = 0; }
if (inol) { fputs("</ol>\n", f); inol = 0; }
if (inquote) { fputs("</blockquote>\n", f); inquote = 0; }
if (intable) { fputs("</table>\n", f); intable = 0; }
continue;
}
/* headers: shift one level down, page h1 is the title */
if (*line == '#') {
int lvl = 0;
while (*line == '#') {
lvl++;
line++;
}
while (*line == ' ')
line++;
if (lvl > 5)
lvl = 5;
if (inpara) { fputs("</p>\n", f); inpara = 0; }
fprintf(f, "<h%d>", lvl + 1);
inlinemd(f, line, rtrim(line));
fprintf(f, "</h%d>\n", lvl + 1);
continue;
}
if (ishr(line)) {
if (inpara) { fputs("</p>\n", f); inpara = 0; }
fputs("<hr>\n", f);
continue;
}
/* blockquote */
if (*line == '>') {
line++;
if (*line == ' ')
line++;
if (!inquote) {
if (inpara) { fputs("</p>\n", f); inpara = 0; }
fputs("<blockquote>\n", f);
inquote = 1;
}
if (isblank_(line)) {
fputs("<br>\n", f);
} else {
inlinemd(f, line, rtrim(line));
fputs("<br>\n", f);
}
continue;
}
if (inquote) {
fputs("</blockquote>\n", f);
inquote = 0;
}
/* table row */
if (*line == '|') {
char *p = line + 1, *cell;
int sep = 1;
for (cell = p; *cell; cell++)
if (*cell != '-' && *cell != '|' && *cell != ' ' &&
*cell != ':')
sep = 0;
if (sep && intable)
continue;
if (!intable) {
if (inpara) { fputs("</p>\n", f); inpara = 0; }
fputs("<table>\n", f);
intable = 1;
}
fputs("<tr>", f);
while (p && *p) {
char *bar = p;
char q = 0;
for (; *bar; bar++) {
if (*bar == '`')
q = !q;
if (*bar == '|' && !q)
break;
}
size_t cn = (*bar ? (size_t)(bar - p) : strlen(p));
size_t cs = 0;
while (cs < cn && p[cs] == ' ')
cs++;
size_t ce = cn;
while (ce > cs && (p[ce-1] == ' ' || p[ce-1] == '\r'))
ce--;
if (!(ce == cs && !*bar)) {
fputs("<td>", f);
inlinemd(f, p + cs, ce - cs);
fputs("</td>", f);
}
p = *bar ? bar + 1 : NULL;
}
fputs("</tr>\n", f);
continue;
}
if (intable) {
fputs("</table>\n", f);
intable = 0;
}
/* lists (single level; nested indentation is flattened) */
const char *t = line;
while (*t == ' ' || *t == '\t')
t++;
if ((t[0] == '-' || t[0] == '*' || t[0] == '+') && t[1] == ' ') {
if (inpara) { fputs("</p>\n", f); inpara = 0; }
if (inol) { fputs("</ol>\n", f); inol = 0; }
if (!inul) {
fputs("<ul>\n", f);
inul = 1;
}
fputs("<li>", f);
inlinemd(f, t + 2, rtrim(t + 2));
fputs("</li>\n", f);
continue;
}
if (isdigit((unsigned char)t[0])) {
const char *d = t;
while (isdigit((unsigned char)*d))
d++;
if (d[0] == '.' && d[1] == ' ') {
if (inpara) { fputs("</p>\n", f); inpara = 0; }
if (inul) { fputs("</ul>\n", f); inul = 0; }
if (!inol) {
fputs("<ol>\n", f);
inol = 1;
}
fputs("<li>", f);
inlinemd(f, d + 2, rtrim(d + 2));
fputs("</li>\n", f);
continue;
}
}
if (inul) { fputs("</ul>\n", f); inul = 0; }
if (inol) { fputs("</ol>\n", f); inol = 0; }
/* raw block-level html: emit without paragraph wrapper */
if (rawtag(t, strlen(t))) {
if (inpara) { fputs("</p>\n", f); inpara = 0; }
inlinemd(f, line, rtrim(line));
fputc('\n', f);
continue;
}
/* paragraph text */
if (!inpara) {
fputs("<p>", f);
inpara = 1;
} else {
fputc('\n', f);
}
inlinemd(f, line, rtrim(line));
}
if (inpara)
fputs("</p>\n", f);
if (inul)
fputs("</ul>\n", f);
if (inol)
fputs("</ol>\n", f);
if (inquote)
fputs("</blockquote>\n", f);
if (intable)
fputs("</table>\n", f);
}
/* ------------------------------------------------------------- templates */
static void
chrome(FILE *f, const char *prefix)
{
fprintf(f,
" <div class=\"mode-container\">\n"
" <input type=\"radio\" name=\"theme\" id=\"theme-auto\" checked>\n"
" <label for=\"theme-auto\" class=\"auto\" title=\"Auto theme\"></label>\n"
" <input type=\"radio\" name=\"theme\" id=\"theme-light\">\n"
" <label for=\"theme-light\" class=\"light\" title=\"Light theme\"></label>\n"
" <input type=\"radio\" name=\"theme\" id=\"theme-dark\">\n"
" <label for=\"theme-dark\" class=\"dark\" title=\"Dark theme\"></label>\n"
" </div>\n"
" <div class=\"top-bar\" role=\"navigation\">\n"
" <a href=\"%sindex.html\" title=\"Main page\"><img src=\"%sc/sigma.svg\" alt=\"Main page\"></a>\n"
" <a href=\"%sabout.html\" title=\"About\"><img src=\"%sc/news.svg\" alt=\"About\"></a>\n"
" <a href=\"#\" title=\"Code\"><img src=\"%sc/code.svg\" alt=\"Code\"></a>\n"
" <a href=\"%spics.html\" title=\"Photography\"><img src=\"%sc/camera.svg\" alt=\"Photography\"></a>\n"
" <a href=\"#\" title=\"Pictures\"><img src=\"%sc/picture.svg\" alt=\"Pictures\"></a>\n"
" <a href=\"%svids.html\" title=\"Videos\"><img src=\"%sc/video.svg\" alt=\"Videos\"></a>\n"
" <a href=\"#\" title=\"Donations\"><img src=\"%sc/beer.svg\" alt=\"Donations\"></a>\n"
" <a href=\"#\" title=\"Social\"><img src=\"%sc/elephant.svg\" alt=\"Social\"></a>\n"
" </div>\n",
prefix, prefix, prefix, prefix, prefix, prefix, prefix, prefix,
prefix, prefix, prefix, prefix);
}
static void
header(FILE *f, const char *title, const char *desc, const char *prefix)
{
fputs("<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n"
" <meta charset=\"utf-8\">\n <title>", f);
escs(f, title);
fprintf(f, " | Kris Yotam</title>\n"
" <link rel=\"stylesheet\" href=\"%sc/site.css\">\n", prefix);
if (desc && *desc) {
fputs(" <meta name=\"description\" content=\"", f);
escs(f, desc);
fputs("\">\n", f);
}
fputs(" <meta name=\"viewport\" content=\"width=device-width, "
"initial-scale=1\">\n </head>\n <body>\n", f);
chrome(f, prefix);
}
static void
footer(FILE *f)
{
fputs(" </body>\n</html>\n", f);
}
static void
lower(char *dst, const char *src, size_t n)
{
size_t i;
for (i = 0; src[i] && i < n - 1; i++)
dst[i] = src[i] == ' ' ? '-' : tolower((unsigned char)src[i]);
dst[i] = '\0';
}
/* s-expression metadata block (plain layout) */
static void
metasexpr(FILE *f, const Entry *e)
{
char scls[64];
fprintf(f, " <div class=\"keepws\">\n(<span class=\"bold\">%s</span>\n", e->type);
fprintf(f, " (date %s)", *e->date ? e->date : "undated");
if (*e->category)
fprintf(f, "\n (category <a href=\"../../%s.html\">%s</a>)",
e->type, e->category);
if (*e->status) {
lower(scls, e->status, sizeof(scls));
fprintf(f, "\n (status <span class=\"status-%s\">%s</span>)", scls, scls);
}
if (*e->confidence)
fprintf(f, "\n (confidence %s)", e->confidence);
if (*e->importance)
fprintf(f, "\n (importance %s)", e->importance);
if (*e->tags)
fprintf(f, "\n (tags %s)", e->tags);
fputs(")\n </div>\n", f);
}
/* stat(1)-style header (after blog.parasrah.com) */
static void
statblock(FILE *f, const Entry *e)
{
char scls[64];
fputs(" <pre class=\"stat\">", f);
fprintf(f, " File: <a href=\"../../%s.html\">/n/kris/%s</a>/%s\n",
e->type, e->type, e->slug);
fprintf(f, " Size: %ld words\n", e->words);
fputs("Access: (0644/-rw-r--r--) Uid: (1000/kris) Gid: (100/users)\n", f);
fprintf(f, "Modify: %s\n", *e->date ? e->date : "unknown");
if (*e->status) {
lower(scls, e->status, sizeof(scls));
fprintf(f, "Status: <span class=\"status-%s\">%s</span>", scls, scls);
if (*e->confidence)
fprintf(f, " confidence: %s", e->confidence);
if (*e->importance)
fprintf(f, " importance: %s", e->importance);
fputc('\n', f);
}
if (*e->tags)
fprintf(f, " Tags: %s\n", e->tags);
fputs("</pre>\n", f);
}
/* one-line centered metadata: 2026-05-15 · haiku · growing */
static void
metaline(FILE *f, const Entry *e, const char *cls)
{
char scls[64];
fprintf(f, " <div class=\"%s\">", cls);
fputs(*e->date ? e->date : "undated", f);
if (*e->category) {
fputs(" · <a href=\"../../", f);
fprintf(f, "%s.html\">%s</a>", e->type, e->category);
}
if (*e->status) {
lower(scls, e->status, sizeof(scls));
fprintf(f, " · <span class=\"status-%s\">%s</span>", scls, scls);
}
fputs("</div>\n", f);
}
static void
writeentry(const Entry *e)
{
char path[512];
FILE *f;
snprintf(path, sizeof(path), OUTDIR "/%s/%s.html", e->type, e->slug);
if (!(f = fopen(path, "w")))
die(path);
header(f, e->title, e->preview, "../../");
switch (layoutof(e->type)) {
case LSTAT:
fputs(" <h1>", f);
escs(f, e->title);
fputs("</h1>\n", f);
statblock(f, e);
if (*e->preview) {
fputs(" <p class=\"preview\">", f);
escs(f, e->preview);
fputs("</p>\n", f);
}
fputs(" <div class=\"prose\">\n", f);
mdrender(f, e->body);
fputs(" </div>\n", f);
break;
case LACADEMIC:
fputs(" <h1 class=\"acad-title\">", f);
escs(f, e->title);
fputs("</h1>\n", f);
metaline(f, e, "acad-meta");
if (*e->preview) {
fputs(" <fieldset class=\"wrap abstract\">"
"<legend>abstract</legend>", f);
escs(f, e->preview);
fputs("</fieldset>\n", f);
}
fputs(" <div class=\"prose justify\">\n", f);
mdrender(f, e->body);
fputs(" </div>\n", f);
break;
case LWINDOW:
fputs(" <div class=\"window\">\n"
" <div class=\"title-bar\"><h1 class=\"title\">", f);
escs(f, e->title);
fputs("</h1></div>\n <div class=\"window-body\">\n", f);
metaline(f, e, "win-meta");
if (*e->preview) {
fputs(" <p class=\"preview\">", f);
escs(f, e->preview);
fputs("</p>\n", f);
}
fputs(" <div class=\"prose\">\n", f);
mdrender(f, e->body);
fputs(" </div>\n </div>\n </div>\n", f);
break;
case LCHAPBOOK:
fputs(" <div class=\"chapbook\">\n <h1>", f);
escs(f, e->title);
fputs("</h1>\n", f);
metaline(f, e, "chap-meta");
fputs(" <div class=\"prose poem\">\n", f);
mdrender(f, e->body);
fputs(" </div>\n </div>\n", f);
break;
default:
fputs(" <h1>", f);
escs(f, e->title);
fputs("</h1>\n", f);
metasexpr(f, e);
if (*e->preview) {
fputs(" <p class=\"preview\">", f);
escs(f, e->preview);
fputs("</p>\n", f);
}
fputs(" <div class=\"prose\">\n", f);
mdrender(f, e->body);
fputs(" </div>\n", f);
}
footer(f);
fclose(f);
}
static void
navline(FILE *f)
{
size_t i;
fputs(" <div class=\"keepws\">(<span class=\"bold\">around-here</span>\n ", f);
for (i = 0; i < NTYPES; i++)
fprintf(f, " (<a href=\"%s.html\">%s</a>)", types[i][0], types[i][0]);
fputs(")</div>\n", f);
}
static int
cmpdate(const void *a, const void *b)
{
const Entry *x = *(Entry * const *)a, *y = *(Entry * const *)b;
return strcmp(y->date, x->date);
}
static void
writelisting(const char *type, const char *title)
{
const Entry *sorted[MAXENTRIES];
char path[512], year[8] = "";
size_t i, n = 0, ncats = 0;
const char *cats[MAXENTRIES];
FILE *f;
for (i = 0; i < nentries; i++)
if (!strcmp(entries[i].type, type))
sorted[n++] = &entries[i];
qsort(sorted, n, sizeof(*sorted), cmpdate);
for (i = 0; i < n; i++) {
size_t j;
if (!*sorted[i]->category)
continue;
for (j = 0; j < ncats; j++)
if (!strcmp(cats[j], sorted[i]->category))
break;
if (j == ncats)
cats[ncats++] = sorted[i]->category;
}
snprintf(path, sizeof(path), "%s.html", type);
if (!(f = fopen(path, "w")))
die(path);
header(f, title, "", "");
switch (layoutof(type)) {
case LSTAT: {
/* parasrah: directory stat header + ls -l listing */
long total = 0;
for (i = 0; i < n; i++)
total += sorted[i]->words;
fputs(" <h1>", f);
escs(f, title);
fputs("</h1>\n", f);
navline(f);
fprintf(f, "\n <div class=\"stat keepws\">"
" File: /n/kris/%s\n"
" Size: %zu entries, %ld words\n"
"Access: (0755/drwxr-xr-x) Uid: (1000/kris) "
"Gid: (100/users)</div>\n", type, n, total);
fputs(" <div class=\"ls keepws\">\n", f);
for (i = 0; i < n; i++) {
const Entry *e = sorted[i];
fprintf(f, "-rw-r--r-- kris %6ldw %s <a href=\""
OUTDIR "/%s/%s.html\">", e->words,
*e->date ? e->date : "----------",
e->type, e->slug);
escs(f, e->title);
fputs("</a>\n", f);
}
fputs(" </div>\n", f);
break;
}
case LWINDOW:
/* system 7: grid of mini window cards */
fputs(" <h1>", f);
escs(f, title);
fputs("</h1>\n", f);
navline(f);
fprintf(f, "\n <div class=\"stats\">\n"
" <span><em>%zu</em> entries</span>\n"
" <span><em>%zu</em> categories</span>\n"
" </div>\n\n", n, ncats);
fputs(" <div class=\"window-grid\">\n", f);
for (i = 0; i < n; i++) {
const Entry *e = sorted[i];
fprintf(f, " <a class=\"window\" href=\""
OUTDIR "/%s/%s.html\">\n"
" <div class=\"title-bar\">"
"<span class=\"title\">", e->type, e->slug);
escs(f, e->title);
fputs("</span></div>\n"
" <div class=\"window-body\">", f);
if (*e->date)
fprintf(f, "%s", e->date);
if (*e->category) {
fputs(" · ", f);
escs(f, e->category);
}
fputs("</div>\n </a>\n", f);
}
fputs(" </div>\n", f);
break;
case LCHAPBOOK: {
/* chapbook: table of contents grouped by form/category */
size_t c;
fputs(" <div class=\"chapbook\">\n <h1>", f);
escs(f, title);
fputs("</h1>\n", f);
navline(f);
fprintf(f, "\n <div class=\"stats\">\n"
" <span><em>%zu</em> entries</span>\n"
" <span><em>%zu</em> forms</span>\n"
" </div>\n\n", n, ncats);
for (c = 0; c <= ncats; c++) {
const char *cat = c < ncats ? cats[c] : "";
int any = 0;
for (i = 0; i < n; i++) {
const Entry *e = sorted[i];
if (strcmp(e->category, cat))
continue;
if (!any) {
fprintf(f, " <fieldset class=\"wrap\">"
"<legend>%s</legend>\n"
" <div class=\"keepws\">\n",
*cat ? cat : "uncollected");
any = 1;
}
fprintf(f, " %s <a href=\"" OUTDIR
"/%s/%s.html\">",
*e->date ? e->date : "----------",
e->type, e->slug);
escs(f, e->title);
fputs("</a>\n", f);
}
if (any)
fputs(" </div>\n </fieldset>\n", f);
}
fputs(" </div>\n", f);
break;
}
default:
/* year-grouped s-expression listing */
fputs(" <h1>", f);
escs(f, title);
fputs("</h1>\n", f);
navline(f);
fprintf(f, "\n <div class=\"stats\">\n"
" <span><em>%zu</em> entries</span>\n"
" <span><em>%zu</em> categories</span>\n"
" </div>\n\n", n, ncats);
for (i = 0; i < n; i++) {
const Entry *e = sorted[i];
const char *y = *e->date ? e->date : "undated";
if (strncmp(year, y, 4)) {
if (*year)
fputs(" </div>\n", f);
snprintf(year, sizeof(year), "%.4s", y);
fprintf(f, " <div class=\"keepws\">\n"
"(<span class=\"bold\">%s</span>\n", year);
}
fprintf(f, " (%s <a href=\"" OUTDIR "/%s/%s.html\">",
strlen(e->date) > 5 ? e->date + 5 : "--",
e->type, e->slug);
escs(f, e->title);
fputs("</a>", f);
if (*e->category)
fprintf(f, " (%s)", e->category);
fputs(")\n", f);
}
if (*year)
fputs(" </div>\n", f);
break;
}
footer(f);
fclose(f);
printf(" %s: %zu pages\n", type, n);
}
/* ------------------------------------------------------------------ main */
static void
loadtype(const char *dir)
{
char path[512];
struct dirent *d;
DIR *dp;
snprintf(path, sizeof(path), "%s/%s", contentdir, dir);
if (!(dp = opendir(path)))
return;
while ((d = readdir(dp))) {
size_t len = strlen(d->d_name);
char *buf, *body;
Entry *e;
size_t i;
if (len < 5 || strcmp(d->d_name + len - 4, ".mdx"))
continue;
if (nentries >= MAXENTRIES) {
fputs("too many entries\n", stderr);
break;
}
snprintf(path, sizeof(path), "%s/%s/%s", contentdir, dir, d->d_name);
if (!(buf = readfile(path)))
continue;
e = &entries[nentries];
memset(e, 0, sizeof(*e));
if (!(body = frontmatter(buf, e))) {
fprintf(stderr, " skip (no frontmatter): %s\n", path);
free(buf);
continue;
}
/* fall back to directory / filename when missing */
for (i = 0; i < NTYPES; i++)
if (!strcmp(e->type, types[i][0]))
break;
if (i == NTYPES)
snprintf(e->type, sizeof(e->type), "%s", dir);
if (!*e->slug)
snprintf(e->slug, sizeof(e->slug), "%.*s",
(int)(len - 4), d->d_name);
if (!*e->title)
memcpy(e->title, e->slug, sizeof(e->slug));
stripjsx(body);
e->words = wordcount(body);
if (!(e->body = strdup(body)))
die("strdup");
free(buf);
nentries++;
}
closedir(dp);
}
int
main(void)
{
const Entry *recent[MAXENTRIES];
char path[512];
size_t i, n = 0;
for (i = 0; i < NTYPES; i++)
loadtype(types[i][0]);
printf("%zu entries loaded\n", nentries);
if (mkdir(OUTDIR, 0755) && errno != EEXIST)
die(OUTDIR);
for (i = 0; i < NTYPES; i++) {
snprintf(path, sizeof(path), OUTDIR "/%s", types[i][0]);
if (mkdir(path, 0755) && errno != EEXIST)
die(path);
}
for (i = 0; i < nentries; i++)
writeentry(&entries[i]);
for (i = 0; i < NTYPES; i++)
writelisting(types[i][0], types[i][1]);
/* recent-writing block for index.html */
for (i = 0; i < nentries; i++)
if (strcmp(entries[i].type, "verse"))
recent[n++] = &entries[i];
qsort(recent, n, sizeof(*recent), cmpdate);
if (n > NRECENT)
n = NRECENT;
puts("\nrecent-writing:");
for (i = 0; i < n; i++)
printf(" (%s <a href=\"" OUTDIR "/%s/%s.html\">%s</a>)\n",
recent[i]->date, recent[i]->type, recent[i]->slug,
recent[i]->title);
return 0;
}