💾 Archived View for gmi.noulin.net › gitRepositories › forb › file › forb.c.gmi captured on 2023-07-10 at 18:10:58. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-01-29)
-=-=-=-=-=-=-
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 "<script ", "<span class=\"nt\"><script </span>", 972 "</script>", "<span class=\"nt\"></script></span>", 973 "<div", "<span class=\"nt\"><div</span>", 974 "</div>", "<span class=\"nt\"></div></span>", 975 "<a", "<span class=\"nt\"><a</span>", 976 "</a>", "<span class=\"nt\"></a></span>", 977 "<title>", "<span class=\"nt\"><title></span>", 978 "</title>", "<span class=\"nt\"></title></span>", 979 "<link", "<span class=\"nt\"><link</span>", 980 "<h1>", "<span class=\"nt\"><h1></span>", 981 "</h1>", "<span class=\"nt\"></h1></span>", 982 "<p>", "<span class=\"nt\"><p></span>", 983 "</p>", "<span class=\"nt\"></p></span>", 984 "<ul>", "<span class=\"nt\"><ul></span>", 985 "</ul>", "<span class=\"nt\"></ul></span>", 986 "<li>", "<span class=\"nt\"><li></span>", 987 "</li>", "<span class=\"nt\"></li></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, """); 998 if (c != 0 and (c & 1) == 0) { 999 highlightQuotes(""", "<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, "<", "<", 1153 ">", ">", 1154 "\"", """); 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: