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 "<script ", "<span class=\"nt\"><script </span>", 973 "</script>", "<span class=\"nt\"></script></span>", 974 "<div", "<span class=\"nt\"><div</span>", 975 "</div>", "<span class=\"nt\"></div></span>", 976 "<a", "<span class=\"nt\"><a</span>", 977 "</a>", "<span class=\"nt\"></a></span>", 978 "<title>", "<span class=\"nt\"><title></span>", 979 "</title>", "<span class=\"nt\"></title></span>", 980 "<link", "<span class=\"nt\"><link</span>", 981 "<h1>", "<span class=\"nt\"><h1></span>", 982 "</h1>", "<span class=\"nt\"></h1></span>", 983 "<p>", "<span class=\"nt\"><p></span>", 984 "</p>", "<span class=\"nt\"></p></span>", 985 "<ul>", "<span class=\"nt\"><ul></span>", 986 "</ul>", "<span class=\"nt\"></ul></span>", 987 "<li>", "<span class=\"nt\"><li></span>", 988 "</li>", "<span class=\"nt\"></li></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, """); 999 if (c != 0 and (c & 1) == 0) { 1000 highlightQuotes(""", "<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, "<", "<", 1154 ">", ">", 1155 "\"", """); 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: