stamd

A static markdown page generator written in C
git clone git://git.dimitrijedobrota.com/stamd.git
Log | Files | Refs | README

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\">&lt;-- back</a><a "
    255      "href=\"" BASE_URL "\">index</a><a href=\"/\">home "
    256      "--&gt;</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\">&lt;-- "
    270        "back</a><a href=\"" BASE_URL "\">index</a><a href=\"/\">home "
    271        "--&gt;</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 }