forb

Forb is static blog generator inspired by jekyll
git clone https://noulin.net/git/forb.git
Log | Files | Refs | README | LICENSE

forb.c (38716B)


      1 #! /usr/bin/env sheepy
      2 #include "libsheepyObject.h"
      3 #include "shpPackages/short/short.h"
      4 #include "shpPackages/preprocessor/preprocessor.h"
      5 #include "shpPackages/simpleTemplates/simpleTemplates.h"
      6 #include "forb.h"
      7 
      8 // forb new: copy templates to current directory
      9 // forb:     generate static blog in the _site directory
     10 
     11 // Steps
     12 // read arguments
     13 // check inputs
     14 // load config
     15 // copy main css
     16 // copy images
     17 // generate index
     18   // read posts
     19   // generate html code for each post for index.html
     20   // create configuation for index
     21 // generate about
     22 // generate posts
     23 // generate feed.xml
     24 // clean tmp files
     25 
     26 #define configFile "_config.yml"
     27 #define indexFile "index.html"
     28 #define layoutDir "_layouts"
     29 #define draftsDir "_drafts"
     30 #define PostsDir "_posts"
     31 #define publishedDir "_published"
     32 #define siteDir "_site"
     33 #define mainCss "css/main.css"
     34 #define imagesDir "images"
     35 #define content "{{ content }}"
     36 
     37 /* enable/disable logging */
     38 /* #undef pLog */
     39 /* #define pLog(...) */
     40 
     41 void help(void);
     42 
     43 void publish(const char *path);
     44 
     45 void update(const char *path);
     46 
     47 bool readFileWithFrontMatter(char *filename, smallJsont *kv, smallArrayt *lines);
     48 
     49 bool generateAPageOrAPost(char *filename, smallJsont *cfg, smallArrayt *postsFeed);
     50 
     51 int main(int ARGC, char** ARGV) {
     52 
     53   initLibsheepy(ARGV[0]);
     54   setLogMode(LOG_FUNC);
     55   //openProgLogFile();
     56   setLogSymbols(LOG_UTF8);
     57   //disableLibsheepyErrorLogs;
     58 
     59   // steps
     60   // process command line arguments
     61   // arg new
     62   //   create directory structure when config file is not found
     63   //   else create a draft
     64   // arg help
     65   //   print help
     66   // arg publish
     67   //   copy draft to post dir, the date field is updated
     68   // arg update
     69   //   copy published post to post dir, the date field is updated
     70   // when there are no arguments, generate the blog
     71   // check inputs
     72   // load config
     73   // copy main css
     74   // copy images
     75   // generate index
     76   //   read posts
     77   //   list all md files in _posts and _published
     78   //   generate html code for each post for index.html
     79   //   create configuation for index
     80   // save index
     81   // generate about page
     82   // generate posts
     83   //   collect html code for 10 last post for feed.xml
     84   // generate feed.xml
     85   // clean tmp files
     86   // list all md files in _posts
     87   // move posts from _posts to _published
     88   // add first publish date if needed
     89 
     90   // read arguments
     91   #define checkInput(path) procbegin\
     92     if (not isPath(path)) {\
     93       logE(BLD"%s not found."RST"\n"\
     94           , path);\
     95       help();\
     96       XFailure;\
     97     }\
     98     procend
     99 
    100   if (ARGC > 1) {
    101     if (eqG(ARGV[1], "new") and not isPath(configFile)) {
    102       // create directory structure when config file is not found
    103       bool a;
    104       if (a = isPath("about.md")) {
    105         if (a) logE("about.md already exists.");
    106         logE("Stop.");
    107         XFailure;
    108       }
    109       cleanCharP(progPath) = shDirname(getRealProgPath());
    110       logCommandf("cp -R %s/template/* .", progPath);
    111       XSuccess;
    112     }
    113     elif (eqG(ARGV[1], "new")) {
    114       // create a draft
    115       checkInput(draftsDir);
    116       if (ARGC < 3) {
    117         logE(BLD"Too few arguments"RST", post title missing.\n"
    118              "forb new "BLD"TITLE"RST"\n"
    119             );
    120         XFailure;
    121       }
    122       // create post title and filename
    123       cleanListP(args) = dupG(ARGV);
    124       delElemG(&args, 1);
    125       delElemG(&args, 0);
    126       cleanCharP(title) = joinG(args, ' ');
    127       uniqG(&title, ' ');
    128       //lv(title);
    129       cleanCharP(draftName) = dupG(title);
    130       char *tmp = draftName;
    131       while(*tmp++) {
    132         if (*tmp and not isalnum(*tmp)) {
    133           *tmp = '-';
    134         }
    135       }
    136       lowerG(&draftName);
    137       uniqG(&draftName, '-');
    138       prependG(&draftName, draftsDir"/");
    139       pushG(&draftName, ".markdown");
    140       //lv(draftName);
    141       cleanCharP(defaultDraft) = replaceG(draftTemplate, "$TITLE", title, 1);
    142       writeFileG(defaultDraft, draftName);
    143       logP(BLD GRN"Generated %s\n"RST
    144           "Open %s in your text editor and write your post.", draftName, draftName);
    145       XSuccess;
    146     }
    147     elif (eqG(ARGV[1], "-h") or eqG(ARGV[1], "--help") or eqG(ARGV[1], "help")) {
    148       help();
    149       XSuccess;
    150     }
    151     elif (eqG(ARGV[1], "publish") or eqG(ARGV[1], "post")) {
    152       // copy draft to post dir
    153       if (ARGC < 3) {
    154         logE("publish failed. Missing path, usage:\n"
    155              "forb publish PATH"
    156              );
    157         XFailure;
    158       }
    159       rangeFrom(i, 2, ARGC) {
    160         if (not isPath(ARGV[i])) {
    161           logE("Path %d '%s' not found", i, ARGV[i]);
    162         }
    163         else {
    164           checkInput(draftsDir);
    165           checkInput(PostsDir);
    166           publish(ARGV[i]);
    167         }
    168       }
    169       XSuccess;
    170     }
    171     elif (eqG(ARGV[1], "update")) {
    172       // copy published post to post dir, the date field is updated
    173       if (ARGC < 3) {
    174         logE("update failed. Missing path, usage:\n"
    175              "forb update PATH"
    176              );
    177         XFailure;
    178       }
    179       rangeFrom(i, 2, ARGC) {
    180         if (not isPath(ARGV[i])) {
    181           logE("Path %d '%s' not found", i, ARGV[i]);
    182         }
    183         else {
    184           checkInput(publishedDir);
    185           checkInput(PostsDir);
    186           update(ARGV[i]);
    187         }
    188       }
    189       XSuccess;
    190     }
    191     else {
    192       logE("Command "BLD"'%s'"RST" not found or invalid parameters.\n\n"
    193            , ARGV[1]);
    194       help();
    195       XFailure;
    196     }
    197   }
    198 
    199   // when there are no arguments, generate the blog
    200 
    201   // check inputs
    202   checkInput(configFile);
    203   checkInput(indexFile);
    204   checkInput(layoutDir);
    205 
    206   // load config
    207   cleanAllocateSmallJson(cfg);
    208   readFileG(cfg, configFile);
    209 
    210   if (not isPath(siteDir)) {
    211     mkdirParents(siteDir);
    212   }
    213 
    214   // copy main css
    215   if (not isPath(siteDir"/css")) {
    216     mkdirParents(siteDir"/css");
    217   }
    218   copy(mainCss, siteDir"/"mainCss);
    219 
    220   // copy images
    221   if (not isPath(siteDir"/"imagesDir)) {
    222     mkdirParents(siteDir"/"imagesDir);
    223   }
    224   command("cp "imagesDir"/* "siteDir"/"imagesDir"/");
    225 
    226 
    227   // generate index
    228   cleanAllocateSmallArray(indexf);
    229   cleanAllocateSmallJson(indexCfg);
    230   readFileWithFrontMatter(indexFile, indexCfg, indexf);
    231 
    232     // read posts
    233     // list all md files in _posts and _published
    234   cleanSmallArrayP(postsDir) = readDirG(rtSmallArrayt, PostsDir);
    235   iter(postsDir, L) {
    236     castS(l,L);
    237     if (not endsWithG(l, ".markdown")) {
    238       delElemG(postsDir, iterIndexG(postsDir));
    239     }
    240   }
    241   //lv(postsDir);
    242   cleanSmallArrayP(pubDir)   = readDirG(rtSmallArrayt, publishedDir);
    243   iter(pubDir, L) {
    244     castS(l,L);
    245     if (not endsWithG(l, ".markdown")) {
    246       delElemG(pubDir, iI(pubDir));
    247     }
    248   }
    249   appendNFreeG(postsDir, dupG(pubDir));
    250   compactG(postsDir);
    251   sortG(postsDir);
    252 
    253     // generate html code for each post for index.html
    254   cleanAllocateSmallArray(postsIndex);
    255   iterLast(postsDir, L) {
    256     castS(l,L);
    257     cleanCharP(postHtmlFile) = copyRngG(ssGet(l), 11, 0);
    258     replaceG(&postHtmlFile, ".markdown", ".html", 1);
    259     // determine if file is in _posts or _published
    260     if (hasG(pubDir, l)) {
    261       // file is in _published
    262       prependG(l, publishedDir"/");
    263     }
    264     else {
    265       prependG(l, PostsDir"/");
    266     }
    267     setPG(postsDir, iI(postsDir), l);
    268     cleanAllocateSmallJson(postCfg);
    269     cleanAllocateSmallArray(postf);
    270     readFileWithFrontMatter(ssGet(l), postCfg, postf);
    271     // select first publish date for html path: root/category/YY/MM/DD/postName.html
    272     cleanCharP(postDate) = hasG(postCfg, "firstPublishDate") ? copyRngG($(postCfg, "firstPublishDate"), 0, 10) : copyRngG($(postCfg, "date"), 0, 10);
    273     replaceG(&postDate, "-", "/", 0);
    274     cleanSmallArrayP(postUrlA) = createSA($(cfg, "baseurl"),
    275         $(postCfg, "categories"), postDate, postHtmlFile);
    276     cleanCharP(postUrl) = joinSG(postUrlA, "/");
    277     // char *date is the visible date string: 'date' when the post is published for the first time, 'firstPublishDate update on date' when the post has been updated
    278     cleanCharP(date);
    279     if (hasG(postCfg, "firstPublishDate")) {
    280       if (eqG($(postCfg, "date"), $(postCfg, "firstPublishDate"))) goto onlyPublishDate;
    281       // there is an update date
    282       $(postCfg, "date")[10] = 0;
    283       $(postCfg, "firstPublishDate")[10] = 0;
    284       date = catS($(postCfg, "firstPublishDate"), " updated on ", $(postCfg, "date"));
    285     }
    286     else {
    287       onlyPublishDate:
    288       $(postCfg, "date")[10] = 0;
    289       date = dupG($(postCfg, "date"));
    290     }
    291     cleanCharP(s) = formatS("<li>\n"
    292                       "  <span class=\"post-meta\">%s</span>\n"
    293                       "\n"
    294                       "  <h2>\n"
    295                       "    <a class=\"post-link\" href=\"%s\">%s</a>\n"
    296                       "  </h2>\n"
    297                       "</li>\n"
    298                       ,date, postUrl, $(postCfg, "title"));
    299     pushG(postsIndex, s);
    300   }
    301   //cleanCharP(postsIndexS) = joinSG(postsIndex, "\n");
    302   //lv(postsIndexS);
    303 
    304   iter(indexf, L) {
    305     castS(l,L);
    306     if (hasG(l, "{{ posts }}")) {
    307       delElemG(indexf, iterIndexG(indexf));
    308       insertNFreeG(indexf, iterIndexG(indexf), dupG(postsIndex));
    309       break;
    310     }
    311   }
    312 
    313   cleanCharP(layoutFile) = catS(layoutDir,"/",$(indexCfg, "layout"),".html");
    314 
    315   //lv(layoutFile);
    316 
    317   var index = preprocess(layoutFile);
    318 
    319   iter(index, L) {
    320     castS(l,L);
    321     if (hasG(l, content)) {
    322       delElemG(index, iterIndexG(index));
    323       insertNFreeG(index, iterIndexG(index), dupG(indexf));
    324       break;
    325     }
    326   }
    327 
    328   //logG(index);
    329 
    330     // create configuation for index
    331 
    332   /* // TODO list all md files in dir (the pages) */
    333   /* cleanSmallArrayP(pages) = readDirG(rtSmallArrayt, "."); */
    334   /* iter(pages, L) { */
    335   /*   castS(l,L); */
    336   /*   if (not endsWithG(l, ".md")) { */
    337   /*     delElemG(pages, iterIndexG(pages)); */
    338   /*   } */
    339   /* } */
    340   /* lv(pages); */
    341 
    342   cleanAllocateSmallArray(about);
    343   cleanAllocateSmallJson(aboutCfg);
    344   readFileWithFrontMatter("about.md", aboutCfg, about);
    345 
    346     // page list on first line on all pages
    347   char *sitePages = catS("<a class=\"page-link\" href=\"", $(cfg, "baseurl"), $(aboutCfg, "permalink"), "\">", $(aboutCfg, "title"), "</a>");
    348 
    349   cleanAllocateSmallDict(pageValues);
    350 
    351   setNFreeG(pageValues, "_\%title", getNDupO(cfg, "title"));
    352   setNFreeG(pageValues, "_\%description", getNDupO(cfg, "description"));
    353   setNFreeG(pageValues, "_\%site.description", getNDupO(cfg, "description"));
    354   setNFreeG(pageValues, "_\%css", catS($(cfg, "baseurl"), "/css/main.css"));
    355   setNFreeG(pageValues, "_\%url", catS($(cfg, "url"), $(cfg, "baseurl")));
    356   setNFreeG(pageValues, "_\%site.title", getNDupO(cfg, "title"));
    357   setNFreeG(pageValues, "_\%feed", catS($(cfg, "url"), $(cfg, "baseurl"), "/feed.xml"));
    358   setNFreeG(pageValues, "_\%baseurl", getNDupO(cfg, "baseurl"));
    359   setNFreeG(pageValues, "_\%site.pages", sitePages);
    360   setNFreeG(pageValues, "_\%site.email", getNDupO(cfg, "email"));
    361   setNFreeG(pageValues, "_\%site.github_username", getNDupO(cfg, "github_username"));
    362   setNFreeG(pageValues, "_\%site.twitter_username", getNDupO(cfg, "twitter_username"));
    363 
    364   //lv(pageValues);
    365 
    366   simpleTemplatesReplaceKeysWithValues(index, pageValues);
    367 
    368   //logG(index);
    369 
    370   // save index
    371   writeFileG(index, siteDir"/index.html");
    372 
    373 
    374   // generate about page
    375   generateAPageOrAPost("about.md", cfg, null/*postsFeed*/);
    376 
    377   // generate posts
    378   cleanAllocateSmallArray(postsFeed);
    379 
    380   int count = 0;
    381   iterLast(postsDir, L) {
    382     castS(l,L);
    383     // collect html code for 10 last post for feed.xml
    384     generateAPageOrAPost(ssGet(l), cfg, count++ < 10 ? postsFeed : null);
    385   }
    386 
    387   // generate feed.xml
    388   cleanAllocateSmallArray(feed);
    389   cleanAllocateSmallJson(feedCfg);
    390   readFileWithFrontMatter("feed.xml", feedCfg, feed);
    391 
    392   //lv(feedCfg);
    393   //lv(feed);
    394 
    395   iter(feed, L) {
    396     castS(l,L);
    397     if (hasG(l, "{{ posts }}")) {
    398       delElemG(feed, iterIndexG(feed));
    399       insertNFreeG(feed, iterIndexG(feed), dupG(postsFeed));
    400       break;
    401     }
    402   }
    403 
    404   setNFreeG(pageValues, "_\%date", getCurrentDate());
    405   simpleTemplatesReplaceKeysWithValues(feed, pageValues);
    406 
    407   writeFileG(feed, siteDir"/feed.xml");
    408 
    409   // clean tmp files
    410   if (isPath("tmp.html")) rmAll("tmp.html");
    411   if (isPath("tmp.md"))   rmAll("tmp.md");
    412 
    413 
    414   // list all md files in _posts
    415   cleanSmallArrayP(postFiles) = readDirG(rtSmallArrayt, PostsDir);
    416   iter(postFiles, L) {
    417     castS(l,L);
    418     if (not endsWithG(l, ".markdown")) {
    419       delElemG(postFiles, iterIndexG(postFiles));
    420     }
    421   }
    422   //lv(postFiles);
    423 
    424   // move posts from _posts to _published
    425   // add first publish date if needed
    426   iter(postFiles, L) {
    427     castS(p,L);
    428     cleanCharP(postPath) = catS(PostsDir"/", ssGet(p));
    429     cleanAllocateSmallArray(post);
    430     readFileG(post, postPath);
    431 
    432     char *status = "search front matter";
    433     i32 dateIdx  = -1;
    434     iter(post, L) {
    435       castS(l,L);
    436       if (not eqG(status, "search front matter") and startsWithG(l, "firstPublishDate:"))
    437         // found first publish date. Stop.
    438         break;
    439       if (not eqG(status, "search front matter") and eqG(l, "---")) {
    440         if (dateIdx == -1) {
    441           logE("Date not found in front matter, post is %m", p);
    442           break;
    443         }
    444         cleanCharP(pDate) = replaceG($(post, dateIdx), "date:", "firstPublishDate:", 1);
    445         injectG(post, iI(post), pDate);
    446         break;
    447       }
    448       if (eqG(status, "date") and startsWithG(l, "date:")) {
    449         dateIdx = iI(post);
    450         status = "publishDate";
    451       }
    452       if (eqG(status, "search front matter") and eqG(l, "---")) {
    453         status = "date";
    454       }
    455     }
    456 
    457     // move post to _published directory
    458     cleanCharP(pubPath)  = catS(publishedDir"/", ssGet(p));
    459     if (not writeFileG(post, pubPath)) {
    460       logE("Could not write '%s', post is: %m", pubPath, p);
    461       continue;
    462     }
    463     if (not rmAllG(postPath)) {
    464       logE("Could not remove: %s", postPath);
    465       continue;
    466     }
    467   }
    468 }
    469 
    470 void help(void) {
    471   logI(BLD GRN"Forb help\n"RST
    472       "Argument convention: the arguments are words\n\n"
    473       BLD YLW"\nFORB SUBCOMMANDS:\n\n"RST
    474       "  help, -h, --help --- print this help message\n"
    475       "  without argument --- generate the blog in the "siteDir" directory, the posts in "PostsDir" are moved to "publishedDir"\n"
    476       "  new [title]      --- copy default template or when running in a forb directory, create a draft post\n"
    477       "  publish PATH     --- publish a draft, the draft file is moved from the "draftsDir" directory to the "PostsDir" directory and today's date is added to the file name and in the post\n"
    478       "  post PATH        --- publish a draft, the draft file is moved from the "draftsDir" directory to the "PostsDir" directory and today's date is added to the file name and in the post\n"
    479       "  update PATH      --- move post in "publishedDir" to "PostsDir", edit the post and generate the blog. The update date is added to the post and the first publish date is kept\n"
    480       );
    481 }
    482 
    483 
    484 /**
    485  * publish a post in _drafts to _posts
    486  */
    487 void publish(const char *path) {
    488   // steps
    489   // check extension
    490   // read post
    491   // create date string for filename
    492   // create destination filename
    493   // create date string for front matter
    494   // add date string in front matter
    495   // failed to find front matter or categories
    496   // save posts directory
    497   // remove path
    498 
    499   // check extension
    500   if (not endsWithG(path, ".markdown")) {
    501     logE("'%s' doesn't have the markdown extension. Stop.", path);
    502     XFailure;
    503   }
    504 
    505   // read post
    506   cleanAllocateSmallArray(post);
    507   readFileG(post, path);
    508 
    509   // create date string for filename
    510   cleanCharP(date) = getCurrentDateYMD();
    511   date[10] = 0;
    512   //lv(date);
    513   //lv(path);
    514 
    515   // create destination filename
    516   cleanCharP(dest) = catS(PostsDir"/", date, "-", basename(path));
    517   //lv(dest);
    518 
    519   // create date string for front matter
    520   date[10] = ' ';
    521   prependG(&date, "date: ");
    522   //lv(date);
    523 
    524   // add date string in front matter
    525   char *status = "search front matter";
    526   iter(post, L) {
    527     castS(l, L);
    528     if (eqG(status, "stop at categories") and startsWithG(l, "categories:")) {
    529       injectG(post, iI(post), date);
    530       goto publishPost;
    531     }
    532     if (eqG(status, "stop at categories") and eqG(l, "---")) {
    533       logE("categories not found in front matter, path is: %s", path);
    534       ret;
    535     }
    536     if (eqG(status, "search front matter") and eqG(l, "---")) {
    537       status = "stop at categories";
    538     }
    539   }
    540 
    541   // failed to find front matter or categories
    542   logE("front matter or categories not found, path is %s", path);
    543   ret;
    544 
    545   publishPost:
    546   // save posts directory
    547   if (not writeFileG(post, dest)) {
    548     logE("Could not write '%s', path is: %s", dest, path);
    549     ret;
    550   }
    551 
    552   // remove path
    553   if (not rmAllG(path)) {
    554     logE("Could not remove: %s", path);
    555   }
    556 
    557   logP("Published draft post to %s", dest);
    558 }
    559 
    560 
    561 /**
    562  * update a post in _published to _posts
    563  */
    564 void update(const char *path) {
    565   // steps
    566   // check extension
    567   // read post
    568   // create destination filename
    569   // create date string for front matter
    570   // change date string in front matter
    571   // failed to find front matter or date
    572   // save posts directory
    573   // remove path
    574 
    575   // check extension
    576   if (not endsWithG(path, ".markdown")) {
    577     logE("'%s' doesn't have the markdown extension. Stop.");
    578     XFailure;
    579   }
    580 
    581   // read post
    582   cleanAllocateSmallArray(post);
    583   readFileG(post, path);
    584 
    585   // create destination filename
    586   cleanCharP(dest) = catS(PostsDir"/", basename(path));
    587   //lv(dest);
    588 
    589   // create date string for front matter
    590   cleanCharP(date) = getCurrentDateYMD();
    591   date[10] = ' ';
    592   prependG(&date, "date: ");
    593   //lv(date);
    594 
    595   // change date string in front matter
    596   char *status = "search front matter";
    597   iter(post, L) {
    598     castS(l, L);
    599     if (eqG(status, "stop at date") and startsWithG(l, "date:")) {
    600       setG(post, iI(post), date);
    601       goto publishPost;
    602     }
    603     if (eqG(status, "stop at date") and eqG(l, "---")) {
    604       logE("date not found in front matter, path is: %s", path);
    605       ret;
    606     }
    607     if (eqG(status, "search front matter") and eqG(l, "---")) {
    608       status = "stop at date";
    609     }
    610   }
    611 
    612   // failed to find front matter or date
    613   logE("front matter or date not found, path is %s", path);
    614   ret;
    615 
    616   publishPost:
    617   // save posts directory
    618   if (not writeFileG(post, dest)) {
    619     logE("Could not write '%s', path is: %s", dest, path);
    620     ret;
    621   }
    622 
    623   // remove path
    624   if (not rmAllG(path)) {
    625     logE("Could not remove: %s", path);
    626   }
    627 
    628   logP("Update already published post in %s", dest);
    629 }
    630 
    631 
    632 /**
    633  * read file filename with front matter and text (any type of text)
    634  *
    635  * \return
    636  *   kv: keys and values for yml code in front matter
    637  *   lines: lines in file filename without front matter
    638  */
    639 bool readFileWithFrontMatter(char *filename, smallJsont *kv, smallArrayt *lines) {
    640   int frontMatterStart = -1, frontMatterEnd = -1;
    641 
    642   // Steps
    643   // find front matter start and end
    644   // parse yml code in front matter
    645   // remove front matter from lines
    646 
    647   if (not isPath(filename)) {
    648     logE("File '%s' not found.", filename);
    649     ret no;
    650   }
    651 
    652   readFileG(lines, filename);
    653 
    654   // find front matter start and end
    655   char *status = "start";
    656   iter(lines, L) {
    657     castS(l,L);
    658     if (eqS(status, "end") and (startsWithG(l, "---"))) {
    659       frontMatterEnd = iterIndexG(lines);
    660       break;
    661     }
    662     if (eqS(status, "start") and (startsWithG(l, "---"))) {
    663       frontMatterStart = iterIndexG(lines)+1;
    664       status = "end";
    665     }
    666   }
    667 
    668   if (frontMatterStart == -1 or frontMatterEnd == -1) {
    669     logE("front matter not found in '%s'", filename);
    670     ret no;
    671   }
    672 
    673   // parse yml code in front matter
    674   var frontMatter = copyRngG(lines, frontMatterStart, frontMatterEnd);
    675 
    676   //lv(frontMatter);
    677 
    678   // TODO change to: parseYMLG(indexCfg, frontMatter);
    679   // when parseYMLG accepts smallArrays
    680   cleanCharP(fm) = joinSG(frontMatter, "\n");
    681   parseYMLG(kv, fm);
    682 
    683   //lv(indexCfg);
    684   finishG(frontMatter);
    685 
    686   // remove front matter from lines
    687   sliceG(lines, frontMatterEnd+1, 0);
    688   //lv(lines);
    689 
    690   ret yes;
    691 }
    692 
    693 /**
    694  * convert post filename ("2020-06-09-getting-started" without markdown)
    695  * to url for linking posts easily in the blog.
    696  *
    697  * The url format is "/category/YYYY/MM/DD/filename.html"
    698  */
    699 char *postToUrl(char *filename) {
    700   cleanCharP(fn) = catS(PostsDir"/",filename,".markdown");
    701   if (not isPath(fn)) {
    702     free(fn);
    703     fn = catS(publishedDir"/",filename,".markdown");
    704   }
    705 
    706   cleanAllocateSmallArray(pf);
    707   cleanAllocateSmallJson(pCfg);
    708   readFileWithFrontMatter(fn, pCfg, pf);
    709 
    710   cleanCharP(postHtmlFile) = copyRngG(fn, 22, 0);
    711   replaceG(&postHtmlFile, ".markdown", ".html", 1);
    712 
    713   // select first publish date for html path: root/category/YY/MM/DD/postName.html
    714   cleanCharP(postDate) = hasG(pCfg, "firstPublishDate") ? copyRngG($(pCfg, "firstPublishDate"), 0, 10) : copyRngG($(pCfg, "date"), 0, 10);
    715   replaceG(&postDate, "-", "/", 0);
    716   char *pUrl = catS("/",$(pCfg,"categories"),"/",postDate,"/",postHtmlFile);
    717 
    718   ret pUrl;
    719 }
    720 
    721 
    722 bool generateAPageOrAPost(char *filename, smallJsont *cfg, smallArrayt *postsFeed) {
    723 
    724   // Steps
    725   // check if filename exists
    726   // parse front matter yml
    727   // convert markdown to html
    728   // add class in <code>
    729   // process {% post_url 2015-12-22-test %}
    730   // add syntax highlighting
    731   // insert html in layout template
    732   // add layout and html code in sub layout
    733   // TODO list pages on first line of all pages
    734   // configure destination path, post url, date and description
    735   // detect if generating a post or a page to select path and parameters
    736   // replace keys with values
    737   // generate feed
    738   // write post html page
    739 
    740   // check if filename exists
    741   if (not isPath(filename)) {
    742     logE("%s not found", filename);
    743     ret false;
    744   }
    745 
    746   // parse front matter yml
    747   cleanAllocateSmallArray(pf);
    748   cleanAllocateSmallJson(pCfg);
    749   readFileWithFrontMatter(filename, pCfg, pf);
    750 
    751   //lv(filename);
    752 
    753   // process {% post_url 2015-12-22-test %}
    754   // skip converting to URL in code blocks
    755   enum {searchcodeblock, incodeblock};
    756   int mdstatus = searchcodeblock;
    757   iter(pf, L) {
    758     castS(l,L);
    759     // process {% post_url 2015-12-22-running-rocket-chat %}
    760     // only outside code block, keep inside code blocks
    761     // (keep this block after code highlighting to get the correct status from the first line
    762     // in the code block)
    763     if (mdstatus equals searchcodeblock and hasG(l, "{\% post_url ")) {
    764       // convert all % post_url to valid url on the line
    765       char *cursor       = hasG(l, "{\% post_url ");
    766       while (cursor) {
    767         char *startp       = cursor;
    768         char *endp         = findS(cursor, " \%}");
    769         int start          = startp - ssGet(l);
    770         int end            = endp   - ssGet(l);
    771         // post filename
    772         char *postFilemane = startp + strlen("{\% post_url ");
    773         *endp              = 0;
    774         //lv(postFilemane);
    775         cleanCharP(url)    = postToUrl(postFilemane);
    776         delG(l, start, end+3);
    777         insertG(l, start, url);
    778         cursor             = findS(ssGet(l)+start, "{\% post_url ");
    779       }
    780       //lv(l);
    781       setPG(pf, iterIndexG(pf), l);
    782     }
    783     if (startsWithG(l, "```"))
    784       mdstatus is mdstatus equals searchcodeblock ? incodeblock : searchcodeblock;
    785   }
    786   //lv(pf);
    787 
    788   // convert markdown to html
    789   writeFileG(pf, "tmp.md");
    790 
    791   // TODO get path once and reuse
    792   cleanCharP(progPath) = shDirname(getRealProgPath());
    793   commandf("%s/shpPackages/md2html/md2html --full-html --ftables --fstrikethrough tmp.md --output=tmp.html", progPath);
    794 
    795   cleanAllocateSmallArray(pHtml);
    796   readFileG(pHtml, "tmp.html");
    797 
    798     // keep only html code between the body tags
    799   int bodyStart = -1, bodyEnd = -1;
    800   iter(pHtml, L) {
    801     castS(l,L);
    802     if (hasG(l, "<body>"))  bodyStart = iterIndexG(pHtml)+1;
    803     if (hasG(l, "</body>")) {
    804       bodyEnd   = iterIndexG(pHtml);
    805       break;
    806     }
    807   }
    808   sliceG(pHtml, bodyStart, bodyEnd);
    809 
    810   // add class in <code>
    811   // add syntax highlighting
    812   enum {searchCode, notspecified, bash, javascript, python, html, coffeescript};
    813   int status = searchCode;
    814   int lastCodeHighlightingLine = -1;
    815   iter(pHtml, L) {
    816     castS(l,L);
    817     // code highlighting
    818     if (status == searchCode and hasG(l, "<code>")) {
    819       if (hasG(l, "<pre><code>")) {
    820         replaceG(l, "<pre><code>", "<figure class=\"highlight\"><pre><code class=\"highlighter-rouge\">", 0);
    821         status = notspecified;
    822       }
    823       else {
    824         replaceG(l, "<code>", "<code class=\"highlighter-rouge\">", 0);
    825       }
    826     }
    827     if (status == searchCode and hasG(l, "<pre><code class=\"language-")) {
    828       lastCodeHighlightingLine = iterIndexG(pHtml);
    829     }
    830     if (status == searchCode and hasG(l, "<pre><code class=\"language-bash\">")) {
    831       status = bash;
    832     }
    833     if (status == searchCode and hasG(l, "<pre><code class=\"language-javascript\">")) {
    834       status = javascript;
    835     }
    836     if (status == searchCode and hasG(l, "<pre><code class=\"language-python\">")) {
    837       status = python;
    838     }
    839     if (status == searchCode and hasG(l, "<pre><code class=\"language-html\">")) {
    840       status = html;
    841     }
    842     if (status == searchCode and hasG(l, "<pre><code class=\"language-coffeescript\">")) {
    843       status = coffeescript;
    844     }
    845     if (hasG(l, "</code></pre>")) {
    846       if (status == searchCode) {
    847         if (lastCodeHighlightingLine == -1) {
    848           logW("line %d in html for "BLD"%s"RST", code highlighting not found. This code block doesn't have highlighting.", iterIndexG(pHtml), &filename[18]);
    849         }
    850         else {
    851           logW("line %d in html for "BLD"%s"RST", code highlighting not recognize: '%s'. This code block doesn't have highlighting.", iterIndexG(pHtml), &filename[18], $(pHtml, lastCodeHighlightingLine));
    852         }
    853       }
    854       else replaceG(l, "</code></pre>", "</code></pre></figure>", 0);
    855       status = searchCode;
    856     }
    857 
    858     // bash highlight
    859     if (status == bash) {
    860       replaceManyG(l, "<pre><code class=\"language-bash\">", "<figure class=\"highlight\"><pre><code class=\"language-bash\" data-lang=\"bash\">",
    861           "cd ", "<span class=\"nb\">cd </span>",
    862           "echo ", "<span class=\"nb\">echo </span>",
    863           "set ", "<span class=\"nb\">set </span>");
    864       // detect comment
    865       if (hasG(l, "#")) {
    866         replaceG(l, "#", "<span class=\"c\">#", 1);
    867         pushG(l, "</span>");
    868       }
    869       // strings
    870       if (not hasG(l, "<figure class=\"highlight\"><pre><code class=\"language-")) {
    871         var c = countG(l, '\'');
    872         if (c != 0 and (c & 1) == 0) {
    873           #define highlightQuotes(q, qS, qE) procbegin\
    874             var len = lenG(l);\
    875             var qL  = lenG(q);\
    876             cleanCharP(str) = malloc(len + ((strlen(qS) + strlen(qE)) * c/2) + 1);\
    877             char *ps  = str;\
    878             char *ln  = ssGet(l);\
    879             enum {qSearch, q1};\
    880             int status = qSearch;\
    881             range(i, len) {\
    882               if (eqIS(ln, q, i)) {\
    883                 if (status == qSearch) {\
    884                   status = q1;\
    885                   strcpy(ps, qS);\
    886                   ps += strlen(qS);\
    887                   *ps++ = ln[i];\
    888                 }\
    889                 elif (status == q1) {\
    890                   status = qSearch;\
    891                   rangeFrom(n, i, i+qL) {\
    892                     *ps++ = ln[n];\
    893                   }\
    894                   i += qL;\
    895                   strcpy(ps, qE);\
    896                   ps += strlen(qE);\
    897                 }\
    898               }\
    899               else {\
    900                 *ps++ = ln[i];\
    901               }\
    902             }\
    903             *ps = 0;\
    904             setValG(l, str);\
    905             procend
    906           highlightQuotes("'", "<span class=\"s1\">", "</span>");
    907         }
    908       }
    909     }
    910 
    911     // javascript highlight
    912     elif (status == javascript) {
    913       replaceManyG(l, "<pre><code class=\"language-javascript\">", "<figure class=\"highlight\"><pre><code class=\"language-javascript\" data-lang=\"javascript\">",
    914                       "*", "<span class=\"o\">*</span>",
    915                       "var", "<span class=\"kd\">var</span>",
    916                       "function", "<span class=\"kd\">function</span>",
    917                       "return", "<span class=\"kd\">return</span>",
    918                       "delete", "<span class=\"kd\">delete</span>",
    919                       "new", "<span class=\"kd\">new</span>",
    920                       "this", "<span class=\"kd\">this</span>",
    921                       "true", "<span class=\"kd\">true</span>",
    922                       "false", "<span class=\"kd\">false</span>",
    923                       "export", "<span class=\"kd\">export</span>",
    924                       "Object", "<span class=\"nb\">Object</span>"
    925                       );
    926       // detect comment
    927       if (hasG(l, "//") and not hasG(l, "://")) {
    928         replaceG(l, "//", "<span class=\"c\">//", 1);
    929         pushG(l, "</span>");
    930       }
    931       // strings
    932       if (not hasG(l, "<figure class=\"highlight\"><pre><code class=\"language-")) {
    933         var c = countG(l, '\'');
    934         if (c != 0 and (c & 1) == 0) {
    935           highlightQuotes("'", "<span class=\"s1\">", "</span>");
    936         }
    937       }
    938     }
    939 
    940     // python highlighting
    941     elif (status == python) {
    942       replaceManyG(l, "<pre><code class=\"language-python\">", "<figure class=\"highlight\"><pre><code class=\"language-python\" data-lang=\"python\">",
    943                       "*", "<span class=\"o\">*</span>",
    944                       "for", "<span class=\"kd\">for</span>",
    945                       "print", "<span class=\"kd\">print</span>",
    946                       "def", "<span class=\"kd\">def</span>",
    947                       "return", "<span class=\"kd\">return</span>",
    948                       "del", "<span class=\"kd\">del</span>",
    949                       "dict", "<span class=\"nb\">dict</span>",
    950                       "range", "<span class=\"nb\">range</span>",
    951                       "zip", "<span class=\"nb\">zip</span>"
    952                       );
    953       // detect comment
    954       if (hasG(l, "#")) {
    955         replaceG(l, "#", "<span class=\"c\">#", 1);
    956         pushG(l, "</span>");
    957       }
    958       // strings
    959       if (not hasG(l, "<figure class=\"highlight\"><pre><code class=\"language-")) {
    960         var c = countG(l, '\'');
    961         if (c != 0 and (c & 1) == 0) {
    962           highlightQuotes("'", "<span class=\"s\">", "</span>");
    963         }
    964       }
    965     }
    966 
    967     // html highlight
    968     elif (status == html) {
    969       // removed "class=", "<span class=\"na\">class=</span>",
    970       // it causes conflicts
    971       replaceManyG(l, "<pre><code class=\"language-html\">", "<figure class=\"highlight\"><pre><code class=\"language-html\" data-lang=\"html\">",
    972                       "&lt;script ", "<span class=\"nt\">&lt;script </span>",
    973                       "&lt;/script&gt;", "<span class=\"nt\">&lt;/script&gt;</span>",
    974                       "&lt;div", "<span class=\"nt\">&lt;div</span>",
    975                       "&lt;/div&gt;", "<span class=\"nt\">&lt;/div&gt;</span>",
    976                       "&lt;a", "<span class=\"nt\">&lt;a</span>",
    977                       "&lt;/a&gt;", "<span class=\"nt\">&lt;/a&gt;</span>",
    978                       "&lt;title&gt;", "<span class=\"nt\">&lt;title&gt;</span>",
    979                       "&lt;/title&gt;", "<span class=\"nt\">&lt;/title&gt;</span>",
    980                       "&lt;link", "<span class=\"nt\">&lt;link</span>",
    981                       "&lt;h1&gt;", "<span class=\"nt\">&lt;h1&gt;</span>",
    982                       "&lt;/h1&gt;", "<span class=\"nt\">&lt;/h1&gt;</span>",
    983                       "&lt;p&gt;", "<span class=\"nt\">&lt;p&gt;</span>",
    984                       "&lt;/p&gt;", "<span class=\"nt\">&lt;/p&gt;</span>",
    985                       "&lt;ul&gt;", "<span class=\"nt\">&lt;ul&gt;</span>",
    986                       "&lt;/ul&gt;", "<span class=\"nt\">&lt;/ul&gt;</span>",
    987                       "&lt;li&gt;", "<span class=\"nt\">&lt;li&gt;</span>",
    988                       "&lt;/li&gt;", "<span class=\"nt\">&lt;/li&gt;</span>",
    989                       "name=", "<span class=\"na\">name=</span>",
    990                       "type=", "<span class=\"na\">type=</span>",
    991                       "input=", "<span class=\"na\">input=</span>",
    992                       "style=", "<span class=\"na\">style=</span>",
    993                       "href=", "<span class=\"na\">href=</span>",
    994                       "src=", "<span class=\"na\">src=</span>"
    995                       );
    996       // strings
    997       if (not hasG(l, "<figure class=\"highlight\"><pre><code class=\"language-")) {
    998         var c = countG(l, "&quot;");
    999         if (c != 0 and (c & 1) == 0) {
   1000           highlightQuotes("&quot;", "<span class=\"s\">", "</span>");
   1001         }
   1002       }
   1003     }
   1004 
   1005     // coffeescript highlight
   1006     elif (status == coffeescript) {
   1007       replaceManyG(l, "<pre><code class=\"language-coffeescript\">", "<figure class=\"highlight\"><pre><code class=\"language-coffeescript\" data-lang=\"coffeescript\">",
   1008                       "*", "<span class=\"o\">*</span>",
   1009                       "delete", "<span class=\"kd\">delete</span>",
   1010                       "new", "<span class=\"kd\">new</span>",
   1011                       "this", "<span class=\"kd\">this</span>",
   1012                       "true", "<span class=\"kd\">true</span>",
   1013                       "false", "<span class=\"kd\">false</span>",
   1014                       "export", "<span class=\"kd\">export</span>",
   1015                       "Object", "<span class=\"nb\">Object</span>"
   1016                       );
   1017       // detect comment
   1018       if (hasG(l, "#")) {
   1019         replaceG(l, "#", "<span class=\"c\">#", 1);
   1020         pushG(l, "</span>");
   1021       }
   1022       // strings
   1023       if (not hasG(l, "<figure class=\"highlight\"><pre><code class=\"language-")) {
   1024         var c = countG(l, '\'');
   1025         if (c != 0 and (c & 1) == 0) {
   1026           highlightQuotes("'", "<span class=\"s1\">", "</span>");
   1027         }
   1028       }
   1029     }
   1030 
   1031     setPG(pHtml, iterIndexG(pHtml), l);
   1032   }
   1033 
   1034   //lv(pHtml);
   1035 
   1036   // insert html in layout template
   1037   cleanCharP(layoutFile) = catS(layoutDir,"/",$(pCfg, "layout"),".html");
   1038 
   1039   //lv(layoutFile);
   1040 
   1041   cleanAllocateSmallArray(layoutf);
   1042   cleanAllocateSmallJson(layoutCfg);
   1043   readFileWithFrontMatter(layoutFile, layoutCfg, layoutf);
   1044 
   1045   //lv(layoutf);
   1046 
   1047   // replace content with mardown generated html
   1048 
   1049   iter(layoutf, L) {
   1050     castS(l,L);
   1051     if (hasG(l, content)) {
   1052       delElemG(layoutf, iterIndexG(layoutf));
   1053       insertNFreeG(layoutf, iterIndexG(layoutf), dupG(pHtml));
   1054       break;
   1055     }
   1056   }
   1057 
   1058   // add layout and html code in sub layout
   1059   cleanCharP(subLayoutFile) = catS(layoutDir,"/",$(layoutCfg, "layout"),".html");
   1060 
   1061   var p = preprocess(subLayoutFile);
   1062 
   1063   //logG(p);
   1064 
   1065   iter(p, L) {
   1066     castS(l,L);
   1067     if (hasG(l, content)) {
   1068       delElemG(p, iterIndexG(p));
   1069       insertNFreeG(p, iterIndexG(p), dupG(layoutf));
   1070       break;
   1071     }
   1072   }
   1073 
   1074   // TODO list pages on first line of all pages
   1075   cleanAllocateSmallArray(about);
   1076   cleanAllocateSmallJson(aboutCfg);
   1077   readFileWithFrontMatter("about.md", aboutCfg, about);
   1078 
   1079   char *sitePages    = catS("<a class=\"page-link\" href=\"", $(cfg, "baseurl"), $(aboutCfg, "permalink"), "\">", $(aboutCfg, "title"), "</a>");
   1080 
   1081   // configure destination path, post url, date and description
   1082   cleanCharP(dst)    = null;
   1083   char *pUrl         = null;
   1084   baset *description = null;
   1085   // char *date is the visible date string: 'date' when the post is published for the first time, 'firstPublishDate update on date' when the post has been updated
   1086   cleanCharP(date)   = null;
   1087   // detect if generating a post or a page to select path and parameters
   1088   if (startsWithG(filename, PostsDir"/") or startsWithG(filename, publishedDir"/")) {
   1089     cleanCharP(postHtmlFile) = startsWithG(filename, PostsDir"/") ? copyRngG(filename, 18, 0) : copyRngG(filename, 22, 0);
   1090     replaceG(&postHtmlFile, ".markdown", ".html", 1);
   1091 
   1092     // select first publish date for html path: root/category/YY/MM/DD/postName.html
   1093     cleanCharP(postDate) = hasG(pCfg, "firstPublishDate") ? copyRngG($(pCfg, "firstPublishDate"), 0, 10) : copyRngG($(pCfg, "date"), 0, 10);
   1094     replaceG(&postDate, "-", "/", 0);
   1095     dst  = catS(siteDir,"/",$(pCfg, "categories"),"/",postDate,"/",postHtmlFile);
   1096     pUrl = catS($(cfg, "url"),$(cfg, "baseurl"),"/",
   1097                 $(pCfg,"categories"),"/",postDate,"/",postHtmlFile);
   1098     if (hasG(pCfg, "firstPublishDate")) {
   1099       if (eqG($(pCfg, "date"), $(pCfg, "firstPublishDate"))) goto onlyPublishDate;
   1100       // there is an update date
   1101       $(pCfg, "date")[10] = 0;
   1102       $(pCfg, "firstPublishDate")[10] = 0;
   1103       date = catS($(pCfg, "firstPublishDate"), " updated on ", $(pCfg, "date"));
   1104     }
   1105     else {
   1106       onlyPublishDate:
   1107       $(pCfg, "date")[10] = 0;
   1108       date = dupG($(pCfg, "date"));
   1109     }
   1110 
   1111     iter(pf, L) {
   1112       castS(l,L);
   1113       if (not isBlankG(l)) {
   1114         description = (baset*) dupG(l);
   1115         break;
   1116       }
   1117     }
   1118     replaceManyG((smallStringt*)description, "\"", "", "'", "", "`", "");
   1119     //lv(description);
   1120   }
   1121   else {
   1122     dst         = catS(siteDir, $(pCfg, "permalink"), "index.html");
   1123     pUrl        = catS($(cfg, "url"), $(cfg, "baseurl"), $(pCfg, "permalink"));
   1124     description = getNDupO(cfg, "description");
   1125   }
   1126 
   1127   // replace keys with values
   1128   cleanAllocateSmallDict(pageValues);
   1129 
   1130   setNFreeG(pageValues, "_\%title", getNDupO(pCfg, "title"));
   1131   setNFreeG(pageValues, "_\%description", description);
   1132   setNFreeG(pageValues, "_\%site.description", getNDupO(cfg, "description"));
   1133   setNFreeG(pageValues, "_\%css", catS($(cfg, "baseurl"), "/css/main.css"));
   1134   setNFreeG(pageValues, "_\%url", pUrl);
   1135   setNFreeG(pageValues, "_\%site.title", getNDupO(cfg, "title"));
   1136   setNFreeG(pageValues, "_\%feed", catS($(cfg, "url"), $(cfg, "baseurl"), "/feed.xml"));
   1137   setNFreeG(pageValues, "_\%baseurl", getNDupO(cfg, "baseurl"));
   1138   setNFreeG(pageValues, "_\%site.pages", sitePages);
   1139   setNFreeG(pageValues, "_\%site.email", getNDupO(cfg, "email"));
   1140   setNFreeG(pageValues, "_\%site.github_username", getNDupO(cfg, "github_username"));
   1141   setNFreeG(pageValues, "_\%site.twitter_username", getNDupO(cfg, "twitter_username"));
   1142   if (startsWithG(filename, PostsDir"/") or startsWithG(filename, publishedDir"/")) {
   1143     setG(pageValues, "_\%date", date);
   1144   }
   1145 
   1146   //lv(pageValues);
   1147 
   1148   simpleTemplatesReplaceKeysWithValues(p, pageValues);
   1149 
   1150   // generate feed
   1151   if (postsFeed) {
   1152     cleanCharP(postContent) = joinSG(pHtml, '\n');
   1153     iReplaceManyS(&postContent, "<",  "&lt;",
   1154                                ">",  "&gt;",
   1155                                "\"", "&quot;");
   1156     pushNFreeG(postsFeed, formatS(
   1157       "<item>\n"
   1158       "  <title>%s</title>\n"
   1159       "  <description>%s</description>\n"
   1160       "  <pubDate>%s</pubDate>\n"
   1161       "  <link>%s</link>\n"
   1162       "  <guid isPermaLink=\"true\">%s</guid>\n"
   1163       "  <category>%s</category>\n"
   1164       "</item>\n",
   1165       $(pCfg, "title"), postContent, date, $(pageValues, "_\%url"), $(pageValues, "_\%url"),
   1166       $(pCfg, "categories")));
   1167   }
   1168 
   1169   //logG(p);
   1170 
   1171   // write post html page
   1172   cleanCharP(dstDir) = shDirname(dst);
   1173 
   1174   //lv(dst);
   1175   //lv(dstDir);
   1176 
   1177   if (not isPath(dstDir)) {
   1178     mkdirParents(dstDir);
   1179   }
   1180 
   1181   writeFileG(p, dst);
   1182 
   1183   ret true;
   1184 }
   1185 
   1186 // vim: set expandtab ts=2 sw=2: