forb

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: