💾 Archived View for gmi.noulin.net › gitRepositories › forb › file › forb.c.gmi captured on 2023-01-29 at 13:25:20. Gemini links have been rewritten to link to archived content

View Raw

More Information

➡️ Next capture (2024-07-09)

-=-=-=-=-=-=-

forb

Log

Files

Refs

README

LICENSE

forb.c (38494B)

     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")) {
   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       "  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"
   479       );
   480 }
   481 
   482 
   483 /**
   484  * publish a post in _drafts to _posts
   485  */
   486 void publish(const char *path) {
   487   // steps
   488   // check extension
   489   // read post
   490   // create date string for filename
   491   // create destination filename
   492   // create date string for front matter
   493   // add date string in front matter
   494   // failed to find front matter or categories
   495   // save posts directory
   496   // remove path
   497 
   498   // check extension
   499   if (not endsWithG(path, ".markdown")) {
   500     logE("'%s' doesn't have the markdown extension. Stop.");
   501     XFailure;
   502   }
   503 
   504   // read post
   505   cleanAllocateSmallArray(post);
   506   readFileG(post, path);
   507 
   508   // create date string for filename
   509   cleanCharP(date) = getCurrentDateYMD();
   510   date[10] = 0;
   511   //lv(date);
   512   //lv(path);
   513 
   514   // create destination filename
   515   cleanCharP(dest) = catS(PostsDir"/", date, "-", basename(path));
   516   //lv(dest);
   517 
   518   // create date string for front matter
   519   date[10] = ' ';
   520   prependG(&date, "date: ");
   521   //lv(date);
   522 
   523   // add date string in front matter
   524   char *status = "search front matter";
   525   iter(post, L) {
   526     castS(l, L);
   527     if (eqG(status, "stop at categories") and startsWithG(l, "categories:")) {
   528       injectG(post, iI(post), date);
   529       goto publishPost;
   530     }
   531     if (eqG(status, "stop at categories") and eqG(l, "---")) {
   532       logE("categories not found in front matter, path is: %s", path);
   533       ret;
   534     }
   535     if (eqG(status, "search front matter") and eqG(l, "---")) {
   536       status = "stop at categories";
   537     }
   538   }
   539 
   540   // failed to find front matter or categories
   541   logE("front matter or categories not found, path is %s", path);
   542   ret;
   543 
   544   publishPost:
   545   // save posts directory
   546   if (not writeFileG(post, dest)) {
   547     logE("Could not write '%s', path is: %s", dest, path);
   548     ret;
   549   }
   550 
   551   // remove path
   552   if (not rmAllG(path)) {
   553     logE("Could not remove: %s", path);
   554   }
   555 
   556   logP("Published draft post to %s", dest);
   557 }
   558 
   559 
   560 /**
   561  * update a post in _published to _posts
   562  */
   563 void update(const char *path) {
   564   // steps
   565   // check extension
   566   // read post
   567   // create destination filename
   568   // create date string for front matter
   569   // change date string in front matter
   570   // failed to find front matter or date
   571   // save posts directory
   572   // remove path
   573 
   574   // check extension
   575   if (not endsWithG(path, ".markdown")) {
   576     logE("'%s' doesn't have the markdown extension. Stop.");
   577     XFailure;
   578   }
   579 
   580   // read post
   581   cleanAllocateSmallArray(post);
   582   readFileG(post, path);
   583 
   584   // create destination filename
   585   cleanCharP(dest) = catS(PostsDir"/", basename(path));
   586   //lv(dest);
   587 
   588   // create date string for front matter
   589   cleanCharP(date) = getCurrentDateYMD();
   590   date[10] = ' ';
   591   prependG(&date, "date: ");
   592   //lv(date);
   593 
   594   // change date string in front matter
   595   char *status = "search front matter";
   596   iter(post, L) {
   597     castS(l, L);
   598     if (eqG(status, "stop at date") and startsWithG(l, "date:")) {
   599       setG(post, iI(post), date);
   600       goto publishPost;
   601     }
   602     if (eqG(status, "stop at date") and eqG(l, "---")) {
   603       logE("date not found in front matter, path is: %s", path);
   604       ret;
   605     }
   606     if (eqG(status, "search front matter") and eqG(l, "---")) {
   607       status = "stop at date";
   608     }
   609   }
   610 
   611   // failed to find front matter or date
   612   logE("front matter or date not found, path is %s", path);
   613   ret;
   614 
   615   publishPost:
   616   // save posts directory
   617   if (not writeFileG(post, dest)) {
   618     logE("Could not write '%s', path is: %s", dest, path);
   619     ret;
   620   }
   621 
   622   // remove path
   623   if (not rmAllG(path)) {
   624     logE("Could not remove: %s", path);
   625   }
   626 
   627   logP("Update already published post in %s", dest);
   628 }
   629 
   630 
   631 /**
   632  * read file filename with front matter and text (any type of text)
   633  *
   634  * \return
   635  *   kv: keys and values for yml code in front matter
   636  *   lines: lines in file filename without front matter
   637  */
   638 bool readFileWithFrontMatter(char *filename, smallJsont *kv, smallArrayt *lines) {
   639   int frontMatterStart = -1, frontMatterEnd = -1;
   640 
   641   // Steps
   642   // find front matter start and end
   643   // parse yml code in front matter
   644   // remove front matter from lines
   645 
   646   if (not isPath(filename)) {
   647     logE("File '%s' not found.", filename);
   648     ret no;
   649   }
   650 
   651   readFileG(lines, filename);
   652 
   653   // find front matter start and end
   654   char *status = "start";
   655   iter(lines, L) {
   656     castS(l,L);
   657     if (eqS(status, "end") and (startsWithG(l, "---"))) {
   658       frontMatterEnd = iterIndexG(lines);
   659       break;
   660     }
   661     if (eqS(status, "start") and (startsWithG(l, "---"))) {
   662       frontMatterStart = iterIndexG(lines)+1;
   663       status = "end";
   664     }
   665   }
   666 
   667   if (frontMatterStart == -1 or frontMatterEnd == -1) {
   668     logE("front matter not found in '%s'", filename);
   669     ret no;
   670   }
   671 
   672   // parse yml code in front matter
   673   var frontMatter = copyRngG(lines, frontMatterStart, frontMatterEnd);
   674 
   675   //lv(frontMatter);
   676 
   677   // TODO change to: parseYMLG(indexCfg, frontMatter);
   678   // when parseYMLG accepts smallArrays
   679   cleanCharP(fm) = joinSG(frontMatter, "\n");
   680   parseYMLG(kv, fm);
   681 
   682   //lv(indexCfg);
   683   finishG(frontMatter);
   684 
   685   // remove front matter from lines
   686   sliceG(lines, frontMatterEnd+1, 0);
   687   //lv(lines);
   688 
   689   ret yes;
   690 }
   691 
   692 /**
   693  * convert post filename ("2020-06-09-getting-started" without markdown)
   694  * to url for linking posts easily in the blog.
   695  *
   696  * The url format is "/category/YYYY/MM/DD/filename.html"
   697  */
   698 char *postToUrl(char *filename) {
   699   cleanCharP(fn) = catS(PostsDir"/",filename,".markdown");
   700   if (not isPath(fn)) {
   701     free(fn);
   702     fn = catS(publishedDir"/",filename,".markdown");
   703   }
   704 
   705   cleanAllocateSmallArray(pf);
   706   cleanAllocateSmallJson(pCfg);
   707   readFileWithFrontMatter(fn, pCfg, pf);
   708 
   709   cleanCharP(postHtmlFile) = copyRngG(fn, 22, 0);
   710   replaceG(&postHtmlFile, ".markdown", ".html", 1);
   711 
   712   // select first publish date for html path: root/category/YY/MM/DD/postName.html
   713   cleanCharP(postDate) = hasG(pCfg, "firstPublishDate") ? copyRngG($(pCfg, "firstPublishDate"), 0, 10) : copyRngG($(pCfg, "date"), 0, 10);
   714   replaceG(&postDate, "-", "/", 0);
   715   char *pUrl = catS("/",$(pCfg,"categories"),"/",postDate,"/",postHtmlFile);
   716 
   717   ret pUrl;
   718 }
   719 
   720 
   721 bool generateAPageOrAPost(char *filename, smallJsont *cfg, smallArrayt *postsFeed) {
   722 
   723   // Steps
   724   // check if filename exists
   725   // parse front matter yml
   726   // convert markdown to html
   727   // add class in <code>
   728   // process {% post_url 2015-12-22-test %}
   729   // add syntax highlighting
   730   // insert html in layout template
   731   // add layout and html code in sub layout
   732   // TODO list pages on first line of all pages
   733   // configure destination path, post url, date and description
   734   // detect if generating a post or a page to select path and parameters
   735   // replace keys with values
   736   // generate feed
   737   // write post html page
   738 
   739   // check if filename exists
   740   if (not isPath(filename)) {
   741     logE("%s not found", filename);
   742     ret false;
   743   }
   744 
   745   // parse front matter yml
   746   cleanAllocateSmallArray(pf);
   747   cleanAllocateSmallJson(pCfg);
   748   readFileWithFrontMatter(filename, pCfg, pf);
   749 
   750   //lv(filename);
   751 
   752   // process {% post_url 2015-12-22-test %}
   753   // skip converting to URL in code blocks
   754   enum {searchcodeblock, incodeblock};
   755   int mdstatus = searchcodeblock;
   756   iter(pf, L) {
   757     castS(l,L);
   758     // process {% post_url 2015-12-22-running-rocket-chat %}
   759     // only outside code block, keep inside code blocks
   760     // (keep this block after code highlighting to get the correct status from the first line
   761     // in the code block)
   762     if (mdstatus equals searchcodeblock and hasG(l, "{\% post_url ")) {
   763       // convert all % post_url to valid url on the line
   764       char *cursor       = hasG(l, "{\% post_url ");
   765       while (cursor) {
   766         char *startp       = cursor;
   767         char *endp         = findS(cursor, " \%}");
   768         int start          = startp - ssGet(l);
   769         int end            = endp   - ssGet(l);
   770         // post filename
   771         char *postFilemane = startp + strlen("{\% post_url ");
   772         *endp              = 0;
   773         //lv(postFilemane);
   774         cleanCharP(url)    = postToUrl(postFilemane);
   775         delG(l, start, end+3);
   776         insertG(l, start, url);
   777         cursor             = findS(ssGet(l)+start, "{\% post_url ");
   778       }
   779       //lv(l);
   780       setPG(pf, iterIndexG(pf), l);
   781     }
   782     if (startsWithG(l, "```"))
   783       mdstatus is mdstatus equals searchcodeblock ? incodeblock : searchcodeblock;
   784   }
   785   //lv(pf);
   786 
   787   // convert markdown to html
   788   writeFileG(pf, "tmp.md");
   789 
   790   // TODO get path once and reuse
   791   cleanCharP(progPath) = shDirname(getRealProgPath());
   792   commandf("%s/shpPackages/md2html/md2html --full-html --ftables --fstrikethrough tmp.md --output=tmp.html", progPath);
   793 
   794   cleanAllocateSmallArray(pHtml);
   795   readFileG(pHtml, "tmp.html");
   796 
   797     // keep only html code between the body tags
   798   int bodyStart = -1, bodyEnd = -1;
   799   iter(pHtml, L) {
   800     castS(l,L);
   801     if (hasG(l, "<body>"))  bodyStart = iterIndexG(pHtml)+1;
   802     if (hasG(l, "</body>")) {
   803       bodyEnd   = iterIndexG(pHtml);
   804       break;
   805     }
   806   }
   807   sliceG(pHtml, bodyStart, bodyEnd);
   808 
   809   // add class in <code>
   810   // add syntax highlighting
   811   enum {searchCode, notspecified, bash, javascript, python, html, coffeescript};
   812   int status = searchCode;
   813   int lastCodeHighlightingLine = -1;
   814   iter(pHtml, L) {
   815     castS(l,L);
   816     // code highlighting
   817     if (status == searchCode and hasG(l, "<code>")) {
   818       if (hasG(l, "<pre><code>")) {
   819         replaceG(l, "<pre><code>", "<figure class=\"highlight\"><pre><code class=\"highlighter-rouge\">", 0);
   820         status = notspecified;
   821       }
   822       else {
   823         replaceG(l, "<code>", "<code class=\"highlighter-rouge\">", 0);
   824       }
   825     }
   826     if (status == searchCode and hasG(l, "<pre><code class=\"language-")) {
   827       lastCodeHighlightingLine = iterIndexG(pHtml);
   828     }
   829     if (status == searchCode and hasG(l, "<pre><code class=\"language-bash\">")) {
   830       status = bash;
   831     }
   832     if (status == searchCode and hasG(l, "<pre><code class=\"language-javascript\">")) {
   833       status = javascript;
   834     }
   835     if (status == searchCode and hasG(l, "<pre><code class=\"language-python\">")) {
   836       status = python;
   837     }
   838     if (status == searchCode and hasG(l, "<pre><code class=\"language-html\">")) {
   839       status = html;
   840     }
   841     if (status == searchCode and hasG(l, "<pre><code class=\"language-coffeescript\">")) {
   842       status = coffeescript;
   843     }
   844     if (hasG(l, "</code></pre>")) {
   845       if (status == searchCode) {
   846         if (lastCodeHighlightingLine == -1) {
   847           logW("line %d in html for "BLD"%s"RST", code highlighting not found. This code block doesn't have highlighting.", iterIndexG(pHtml), &filename[18]);
   848         }
   849         else {
   850           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));
   851         }
   852       }
   853       else replaceG(l, "</code></pre>", "</code></pre></figure>", 0);
   854       status = searchCode;
   855     }
   856 
   857     // bash highlight
   858     if (status == bash) {
   859       replaceManyG(l, "<pre><code class=\"language-bash\">", "<figure class=\"highlight\"><pre><code class=\"language-bash\" data-lang=\"bash\">",
   860           "cd ", "<span class=\"nb\">cd </span>",
   861           "echo ", "<span class=\"nb\">echo </span>",
   862           "set ", "<span class=\"nb\">set </span>");
   863       // detect comment
   864       if (hasG(l, "#")) {
   865         replaceG(l, "#", "<span class=\"c\">#", 1);
   866         pushG(l, "</span>");
   867       }
   868       // strings
   869       if (not hasG(l, "<figure class=\"highlight\"><pre><code class=\"language-")) {
   870         var c = countG(l, '\'');
   871         if (c != 0 and (c & 1) == 0) {
   872           #define highlightQuotes(q, qS, qE) procbegin\
   873             var len = lenG(l);\
   874             var qL  = lenG(q);\
   875             cleanCharP(str) = malloc(len + ((strlen(qS) + strlen(qE)) * c/2) + 1);\
   876             char *ps  = str;\
   877             char *ln  = ssGet(l);\
   878             enum {qSearch, q1};\
   879             int status = qSearch;\
   880             range(i, len) {\
   881               if (eqIS(ln, q, i)) {\
   882                 if (status == qSearch) {\
   883                   status = q1;\
   884                   strcpy(ps, qS);\
   885                   ps += strlen(qS);\
   886                   *ps++ = ln[i];\
   887                 }\
   888                 elif (status == q1) {\
   889                   status = qSearch;\
   890                   rangeFrom(n, i, i+qL) {\
   891                     *ps++ = ln[n];\
   892                   }\
   893                   i += qL;\
   894                   strcpy(ps, qE);\
   895                   ps += strlen(qE);\
   896                 }\
   897               }\
   898               else {\
   899                 *ps++ = ln[i];\
   900               }\
   901             }\
   902             *ps = 0;\
   903             setValG(l, str);\
   904             procend
   905           highlightQuotes("'", "<span class=\"s1\">", "</span>");
   906         }
   907       }
   908     }
   909 
   910     // javascript highlight
   911     elif (status == javascript) {
   912       replaceManyG(l, "<pre><code class=\"language-javascript\">", "<figure class=\"highlight\"><pre><code class=\"language-javascript\" data-lang=\"javascript\">",
   913                       "*", "<span class=\"o\">*</span>",
   914                       "var", "<span class=\"kd\">var</span>",
   915                       "function", "<span class=\"kd\">function</span>",
   916                       "return", "<span class=\"kd\">return</span>",
   917                       "delete", "<span class=\"kd\">delete</span>",
   918                       "new", "<span class=\"kd\">new</span>",
   919                       "this", "<span class=\"kd\">this</span>",
   920                       "true", "<span class=\"kd\">true</span>",
   921                       "false", "<span class=\"kd\">false</span>",
   922                       "export", "<span class=\"kd\">export</span>",
   923                       "Object", "<span class=\"nb\">Object</span>"
   924                       );
   925       // detect comment
   926       if (hasG(l, "//") and not hasG(l, "://")) {
   927         replaceG(l, "//", "<span class=\"c\">//", 1);
   928         pushG(l, "</span>");
   929       }
   930       // strings
   931       if (not hasG(l, "<figure class=\"highlight\"><pre><code class=\"language-")) {
   932         var c = countG(l, '\'');
   933         if (c != 0 and (c & 1) == 0) {
   934           highlightQuotes("'", "<span class=\"s1\">", "</span>");
   935         }
   936       }
   937     }
   938 
   939     // python highlighting
   940     elif (status == python) {
   941       replaceManyG(l, "<pre><code class=\"language-python\">", "<figure class=\"highlight\"><pre><code class=\"language-python\" data-lang=\"python\">",
   942                       "*", "<span class=\"o\">*</span>",
   943                       "for", "<span class=\"kd\">for</span>",
   944                       "print", "<span class=\"kd\">print</span>",
   945                       "def", "<span class=\"kd\">def</span>",
   946                       "return", "<span class=\"kd\">return</span>",
   947                       "del", "<span class=\"kd\">del</span>",
   948                       "dict", "<span class=\"nb\">dict</span>",
   949                       "range", "<span class=\"nb\">range</span>",
   950                       "zip", "<span class=\"nb\">zip</span>"
   951                       );
   952       // detect comment
   953       if (hasG(l, "#")) {
   954         replaceG(l, "#", "<span class=\"c\">#", 1);
   955         pushG(l, "</span>");
   956       }
   957       // strings
   958       if (not hasG(l, "<figure class=\"highlight\"><pre><code class=\"language-")) {
   959         var c = countG(l, '\'');
   960         if (c != 0 and (c & 1) == 0) {
   961           highlightQuotes("'", "<span class=\"s\">", "</span>");
   962         }
   963       }
   964     }
   965 
   966     // html highlight
   967     elif (status == html) {
   968       // removed "class=", "<span class=\"na\">class=</span>",
   969       // it causes conflicts
   970       replaceManyG(l, "<pre><code class=\"language-html\">", "<figure class=\"highlight\"><pre><code class=\"language-html\" data-lang=\"html\">",
   971                       "&lt;script ", "<span class=\"nt\">&lt;script </span>",
   972                       "&lt;/script&gt;", "<span class=\"nt\">&lt;/script&gt;</span>",
   973                       "&lt;div", "<span class=\"nt\">&lt;div</span>",
   974                       "&lt;/div&gt;", "<span class=\"nt\">&lt;/div&gt;</span>",
   975                       "&lt;a", "<span class=\"nt\">&lt;a</span>",
   976                       "&lt;/a&gt;", "<span class=\"nt\">&lt;/a&gt;</span>",
   977                       "&lt;title&gt;", "<span class=\"nt\">&lt;title&gt;</span>",
   978                       "&lt;/title&gt;", "<span class=\"nt\">&lt;/title&gt;</span>",
   979                       "&lt;link", "<span class=\"nt\">&lt;link</span>",
   980                       "&lt;h1&gt;", "<span class=\"nt\">&lt;h1&gt;</span>",
   981                       "&lt;/h1&gt;", "<span class=\"nt\">&lt;/h1&gt;</span>",
   982                       "&lt;p&gt;", "<span class=\"nt\">&lt;p&gt;</span>",
   983                       "&lt;/p&gt;", "<span class=\"nt\">&lt;/p&gt;</span>",
   984                       "&lt;ul&gt;", "<span class=\"nt\">&lt;ul&gt;</span>",
   985                       "&lt;/ul&gt;", "<span class=\"nt\">&lt;/ul&gt;</span>",
   986                       "&lt;li&gt;", "<span class=\"nt\">&lt;li&gt;</span>",
   987                       "&lt;/li&gt;", "<span class=\"nt\">&lt;/li&gt;</span>",
   988                       "name=", "<span class=\"na\">name=</span>",
   989                       "type=", "<span class=\"na\">type=</span>",
   990                       "input=", "<span class=\"na\">input=</span>",
   991                       "style=", "<span class=\"na\">style=</span>",
   992                       "href=", "<span class=\"na\">href=</span>",
   993                       "src=", "<span class=\"na\">src=</span>"
   994                       );
   995       // strings
   996       if (not hasG(l, "<figure class=\"highlight\"><pre><code class=\"language-")) {
   997         var c = countG(l, "&quot;");
   998         if (c != 0 and (c & 1) == 0) {
   999           highlightQuotes("&quot;", "<span class=\"s\">", "</span>");
  1000         }
  1001       }
  1002     }
  1003 
  1004     // coffeescript highlight
  1005     elif (status == coffeescript) {
  1006       replaceManyG(l, "<pre><code class=\"language-coffeescript\">", "<figure class=\"highlight\"><pre><code class=\"language-coffeescript\" data-lang=\"coffeescript\">",
  1007                       "*", "<span class=\"o\">*</span>",
  1008                       "delete", "<span class=\"kd\">delete</span>",
  1009                       "new", "<span class=\"kd\">new</span>",
  1010                       "this", "<span class=\"kd\">this</span>",
  1011                       "true", "<span class=\"kd\">true</span>",
  1012                       "false", "<span class=\"kd\">false</span>",
  1013                       "export", "<span class=\"kd\">export</span>",
  1014                       "Object", "<span class=\"nb\">Object</span>"
  1015                       );
  1016       // detect comment
  1017       if (hasG(l, "#")) {
  1018         replaceG(l, "#", "<span class=\"c\">#", 1);
  1019         pushG(l, "</span>");
  1020       }
  1021       // strings
  1022       if (not hasG(l, "<figure class=\"highlight\"><pre><code class=\"language-")) {
  1023         var c = countG(l, '\'');
  1024         if (c != 0 and (c & 1) == 0) {
  1025           highlightQuotes("'", "<span class=\"s1\">", "</span>");
  1026         }
  1027       }
  1028     }
  1029 
  1030     setPG(pHtml, iterIndexG(pHtml), l);
  1031   }
  1032 
  1033   //lv(pHtml);
  1034 
  1035   // insert html in layout template
  1036   cleanCharP(layoutFile) = catS(layoutDir,"/",$(pCfg, "layout"),".html");
  1037 
  1038   //lv(layoutFile);
  1039 
  1040   cleanAllocateSmallArray(layoutf);
  1041   cleanAllocateSmallJson(layoutCfg);
  1042   readFileWithFrontMatter(layoutFile, layoutCfg, layoutf);
  1043 
  1044   //lv(layoutf);
  1045 
  1046   // replace content with mardown generated html
  1047 
  1048   iter(layoutf, L) {
  1049     castS(l,L);
  1050     if (hasG(l, content)) {
  1051       delElemG(layoutf, iterIndexG(layoutf));
  1052       insertNFreeG(layoutf, iterIndexG(layoutf), dupG(pHtml));
  1053       break;
  1054     }
  1055   }
  1056 
  1057   // add layout and html code in sub layout
  1058   cleanCharP(subLayoutFile) = catS(layoutDir,"/",$(layoutCfg, "layout"),".html");
  1059 
  1060   var p = preprocess(subLayoutFile);
  1061 
  1062   //logG(p);
  1063 
  1064   iter(p, L) {
  1065     castS(l,L);
  1066     if (hasG(l, content)) {
  1067       delElemG(p, iterIndexG(p));
  1068       insertNFreeG(p, iterIndexG(p), dupG(layoutf));
  1069       break;
  1070     }
  1071   }
  1072 
  1073   // TODO list pages on first line of all pages
  1074   cleanAllocateSmallArray(about);
  1075   cleanAllocateSmallJson(aboutCfg);
  1076   readFileWithFrontMatter("about.md", aboutCfg, about);
  1077 
  1078   char *sitePages    = catS("<a class=\"page-link\" href=\"", $(cfg, "baseurl"), $(aboutCfg, "permalink"), "\">", $(aboutCfg, "title"), "</a>");
  1079 
  1080   // configure destination path, post url, date and description
  1081   cleanCharP(dst)    = null;
  1082   char *pUrl         = null;
  1083   baset *description = null;
  1084   // 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
  1085   cleanCharP(date)   = null;
  1086   // detect if generating a post or a page to select path and parameters
  1087   if (startsWithG(filename, PostsDir"/") or startsWithG(filename, publishedDir"/")) {
  1088     cleanCharP(postHtmlFile) = startsWithG(filename, PostsDir"/") ? copyRngG(filename, 18, 0) : copyRngG(filename, 22, 0);
  1089     replaceG(&postHtmlFile, ".markdown", ".html", 1);
  1090 
  1091     // select first publish date for html path: root/category/YY/MM/DD/postName.html
  1092     cleanCharP(postDate) = hasG(pCfg, "firstPublishDate") ? copyRngG($(pCfg, "firstPublishDate"), 0, 10) : copyRngG($(pCfg, "date"), 0, 10);
  1093     replaceG(&postDate, "-", "/", 0);
  1094     dst  = catS(siteDir,"/",$(pCfg, "categories"),"/",postDate,"/",postHtmlFile);
  1095     pUrl = catS($(cfg, "url"),$(cfg, "baseurl"),"/",
  1096                 $(pCfg,"categories"),"/",postDate,"/",postHtmlFile);
  1097     if (hasG(pCfg, "firstPublishDate")) {
  1098       if (eqG($(pCfg, "date"), $(pCfg, "firstPublishDate"))) goto onlyPublishDate;
  1099       // there is an update date
  1100       $(pCfg, "date")[10] = 0;
  1101       $(pCfg, "firstPublishDate")[10] = 0;
  1102       date = catS($(pCfg, "firstPublishDate"), " updated on ", $(pCfg, "date"));
  1103     }
  1104     else {
  1105       onlyPublishDate:
  1106       $(pCfg, "date")[10] = 0;
  1107       date = dupG($(pCfg, "date"));
  1108     }
  1109 
  1110     iter(pf, L) {
  1111       castS(l,L);
  1112       if (not isBlankG(l)) {
  1113         description = (baset*) dupG(l);
  1114         break;
  1115       }
  1116     }
  1117     replaceManyG((smallStringt*)description, "\"", "", "'", "", "`", "");
  1118     //lv(description);
  1119   }
  1120   else {
  1121     dst         = catS(siteDir, $(pCfg, "permalink"), "index.html");
  1122     pUrl        = catS($(cfg, "url"), $(cfg, "baseurl"), $(pCfg, "permalink"));
  1123     description = getNDupO(cfg, "description");
  1124   }
  1125 
  1126   // replace keys with values
  1127   cleanAllocateSmallDict(pageValues);
  1128 
  1129   setNFreeG(pageValues, "_\%title", getNDupO(pCfg, "title"));
  1130   setNFreeG(pageValues, "_\%description", description);
  1131   setNFreeG(pageValues, "_\%site.description", getNDupO(cfg, "description"));
  1132   setNFreeG(pageValues, "_\%css", catS($(cfg, "baseurl"), "/css/main.css"));
  1133   setNFreeG(pageValues, "_\%url", pUrl);
  1134   setNFreeG(pageValues, "_\%site.title", getNDupO(cfg, "title"));
  1135   setNFreeG(pageValues, "_\%feed", catS($(cfg, "url"), $(cfg, "baseurl"), "/feed.xml"));
  1136   setNFreeG(pageValues, "_\%baseurl", getNDupO(cfg, "baseurl"));
  1137   setNFreeG(pageValues, "_\%site.pages", sitePages);
  1138   setNFreeG(pageValues, "_\%site.email", getNDupO(cfg, "email"));
  1139   setNFreeG(pageValues, "_\%site.github_username", getNDupO(cfg, "github_username"));
  1140   setNFreeG(pageValues, "_\%site.twitter_username", getNDupO(cfg, "twitter_username"));
  1141   if (startsWithG(filename, PostsDir"/") or startsWithG(filename, publishedDir"/")) {
  1142     setG(pageValues, "_\%date", date);
  1143   }
  1144 
  1145   //lv(pageValues);
  1146 
  1147   simpleTemplatesReplaceKeysWithValues(p, pageValues);
  1148 
  1149   // generate feed
  1150   if (postsFeed) {
  1151     cleanCharP(postContent) = joinSG(pHtml, '\n');
  1152     iReplaceManyS(&postContent, "<",  "&lt;",
  1153                                ">",  "&gt;",
  1154                                "\"", "&quot;");
  1155     pushNFreeG(postsFeed, formatS(
  1156       "<item>\n"
  1157       "  <title>%s</title>\n"
  1158       "  <description>%s</description>\n"
  1159       "  <pubDate>%s</pubDate>\n"
  1160       "  <link>%s</link>\n"
  1161       "  <guid isPermaLink=\"true\">%s</guid>\n"
  1162       "  <category>%s</category>\n"
  1163       "</item>\n",
  1164       $(pCfg, "title"), postContent, date, $(pageValues, "_\%url"), $(pageValues, "_\%url"),
  1165       $(pCfg, "categories")));
  1166   }
  1167 
  1168   //logG(p);
  1169 
  1170   // write post html page
  1171   cleanCharP(dstDir) = shDirname(dst);
  1172 
  1173   //lv(dst);
  1174   //lv(dstDir);
  1175 
  1176   if (not isPath(dstDir)) {
  1177     mkdirParents(dstDir);
  1178   }
  1179 
  1180   writeFileG(p, dst);
  1181 
  1182   ret true;
  1183 }
  1184 
  1185 // vim: set expandtab ts=2 sw=2: