stamd.c (16472B)
1 #define _XOPEN_SOURCE 700 2 3 #include <ctype.h> 4 #include <errno.h> 5 #include <error.h> 6 #include <fcntl.h> 7 #include <libgen.h> 8 #include <limits.h> 9 #include <stdio.h> 10 #include <stdlib.h> 11 #include <string.h> 12 #include <sys/mman.h> 13 #include <sys/stat.h> 14 #include <time.h> 15 #include <unistd.h> 16 17 #include "md4c-html.h" 18 19 #include <cii/atom.h> 20 #include <cii/list.h> 21 #include <cii/mem.h> 22 #include <cii/str.h> 23 #include <cii/table.h> 24 25 #define MAX_SIZE 100 26 27 #define BASE_URL "https://dimitrijedobrota.com/blog" 28 #define TITLE "Dimitrije Dobrota's blog" 29 #define AUTHOR "Dimitrije Dobrota" 30 #define AUTHOR_EMAIL "mail@dimitrijedobrota.com" 31 32 #define SETTINGS_TIME_FORMAT "%Y-%m-%d" 33 34 #define ATOM_TIME_FORMAT "%Y-%m-%dT%H:%M:%SZ" 35 #define ATOM_FILE "index.atom" 36 #define ATOM_LOCATION BASE_URL "/" ATOM_FILE 37 38 #define RSS_TIME_FORMAT "%a, %d %b %Y %H:%M:%S +0200" 39 #define RSS_FILE "rss.xml" 40 #define RSS_LOCATION BASE_URL "/" RSS_FILE 41 42 #define SITEMAP "%a, %d %b %Y %H:%M:%S +0200" 43 #define SITEMAP_FILE "sitemap.xml" 44 45 List_T articles; /* List of all articles */ 46 List_T articlesVisible; /* List of all articles that are not hidden*/ 47 Table_T category_table; /* Table of all non hidden articles for each category*/ 48 49 void usage(char *argv0) { 50 fprintf(stderr, "Usage: %s [-o output_dir] article\n", argv0); 51 exit(EXIT_FAILURE); 52 } 53 54 void process(const MD_CHAR *text, MD_SIZE size, void *userdata) { 55 fprintf((FILE *)userdata, "%.*s", size, text); 56 } 57 58 char *memory_open(char *infile, int *size) { 59 char *addr; 60 int fd, ret; 61 struct stat st; 62 63 if ((fd = open(infile, O_RDWR)) < 0) 64 error(EXIT_FAILURE, errno, "%s", infile); 65 66 if ((ret = fstat(fd, &st)) < 0) 67 error(EXIT_FAILURE, errno, "line %d, fstat", __LINE__); 68 69 if ((addr = mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 70 0)) == MAP_FAILED) 71 error(EXIT_FAILURE, errno, "line %d, addr", __LINE__); 72 73 if (size) 74 *size = st.st_size; 75 76 return addr; 77 } 78 79 void memory_close(char *content, int size) { 80 if (munmap(content, size) == -1) 81 error(EXIT_FAILURE, errno, "line %d, munmap", __LINE__); 82 } 83 84 char *normalize(char *name) { 85 char *s = name, *c = name; 86 87 while (isspace(*c)) 88 c++; 89 90 for (; *c; c++) 91 if (isspace(*c) && *(name - 1) != '_') 92 *name++ = '_'; 93 else if (isalnum(*c)) 94 *name++ = *c; 95 *name = '\0'; 96 return s; 97 } 98 99 void strip_whitspace(char *str) { 100 char *p; 101 102 if (!str) 103 return; 104 105 p = str + strlen(str); 106 do { 107 p--; 108 } while (isspace(*p)); 109 110 *(p + 1) = '\0'; 111 112 p = str; 113 while (isspace(*p)) 114 p++; 115 116 while (*p) 117 *str++ = *p++; 118 119 *str = '\0'; 120 } 121 122 int strscmp(const void *a, const void *b) { 123 return -strcmp((char *)b, (char *)a); 124 } 125 126 void applyListFree(void **ptr, void *cl) { FREE(*ptr); } 127 128 #define T Article_T 129 typedef struct T *T; 130 struct T { 131 char *content; 132 char *output_dir; 133 int content_size; 134 135 int hidden; 136 int nonav; 137 138 FILE *outfile; 139 140 Table_T symbols; 141 List_T categories; 142 }; 143 144 #define AP(...) fprintf(article->outfile, __VA_ARGS__); 145 146 T Article_new(char *output_dir, char *title) { 147 T p; 148 149 NEW0(p); 150 151 if (!title) 152 title = "article"; 153 154 p->output_dir = output_dir; 155 156 p->symbols = Table_new(0, NULL, NULL); 157 p->categories = List_list(NULL); 158 159 Table_put(p->symbols, Atom_string("title"), title); 160 Table_put(p->symbols, Atom_string("date"), "1970-01-01"); 161 Table_put(p->symbols, Atom_string("lang"), "en"); 162 163 return p; 164 } 165 166 void Article_setContent(T self, char *content, int content_size) { 167 self->content = content; 168 self->content_size = content_size; 169 } 170 171 int Article_cmp(const void *a, const void *b) { 172 Article_T a1 = (Article_T)a; 173 Article_T a2 = (Article_T)b; 174 175 int res = strcmp(Table_get(a1->symbols, Atom_string("date")), 176 Table_get(a2->symbols, Atom_string("date"))); 177 if (res) 178 return -res; 179 180 return strcmp(Table_get(a1->symbols, Atom_string("title")), 181 Table_get(a2->symbols, Atom_string("title"))); 182 } 183 184 void Article_openWrite(T self) { 185 char outfile[2 * PATH_MAX]; 186 187 if (!Table_get(self->symbols, Atom_string("filename"))) { 188 Table_put(self->symbols, Atom_string("filename"), 189 Str_dup(Table_get(self->symbols, Atom_string("title")), 1, 0, 1)); 190 } 191 192 char *filename = Table_get(self->symbols, Atom_string("filename")); 193 normalize(filename); 194 sprintf(outfile, "%s/%s.html", self->output_dir, filename); 195 196 if ((self->outfile = fopen(outfile, "w")) == NULL) 197 error(EXIT_FAILURE, errno, "line %d, fopen(%s)", __LINE__, outfile); 198 } 199 200 void Article_closeWrite(T self) { 201 if (self->outfile) 202 fclose(self->outfile); 203 } 204 205 void Article_free(T self) { 206 Table_free(&self->symbols); 207 208 /* List_map(self->categories, applyListFree, NULL); */ 209 List_free(&self->categories); 210 211 FREE(self); 212 } 213 214 void print_category_item(void **item, void *article_pointer) { 215 char *category = *(char **)item, *norm; 216 217 Article_T article = (Article_T)article_pointer; 218 norm = normalize(Str_dup(category, 1, 0, 1)); 219 AP("<a href=\"./%s.html\">%s</a>\n", norm, category); 220 FREE(norm); 221 } 222 223 void print_header(T article) { 224 AP("<!DOCTYPE html>\n" 225 "<html lang=\"%s\">\n" 226 "<head>\n" 227 "<title>%s</title>\n", 228 (char *)Table_get(article->symbols, Atom_string("lang")), 229 (char *)Table_get(article->symbols, Atom_string("title"))); 230 231 AP("<meta charset=\"UTF-8\" />\n" 232 "<meta name=\"viewport\" " 233 "content=\"width=device-width,initial-scale=1\"/>\n" 234 "<meta name=\"description\" content=\"Dimitrije Dobrota's personal site. " 235 "You can find my daily findings in a form of articles on my blog as well " 236 "as various programming projects.\" />\n" 237 "<link rel=\"stylesheet\" type=\"text/css\" href=\"/css/index.css\" />\n" 238 "<link rel=\"stylesheet\" type=\"text/css\" href=\"/css/colors.css\" />\n" 239 240 "<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" " 241 "href=\"/img/favicon-32x32.png\">\n" 242 "<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" " 243 "href=\"/img/favicon-16x16.png\">\n" 244 "</head>\n" 245 "<body>\n" 246 "<input type=\"checkbox\" id=\"theme_switch\" class=\"theme_switch\">\n" 247 "<main>\n" 248 "<div class=\"content\">\n" 249 "<label for=\"theme_switch\" class=\"switch_label\"></label>\n"); 250 251 if (article->nonav) 252 return; 253 254 AP("<div>\n<nav><a class=\"back\"><-- back</a><a " 255 "href=\"" BASE_URL "\">index</a><a href=\"/\">home " 256 "--></a></nav><hr>\n</div>\n"); 257 258 if (List_length(article->categories) > 0) { 259 List_sort(article->categories, strscmp); 260 261 AP("<div class=\"categories\"><h3>Categories:</h3><p>\n"); 262 List_map(article->categories, print_category_item, article); 263 AP("</p></div>\n"); 264 } 265 } 266 267 void print_footer(T article) { 268 if (!article->nonav) { 269 AP("<div class=\"bottom\">\n<hr>\n<nav>\n<a class=\"back\"><-- " 270 "back</a><a href=\"" BASE_URL "\">index</a><a href=\"/\">home " 271 "--></a></nav></div>\n"); 272 } 273 274 AP("</div>\n</main>\n" 275 "<script src=\"/scripts/main.js\"></script>\n" 276 "</body>\n</html>\n"); 277 } 278 279 void print_index_item(void **article_item_pointer, void *article_pointer) { 280 Article_T article_item = *(Article_T *)article_item_pointer; 281 Article_T article = (Article_T)article_pointer; 282 283 AP("<li><div>%s - </div><div><a href=\"%s.html\">%s</a></div></li>\n", 284 (char *)Table_get(article_item->symbols, Atom_string("date")), 285 (char *)Table_get(article_item->symbols, Atom_string("filename")), 286 (char *)Table_get(article_item->symbols, Atom_string("title"))) 287 } 288 289 void print_index(Article_T article, List_T articles, List_T categories) { 290 291 article->categories = categories; 292 Article_openWrite(article); 293 294 print_header(article); 295 { 296 List_sort(articles, Article_cmp); 297 298 AP("<h1>%s</h1>\n", 299 (char *)Table_get(article->symbols, Atom_string("title"))); 300 301 AP("<ul class=\"index\">\n"); 302 List_map(articles, print_index_item, article); 303 AP("</ul>\n"); 304 } 305 print_footer(article); 306 307 Article_closeWrite(article); 308 Article_free(article); 309 } 310 311 void Article_preprocess(T self) { 312 char *text = self->content; 313 314 char *line; 315 for (line = strtok(text, "\n"); line; line = strtok(NULL, "\n")) { 316 strip_whitspace(line); 317 if (!*line) 318 continue; 319 else if (*line == '@') { 320 char *keys, *values; 321 322 keys = CALLOC(1000, sizeof(char)); 323 values = CALLOC(1000, sizeof(char)); 324 325 sscanf(line, " @%[^:]: %[^\n] ", keys, values); 326 327 if (!strcmp(keys, "hidden")) 328 self->hidden = 1; 329 else if (!strcmp(keys, "nonav")) 330 self->nonav = 1; 331 else 332 Table_put(self->symbols, Atom_string(keys), Str_dup(values, 1, 0, 1)); 333 334 FREE(values); 335 FREE(keys); 336 } else { 337 *(line + strlen(line)) = '\n'; 338 text = line; 339 break; 340 } 341 } 342 343 self->content_size = self->content_size - (text - self->content); 344 self->content = text; 345 346 char *cat; 347 if ((cat = (char *)Table_get(self->symbols, Atom_string("categories")))) { 348 char delim[] = ",", *category; 349 for (category = strtok(cat, delim); category; 350 category = strtok(NULL, delim)) { 351 if (strlen(category) > 1) { 352 strip_whitspace(category); 353 354 const char *atom = Atom_string(category); 355 self->categories = List_push(self->categories, category); 356 357 /* append the article to the list of articles for a current category*/ 358 if (!self->hidden) 359 Table_put(category_table, atom, 360 List_push(Table_get(category_table, atom), self)); 361 } 362 } 363 } 364 365 if (!self->hidden) 366 articlesVisible = List_push(articlesVisible, self); 367 } 368 369 void Article_translate(T self) { 370 Article_preprocess(self); 371 Article_openWrite(self); 372 print_header(self); 373 md_html(self->content, self->content_size, process, self->outfile, 374 MD_DIALECT_GITHUB, 0); 375 print_footer(self); 376 Article_closeWrite(self); 377 } 378 379 char *get_date(char *date_str, char **date_buf, char *conversion) { 380 struct tm date; 381 memset(&date, 0, sizeof(date)); 382 383 strptime(date_str, SETTINGS_TIME_FORMAT, &date); 384 strftime(*date_buf, MAX_SIZE, conversion, &date); 385 386 return *date_buf; 387 } 388 389 void print_atom_item(void **article_item_pointer, void *file_pointer) { 390 Article_T article_item = *(Article_T *)article_item_pointer; 391 char *date_buffer = ALLOC(MAX_SIZE); 392 FILE *f = (FILE *)file_pointer; 393 394 get_date((char *)Table_get(article_item->symbols, Atom_string("date")), 395 &date_buffer, ATOM_TIME_FORMAT); 396 fprintf(f, 397 "<entry>\n" 398 " <title>%s</title>\n" 399 " <link href=\"" BASE_URL "/%s.html\"/>\n" 400 " <id>" BASE_URL "/%s.html</id>\n" 401 " <updated>%s</updated>\n" 402 " <summary>Click on the article link to read...</summary>\n" 403 "</entry>\n", 404 (char *)Table_get(article_item->symbols, Atom_string("title")), 405 (char *)Table_get(article_item->symbols, Atom_string("filename")), 406 (char *)Table_get(article_item->symbols, Atom_string("filename")), 407 date_buffer); 408 FREE(date_buffer); 409 } 410 void print_atom(List_T articles, FILE *f) { 411 fprintf(f, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" 412 "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n" 413 "<title>" TITLE "</title>\n" 414 "<link href=\"" BASE_URL "/\"/>\n" 415 "<link rel=\"self\" " 416 "href=\"" ATOM_LOCATION "\" />\n" 417 "<id>" BASE_URL "</id>\n" 418 "<updated>2003-12-13T18:30:02Z</updated>\n" 419 "<author>\n" 420 "<name>" AUTHOR "</name>\n" 421 "</author>\n"); 422 List_map(articles, print_atom_item, f); 423 fprintf(f, "</feed>\n"); 424 } 425 426 void print_rss_item(void **article_item_pointer, void *file_pointer) { 427 Article_T article_item = *(Article_T *)article_item_pointer; 428 char *date_buffer = ALLOC(MAX_SIZE); 429 FILE *f = (FILE *)file_pointer; 430 431 get_date((char *)Table_get(article_item->symbols, Atom_string("date")), 432 &date_buffer, RSS_TIME_FORMAT); 433 fprintf(f, 434 "<item>\n" 435 " <title>%s</title>\n" 436 " <link>" BASE_URL "/%s.html</link>\n" 437 " <guid>" BASE_URL "/%s.html</guid>\n" 438 " <pubDate>%s</pubDate>\n" 439 " <author>" AUTHOR_EMAIL " (" AUTHOR ")</author>\n" 440 "</item>\n", 441 (char *)Table_get(article_item->symbols, Atom_string("title")), 442 (char *)Table_get(article_item->symbols, Atom_string("filename")), 443 (char *)Table_get(article_item->symbols, Atom_string("filename")), 444 date_buffer); 445 FREE(date_buffer); 446 } 447 448 void print_rss(List_T articles, FILE *f) { 449 fprintf(f, 450 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" 451 "<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n" 452 "<channel>\n" 453 "<title>" TITLE "</title>\n" 454 "<link>" BASE_URL "</link>\n" 455 "<description>Contents of Dimitrije Dobrota's webpage</description>" 456 "<generator>stamd</generator>" 457 "<language>en-us</language>\n" 458 "<atom:link href=\"" RSS_LOCATION "\" rel=\"self\" " 459 "type=\"application/rss+xml\" />"); 460 List_map(articles, print_rss_item, f); 461 fprintf(f, "</channel>\n" 462 "</rss>\n"); 463 } 464 465 void print_sitemap_item(void **article_item_pointer, void *file_pointer) { 466 Article_T article_item = *(Article_T *)article_item_pointer; 467 char *date_buffer = ALLOC(MAX_SIZE); 468 FILE *f = (FILE *)file_pointer; 469 470 get_date((char *)Table_get(article_item->symbols, Atom_string("date")), 471 &date_buffer, RSS_TIME_FORMAT); 472 fprintf(f, 473 "<url>\n" 474 " <loc>" BASE_URL "/%s.html</loc>\n" 475 " <changefreq>weekly</changefreq>\n" 476 "</url>\n", 477 (char *)Table_get(article_item->symbols, Atom_string("filename"))); 478 FREE(date_buffer); 479 } 480 481 void print_sitemap(List_T articles, FILE *f) { 482 fprintf(f, 483 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" 484 "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n"); 485 List_map(articles, print_sitemap_item, f); 486 fprintf(f, "</ulrset>\n"); 487 } 488 489 /* void print_xml(char *file_name, void(*)) */ 490 491 int main(int argc, char *argv[]) { 492 char output_dir[PATH_MAX]; 493 int opt; 494 495 while ((opt = getopt(argc, argv, "o:")) != -1) { 496 switch (opt) { 497 case 'o': 498 if (!realpath(optarg, output_dir)) 499 error(EXIT_FAILURE, errno, "-o %s", optarg); 500 break; 501 default: 502 usage(argv[0]); 503 } 504 } 505 506 if (optind >= argc) 507 usage(argv[0]); 508 509 if (!*output_dir) 510 realpath(".", output_dir); 511 512 category_table = Table_new(0, NULL, NULL); 513 514 articles = List_list(NULL); 515 articlesVisible = List_list(NULL); 516 517 for (; optind < argc; optind++) { 518 T article; 519 char *content; 520 int content_size; 521 522 content = memory_open(argv[optind], &content_size); 523 article = Article_new(output_dir, NULL); 524 Article_setContent(article, content, content_size); 525 Article_translate(article); 526 memory_close(content, content_size); 527 528 articles = List_push(articles, article); 529 } 530 531 /* Print main index and index for each encountered category*/ 532 { 533 List_T categories = List_list(NULL); 534 void **array = Table_toArray(category_table, NULL); 535 536 for (int i = 0; array[i]; i += 2) { 537 categories = List_push(categories, array[i]); 538 print_index(Article_new(output_dir, array[i]), array[i + 1], NULL); 539 } 540 541 if (List_length(articlesVisible) > 1) { 542 print_index(Article_new(output_dir, "index"), articlesVisible, 543 categories); 544 545 char outfile[2 * PATH_MAX]; 546 FILE *f; 547 548 sprintf(outfile, "%s/%s", output_dir, ATOM_FILE); 549 f = fopen(outfile, "w"); 550 print_atom(articlesVisible, f); 551 fclose(f); 552 553 sprintf(outfile, "%s/%s", output_dir, RSS_FILE); 554 f = fopen(outfile, "w"); 555 print_rss(articlesVisible, f); 556 fclose(f); 557 558 sprintf(outfile, "%s/%s", output_dir, SITEMAP_FILE); 559 f = fopen(outfile, "w"); 560 print_sitemap(articlesVisible, f); 561 fclose(f); 562 } 563 564 FREE(array); 565 } 566 567 /* Free category table*/ 568 { 569 List_T *symbols = (List_T *)Table_toArray(category_table, NULL); 570 for (int i = 0; symbols[i]; i += 2) 571 List_free(&symbols[i + 1]); 572 FREE(symbols); 573 } 574 575 /* Free articles */ 576 { 577 Article_T *article_list = (Article_T *)List_toArray(articles, NULL); 578 for (int i = 0; article_list[i]; i++) 579 Article_free(article_list[i]); 580 FREE(article_list); 581 } 582 583 Table_free(&category_table); 584 585 List_free(&articles); 586 List_free(&articlesVisible); 587 588 return 0; 589 }