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 }