website

website.git
git clone git://git.lenczewski.org/website.git
Log | Files | Refs

template.c (8949B)


      1 #define UTILS_IMPL
      2 #include "utils.h"
      3 
      4 #include <assert.h>
      5 #include <ctype.h>
      6 #include <limits.h>
      7 #include <stdio.h>
      8 #include <stdlib.h>
      9 #include <string.h>
     10 #include <time.h>
     11 
     12 #include <getopt.h>
     13 
     14 #include <libgen.h> /* dirname() and basename() */
     15 
     16 #include <sys/uio.h>
     17 
     18 static struct opts {
     19 	int verbose;
     20 
     21 	char *outdir, *template;
     22 
     23 	struct {
     24 		char **ptr;
     25 		size_t len;
     26 	} sources;
     27 
     28 	char *index;
     29 	char *urlfrag;
     30 } opts = {
     31 	.verbose = 0,
     32 	.outdir = NULL,
     33 	.template = NULL,
     34 	.urlfrag = "/",
     35 };
     36 
     37 #define OPTSTR "hvo:t:i:u:"
     38 
     39 static void
     40 usage(char *prog)
     41 {
     42 	fprintf(stderr, "Usage: %s [-hv] -o <outdir> -t <template.html> "
     43 			"[-i <index.html> -u <urlfrag>] <sources...>\n",
     44 			prog);
     45 
     46 	fprintf(stderr, "\t-h : display this help information\n");
     47 	fprintf(stderr, "\t-v : enable verbose output\n");
     48 	fprintf(stderr, "\t-o : specify the output directory\n");
     49 	fprintf(stderr, "\t-t : specify the base template file\n");
     50 	fprintf(stderr, "\t-i : specify an index file to generate\n");
     51 	fprintf(stderr, "\t-u : specify a base url fragment for generated links\n");
     52 	fprintf(stderr, "\tsources... : one or more source files\n");
     53 }
     54 
     55 static int
     56 parse_opts(int argc, char **argv)
     57 {
     58 	int opt;
     59 	while ((opt = getopt(argc, argv, OPTSTR)) > 0) {
     60 		switch (opt) {
     61 		case 'v':
     62 			opts.verbose = 1;
     63 			break;
     64 
     65 		case 'o':
     66 			opts.outdir = optarg;
     67 			break;
     68 
     69 		case 't':
     70 			opts.template = optarg;
     71 			break;
     72 
     73 		case 'i':
     74 			opts.index = optarg;
     75 			break;
     76 
     77 		case 'u':
     78 			opts.urlfrag = optarg;
     79 			break;
     80 
     81 		default:
     82 			return -1;
     83 		}
     84 	}
     85 
     86 	if (!opts.outdir) {
     87 		fprintf(stderr, "Missing output directory\n");
     88 		return -1;
     89 	}
     90 
     91 	if (!opts.template) {
     92 		fprintf(stderr, "Missing base template file\n");
     93 		return -1;
     94 	}
     95 
     96 	opts.sources.ptr = argv + optind;
     97 	opts.sources.len = argc - optind;
     98 
     99 	if (!opts.sources.len) {
    100 		fprintf(stderr, "Missing source files\n");
    101 		return -1;
    102 	}
    103 
    104 	if (opts.index && !opts.urlfrag) {
    105 		fprintf(stderr, "Specified an index file, but no base url fragment\n");
    106 		return -1;
    107 	}
    108 
    109 	return 0;
    110 }
    111 
    112 struct substitutions {
    113 	struct str title, created, edited, body;
    114 };
    115 
    116 #define SUBST_PATH_CAP 128
    117 #define SUBST_TITLE_CAP 128
    118 
    119 static inline struct str
    120 title_from_basename(char const *basename, char buf[static SUBST_TITLE_CAP])
    121 {
    122 	struct str res;
    123 
    124 	res.ptr = strncpy(buf, basename, SUBST_TITLE_CAP - 1);
    125 	*(res.ptr + SUBST_TITLE_CAP - 1) = 0; // ensure string is terminated
    126 
    127 	res.ptr[0] = toupper(res.ptr[0]); // capitalise first letter
    128 
    129 	char *ext = strrchr(res.ptr, '.');
    130 	if (ext) *ext = '\0';  // strip file extension
    131 
    132 	res.len = ext - res.ptr;
    133 
    134 	return res;
    135 }
    136 
    137 static inline struct str
    138 title_from_filepath(char *filepath, char buf[static SUBST_TITLE_CAP])
    139 {
    140 	struct str res;
    141 
    142 	char path[PATH_MAX];
    143 	strcpy(stpncpy(path, filepath, sizeof path), ".title.meta");
    144 
    145 	int title_file = open(path, O_RDONLY);
    146 	if (title_file < 0)
    147 		return title_from_basename(basename(filepath), buf);
    148 
    149 	res.ptr = buf;
    150 	res.len = read(title_file, res.ptr, SUBST_TITLE_CAP);
    151 
    152 	return res;
    153 }
    154 
    155 #define SUBST_TIME_CAP 32
    156 
    157 static inline struct str
    158 date_from_timespec(struct timespec *ts, char buf[static SUBST_TIME_CAP])
    159 {
    160 	strftime(buf, SUBST_TIME_CAP, "%Y/%m/%d", localtime(&ts->tv_sec));
    161 
    162 	struct str res;
    163 	res.ptr = buf;
    164 	res.len = strlen(res.ptr);
    165 	return res;
    166 }
    167 
    168 struct htmlpage {
    169 	char path[SUBST_PATH_CAP];
    170 	char title[SUBST_TITLE_CAP];
    171 	char created_buf[SUBST_TIME_CAP];
    172 	struct timespec created;
    173 
    174 	struct list_node list_node;
    175 };
    176 
    177 static inline int
    178 htmlpage_compare(void const *a, void const *b, void *arg)
    179 {
    180 	(void) arg;
    181 
    182 	struct htmlpage const *lhs = b, *rhs = a; // reverse order
    183 	return timespec_compare(&lhs->created, &rhs->created);
    184 }
    185 
    186 static void
    187 format_index_page(struct htmlpage *pages, size_t len, int fd, char *urlfrag)
    188 {
    189 	qsort_r(pages, len, sizeof *pages, htmlpage_compare, NULL);
    190 
    191 	for (size_t i = 0; i < len; i++) {
    192 		struct htmlpage *page = pages + i;
    193 
    194 		dprintf(fd, "<li><span><a href=\"%s%s\">%s: %s</a></span></li>\n",
    195 			    urlfrag, page->path, page->created_buf, page->title);
    196 	}
    197 }
    198 
    199 int
    200 template(int fd, char *tpl, size_t tpl_len, struct substitutions const *substs);
    201 
    202 int
    203 main(int argc, char **argv)
    204 {
    205 	if (parse_opts(argc, argv)) {
    206 		usage(argv[0]);
    207 		exit(EXIT_FAILURE);
    208 	}
    209 
    210 	if (opts.verbose) {
    211 		printf("Sources (%zu files):\n", opts.sources.len);
    212 		for (size_t i = 0; i < opts.sources.len; i++)
    213 			printf("\t%s\n", opts.sources.ptr[i]);
    214 	}
    215 
    216 	int dirfd = open(opts.outdir, O_DIRECTORY | O_PATH | O_CLOEXEC, O_RDONLY);
    217 	if (dirfd < 0) {
    218 		fprintf(stderr, "Failed to open destination directory: %s\n", opts.outdir);
    219 		exit(EXIT_FAILURE);
    220 	}
    221 
    222 	if (opts.verbose)
    223 		printf("Destination directory: %s\n", opts.outdir);
    224 
    225 	int indexfd = -1;
    226 	if (opts.index && (indexfd = creat(opts.index, 0644)) < 0) {
    227 		fprintf(stderr, "Failed to create index file: %s\n", opts.index);
    228 		exit(EXIT_FAILURE);
    229 	}
    230 
    231 	char *tpl;
    232 	size_t tpl_len;
    233 	if (mmap_file(opts.template, O_RDONLY, &tpl, &tpl_len, NULL)) {
    234 		fprintf(stderr, "Failed to mmap() template file\n");
    235 		exit(EXIT_FAILURE);
    236 	}
    237 
    238 	struct arena arena = {
    239 		.ptr = malloc(8 * MiB),
    240 		.cap = 8 * MiB,
    241 		.len = 0,
    242 	};
    243 
    244 	for (size_t i = 0; i < opts.sources.len; i++) {
    245 		char *original_srcpath = opts.sources.ptr[i];
    246 
    247 		char srcpath[PATH_MAX];
    248 		*stpncpy(srcpath, original_srcpath, sizeof srcpath) = 0;
    249 
    250 		if (opts.verbose)
    251 			printf("Processing source file: %s\n", srcpath);
    252 
    253 		char *source;
    254 		size_t source_len;
    255 		struct stat source_statbuf;
    256 		if (mmap_file(srcpath, O_RDONLY, &source, &source_len, &source_statbuf)) {
    257 			fprintf(stderr, "Failed to mmap() source file\n");
    258 			goto err;
    259 		}
    260 
    261 		struct htmlpage *page = ALLOC_SIZED(&arena, struct htmlpage);
    262 		assert(page);
    263 
    264 		page->list_node.prev = page->list_node.next = NULL;
    265 
    266 		*stpncpy(page->path, basename(srcpath), (sizeof page->path) - 1) = 0;
    267 		page->created = source_statbuf.st_mtim;
    268 
    269 		if (opts.verbose)
    270 			printf("Placing new file at %s/%s\n", opts.outdir, page->path);
    271 
    272 		int dstfd = openat(dirfd, page->path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
    273 		if (dstfd < 0) {
    274 			munmap(source, source_len);
    275 			goto err;
    276 		}
    277 
    278 		struct substitutions substs;
    279 
    280 		substs.title = title_from_filepath(original_srcpath, page->title);
    281 		substs.created = date_from_timespec(&page->created, page->created_buf);
    282 
    283 		substs.body.ptr = source;
    284 		substs.body.len = source_len;
    285 
    286 		int res = template(dstfd, tpl, tpl_len, &substs);
    287 
    288 		close(dstfd);
    289 
    290 		if (source_len)
    291 			munmap(source, source_len);
    292 
    293 		if (res)
    294 			goto err;
    295 
    296 		if (opts.verbose)
    297 			printf("Processed file: %s -> %s/%s\n", srcpath, opts.outdir, page->path);
    298 
    299 		continue;
    300 
    301 err:
    302 		fprintf(stderr, "Failed while processing %s\n", srcpath);
    303 		exit(EXIT_FAILURE);
    304 	}
    305 
    306 	if (indexfd > 0)
    307 		format_index_page(arena.ptr, arena.len / sizeof(struct htmlpage),
    308 				  indexfd, opts.urlfrag);
    309 
    310 	exit(EXIT_SUCCESS);
    311 }
    312 
    313 #define PLACEHOLDER_START_MARKER '{'
    314 #define PLACEHOLDER_END_MARKER '}'
    315 #define PLACEHOLDER(str) "{" str "}"
    316 
    317 static char *
    318 next_placeholder(char *start, char *end, struct str *out)
    319 {
    320 	char *ptr;
    321 	if ((ptr = strnchr(start, end, PLACEHOLDER_START_MARKER)) == end)
    322 		goto no_placeholder; /* no placeholder found */
    323 
    324 	struct str key;
    325 	key.ptr = ptr;
    326 
    327 	if ((ptr = strnchr(ptr, end, PLACEHOLDER_END_MARKER)) == end)
    328 		goto no_placeholder; /* unterminated placeholder key */
    329 
    330 	key.len = ++ptr - key.ptr; /* include placeholder end marker in key */
    331 
    332 	*out = key;
    333 
    334 	return key.ptr;
    335 
    336 no_placeholder:
    337 	return ptr;
    338 }
    339 
    340 static int
    341 substitute(struct substitutions const *substs, struct str key, struct iovec *iov)
    342 {
    343 	if (key.len == strlen(PLACEHOLDER("title")) &&
    344 	    strncmp(key.ptr, PLACEHOLDER("title"), key.len) == 0) {
    345 		iov->iov_base = substs->title.ptr;
    346 		iov->iov_len = substs->title.len;
    347 		return 0;
    348 	}
    349 
    350 	if (key.len == strlen(PLACEHOLDER("created")) &&
    351 	    strncmp(key.ptr, PLACEHOLDER("created"), key.len) == 0) {
    352 		iov->iov_base = substs->created.ptr;
    353 		iov->iov_len = substs->created.len;
    354 		return 0;
    355 	}
    356 
    357 	if (key.len == strlen(PLACEHOLDER("body")) &&
    358 	    strncmp(key.ptr, PLACEHOLDER("body"), key.len) == 0) {
    359 		iov->iov_base = substs->body.ptr;
    360 		iov->iov_len = substs->body.len;
    361 		return 0;
    362 	}
    363 
    364 	return -1;
    365 }
    366 
    367 int
    368 template(int fd, char *tpl, size_t tpl_len, struct substitutions const *substs)
    369 {
    370 	char *ptr = tpl, *end = tpl + tpl_len;
    371 
    372 	size_t count = 0, expected_nwritten = 0;
    373 	struct iovec iovs[IOV_MAX];
    374 
    375 	while (ptr < end) {
    376 		struct str key;
    377 		char *placeholder = next_placeholder(ptr, end, &key);
    378 
    379 		assert(count < IOV_MAX);
    380 		iovs[count].iov_base = ptr;
    381 		iovs[count].iov_len = placeholder - ptr;
    382 		expected_nwritten += iovs[count].iov_len;
    383 		count++;
    384 
    385 		if (placeholder == end) /* reached the end of the file */
    386 			break;
    387 
    388 		assert(count < IOV_MAX);
    389 		if (substitute(substs, key, &iovs[count++]) < 0) {
    390 			fprintf(stderr, "warnings: unknown key '%.*s', ignoring\n",
    391 					(int) key.len, key.ptr);
    392 		}
    393 
    394 		expected_nwritten += iovs[count-1].iov_len;
    395 
    396 		ptr = placeholder + key.len;
    397 	}
    398 
    399 	ssize_t res = writev(fd, iovs, count);
    400 	return (res == (ssize_t) expected_nwritten) ? 0 : -1;
    401 }