/* 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 #include #include #include #include #include #include #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; } /* | | */ 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*, ![alt](src), * [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("", f); esc(f, s + i + 1, end - (s + i + 1)); fputs("", 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("", f); inlinemd(f, s + i + 2, end - (s + i + 2)); fputs("", 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("", f); inlinemd(f, s + i + 1, end - (s + i + 1)); fputs("", 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("\"",", 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("", f); inlinemd(f, s + i + 1, rb - (s + i + 1)); fputs("", 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("

\n", f); inpara = 0; } if (inul) { fputs("\n", f); inul = 0; } if (inol) { fputs("\n", f); inol = 0; } if (inquote) { fputs("\n", f); inquote = 0; } if (intable) { fputs("\n", f); intable = 0; } fputs("
", 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("
\n", f); continue; } if (isblank_(line)) { if (inpara) { fputs("

\n", f); inpara = 0; } if (inul) { fputs("\n", f); inul = 0; } if (inol) { fputs("\n", f); inol = 0; } if (inquote) { fputs("\n", f); inquote = 0; } if (intable) { fputs("\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("

\n", f); inpara = 0; } fprintf(f, "", lvl + 1); inlinemd(f, line, rtrim(line)); fprintf(f, "\n", lvl + 1); continue; } if (ishr(line)) { if (inpara) { fputs("

\n", f); inpara = 0; } fputs("
\n", f); continue; } /* blockquote */ if (*line == '>') { line++; if (*line == ' ') line++; if (!inquote) { if (inpara) { fputs("

\n", f); inpara = 0; } fputs("
\n", f); inquote = 1; } if (isblank_(line)) { fputs("
\n", f); } else { inlinemd(f, line, rtrim(line)); fputs("
\n", f); } continue; } if (inquote) { fputs("
\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("

\n", f); inpara = 0; } fputs("\n", f); intable = 1; } fputs("", 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("", f); } p = *bar ? bar + 1 : NULL; } fputs("\n", f); continue; } if (intable) { fputs("
", f); inlinemd(f, p + cs, ce - cs); fputs("
\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("

\n", f); inpara = 0; } if (inol) { fputs("\n", f); inol = 0; } if (!inul) { fputs("
    \n", f); inul = 1; } fputs("
  • ", f); inlinemd(f, t + 2, rtrim(t + 2)); fputs("
  • \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("

    \n", f); inpara = 0; } if (inul) { fputs("
\n", f); inul = 0; } if (!inol) { fputs("
    \n", f); inol = 1; } fputs("
  1. ", f); inlinemd(f, d + 2, rtrim(d + 2)); fputs("
  2. \n", f); continue; } } if (inul) { fputs("\n", f); inul = 0; } if (inol) { fputs("
\n", f); inol = 0; } /* raw block-level html: emit without paragraph wrapper */ if (rawtag(t, strlen(t))) { if (inpara) { fputs("

\n", f); inpara = 0; } inlinemd(f, line, rtrim(line)); fputc('\n', f); continue; } /* paragraph text */ if (!inpara) { fputs("

", f); inpara = 1; } else { fputc('\n', f); } inlinemd(f, line, rtrim(line)); } if (inpara) fputs("

\n", f); if (inul) fputs("\n", f); if (inol) fputs("\n", f); if (inquote) fputs("\n", f); if (intable) fputs("\n", f); } /* ------------------------------------------------------------- templates */ static void chrome(FILE *f, const char *prefix) { fprintf(f, "
\n" " \n" " \n" " \n" " \n" " \n" " \n" "
\n" "
\n" " \"Main\n" " \"About\"\n" " \"Code\"\n" " \"Photography\"\n" " \"Pictures\"\n" " \"Videos\"\n" " \"Donations\"\n" " \"Social\"\n" "
\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("\n\n \n" " \n ", f); escs(f, title); fprintf(f, " | Kris Yotam\n" " \n", prefix); if (desc && *desc) { fputs(" \n", f); } fputs(" \n \n \n", f); chrome(f, prefix); } static void footer(FILE *f) { fputs(" \n\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, "
\n(%s\n", e->type); fprintf(f, " (date %s)", *e->date ? e->date : "undated"); if (*e->category) fprintf(f, "\n (category %s)", e->type, e->category); if (*e->status) { lower(scls, e->status, sizeof(scls)); fprintf(f, "\n (status %s)", 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
\n", f); } /* stat(1)-style header (after blog.parasrah.com) */ static void statblock(FILE *f, const Entry *e) { char scls[64]; fputs("
", f);
	fprintf(f, "  File: /n/kris/%s/%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: %s", 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("
\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, "
", cls); fputs(*e->date ? e->date : "undated", f); if (*e->category) { fputs(" · %s", e->type, e->category); } if (*e->status) { lower(scls, e->status, sizeof(scls)); fprintf(f, " · %s", scls, scls); } fputs("
\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("

", f); escs(f, e->title); fputs("

\n", f); statblock(f, e); if (*e->preview) { fputs("

", f); escs(f, e->preview); fputs("

\n", f); } fputs("
\n", f); mdrender(f, e->body); fputs("
\n", f); break; case LACADEMIC: fputs("

", f); escs(f, e->title); fputs("

\n", f); metaline(f, e, "acad-meta"); if (*e->preview) { fputs("
" "abstract", f); escs(f, e->preview); fputs("
\n", f); } fputs("
\n", f); mdrender(f, e->body); fputs("
\n", f); break; case LWINDOW: fputs("
\n" "

", f); escs(f, e->title); fputs("

\n
\n", f); metaline(f, e, "win-meta"); if (*e->preview) { fputs("

", f); escs(f, e->preview); fputs("

\n", f); } fputs("
\n", f); mdrender(f, e->body); fputs("
\n
\n
\n", f); break; case LCHAPBOOK: fputs("
\n

", f); escs(f, e->title); fputs("

\n", f); metaline(f, e, "chap-meta"); fputs("
\n", f); mdrender(f, e->body); fputs("
\n
\n", f); break; default: fputs("

", f); escs(f, e->title); fputs("

\n", f); metasexpr(f, e); if (*e->preview) { fputs("

", f); escs(f, e->preview); fputs("

\n", f); } fputs("
\n", f); mdrender(f, e->body); fputs("
\n", f); } footer(f); fclose(f); } static void navline(FILE *f) { size_t i; fputs("
(around-here\n ", f); for (i = 0; i < NTYPES; i++) fprintf(f, " (%s)", types[i][0], types[i][0]); fputs(")
\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("

", f); escs(f, title); fputs("

\n", f); navline(f); fprintf(f, "\n
" " File: /n/kris/%s\n" " Size: %zu entries, %ld words\n" "Access: (0755/drwxr-xr-x) Uid: (1000/kris) " "Gid: (100/users)
\n", type, n, total); fputs("
\n", f); for (i = 0; i < n; i++) { const Entry *e = sorted[i]; fprintf(f, "-rw-r--r-- kris %6ldw %s ", e->words, *e->date ? e->date : "----------", e->type, e->slug); escs(f, e->title); fputs("\n", f); } fputs("
\n", f); break; } case LWINDOW: /* system 7: grid of mini window cards */ fputs("

", f); escs(f, title); fputs("

\n", f); navline(f); fprintf(f, "\n
\n" " %zu entries\n" " %zu categories\n" "
\n\n", n, ncats); fputs(" \n", f); break; case LCHAPBOOK: { /* chapbook: table of contents grouped by form/category */ size_t c; fputs("
\n

", f); escs(f, title); fputs("

\n", f); navline(f); fprintf(f, "\n
\n" " %zu entries\n" " %zu forms\n" "
\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, "
" "%s\n" "
\n", *cat ? cat : "uncollected"); any = 1; } fprintf(f, " %s ", *e->date ? e->date : "----------", e->type, e->slug); escs(f, e->title); fputs("\n", f); } if (any) fputs("
\n
\n", f); } fputs("
\n", f); break; } default: /* year-grouped s-expression listing */ fputs("

", f); escs(f, title); fputs("

\n", f); navline(f); fprintf(f, "\n
\n" " %zu entries\n" " %zu categories\n" "
\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(" \n", f); snprintf(year, sizeof(year), "%.4s", y); fprintf(f, "
\n" "(%s\n", year); } fprintf(f, " (%s ", strlen(e->date) > 5 ? e->date + 5 : "--", e->type, e->slug); escs(f, e->title); fputs("", f); if (*e->category) fprintf(f, " (%s)", e->category); fputs(")\n", f); } if (*year) fputs("
\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 %s)\n", recent[i]->date, recent[i]->type, recent[i]->slug, recent[i]->title); return 0; }