commit 3f618f52abd4b338ffb7b987de12038f8d93502b
parent 80c29d144cbb6ef2f34c5475f4cd296001a6e941
Author: Remy Noulin <loader2x@gmail.com>
Date: Mon, 3 Aug 2020 21:43:26 +0200
add update command to republish a post, change blog generator to support first publish date and 'update date'
README.md | 23 +++++-
forb.c | 278 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
forb.h | 7 ++
3 files changed, 283 insertions(+), 25 deletions(-)
Diffstat:
M | README.md | | | 23 | +++++++++++++++++++---- |
M | forb.c | | | 278 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------ |
A | forb.h | | | 7 | +++++++ |
3 files changed, 283 insertions(+), 25 deletions(-)
diff --git a/README.md b/README.md
@@ -21,11 +21,26 @@ The site configuration is located in `_config.yml`. The default `_config.yml` is
# Posts
-Add your posts in the `_posts` directory, with filenames in the format:
-`YYYY-MM-DD-TITLE.markdown`
+To create a post, run:
+```
+forb new post title
+```
+
+`forb new title` creates a post template in _draft, edit the draft and when ready, run:
+```
+forb publish _draft/title.markdown
+```
-For example:
-`2020-06-09-getting-started.markdown`
+# Update already published post
+
+To generate the blog, run `forb`, after this the posts are moved from the _posts directory to the _published directory.
+
+To update a post, edit the text in the _published directory, then run:
+```
+forb update _published/title.markdown
+# generate
+forb
+```
# About markdown
diff --git a/forb.c b/forb.c
@@ -32,7 +32,6 @@
#define siteDir "_site"
#define mainCss "css/main.css"
#define imagesDir "images"
-
#define content "{{ content }}"
/* enable/disable logging */
@@ -43,6 +42,8 @@ void help(void);
void publish(const char *path);
+void update(const char *path);
+
bool readFileWithFrontMatter(char *filename, smallJsont *kv, smallArrayt *lines);
bool generateAPageOrAPost(char *filename, smallJsont *cfg, smallArrayt *postsFeed);
@@ -55,6 +56,37 @@ int main(int ARGC, char** ARGV) {
setLogSymbols(LOG_UTF8);
//disableLibsheepyErrorLogs;
+ // steps
+ // process command line arguments
+ // arg new
+ // create directory structure when config file is not found
+ // else create a draft
+ // arg help
+ // print help
+ // arg publish
+ // copy draft to post dir, the date field is updated
+ // arg update
+ // copy published post to post dir, the date field is updated
+ // when there are no arguments, generate the blog
+ // check inputs
+ // load config
+ // copy main css
+ // copy images
+ // generate index
+ // read posts
+ // list all md files in _posts and _published
+ // generate html code for each post for index.html
+ // create configuation for index
+ // save index
+ // generate about page
+ // generate posts
+ // collect html code for 10 last post for feed.xml
+ // generate feed.xml
+ // clean tmp files
+ // list all md files in _posts
+ // move posts from _posts to _published
+ // add first publish date if needed
+
// read arguments
#define checkInput(path) procbegin\
if (not isPath(path)) {\
@@ -79,6 +111,7 @@ int main(int ARGC, char** ARGV) {
XSuccess;
}
elif (eqG(ARGV[1], "new")) {
+ // create a draft
checkInput(draftsDir);
if (ARGC < 3) {
logE(BLD"Too few arguments"RST", post title missing.\n"
@@ -86,6 +119,7 @@ int main(int ARGC, char** ARGV) {
);
XFailure;
}
+ // create post title and filename
cleanListP(args) = dupG(ARGV);
delElemG(&args, 1);
delElemG(&args, 0);
@@ -100,6 +134,7 @@ int main(int ARGC, char** ARGV) {
}
}
lowerG(&draftName);
+ uniqG(&draftName, '-');
prependG(&draftName, draftsDir"/");
pushG(&draftName, ".markdown");
//lv(draftName);
@@ -114,6 +149,7 @@ int main(int ARGC, char** ARGV) {
XSuccess;
}
elif (eqG(ARGV[1], "publish")) {
+ // copy draft to post dir
if (ARGC < 3) {
logE("publish failed. Missing path, usage:\n"
"forb publish PATH"
@@ -132,6 +168,26 @@ int main(int ARGC, char** ARGV) {
}
XSuccess;
}
+ elif (eqG(ARGV[1], "update")) {
+ // copy published post to post dir, the date field is updated
+ if (ARGC < 3) {
+ logE("update failed. Missing path, usage:\n"
+ "forb update PATH"
+ );
+ XFailure;
+ }
+ rangeFrom(i, 2, ARGC) {
+ if (not isPath(ARGV[i])) {
+ logE("Path %d '%s' not found", i, ARGV[i]);
+ }
+ else {
+ checkInput(publishedDir);
+ checkInput(PostsDir);
+ update(ARGV[i]);
+ }
+ }
+ XSuccess;
+ }
else {
logE("Command "BLD"'%s'"RST" not found or invalid parameters.\n\n"
, ARGV[1]);
@@ -140,6 +196,7 @@ int main(int ARGC, char** ARGV) {
}
}
+ // when there are no arguments, generate the blog
// check inputs
checkInput(configFile);
@@ -173,7 +230,7 @@ int main(int ARGC, char** ARGV) {
readFileWithFrontMatter(indexFile, indexCfg, indexf);
// read posts
- // list all md files in _posts
+ // list all md files in _posts and _published
cleanSmallArrayP(postsDir) = readDirG(rtSmallArrayt, PostsDir);
iter(postsDir, L) {
castS(l,L);
@@ -182,6 +239,16 @@ int main(int ARGC, char** ARGV) {
}
}
//lv(postsDir);
+ cleanSmallArrayP(pubDir) = readDirG(rtSmallArrayt, publishedDir);
+ iter(pubDir, L) {
+ castS(l,L);
+ if (not endsWithG(l, ".markdown")) {
+ delElemG(pubDir, iI(pubDir));
+ }
+ }
+ appendNFreeG(postsDir, dupG(pubDir));
+ compactG(postsDir);
+ sortG(postsDir);
// generate html code for each post for index.html
cleanAllocateSmallArray(postsIndex);
@@ -189,17 +256,38 @@ int main(int ARGC, char** ARGV) {
castS(l,L);
cleanCharP(postHtmlFile) = copyRngG(ssGet(l), 11, 0);
replaceG(&postHtmlFile, ".markdown", ".html", 1);
- prependG(l, PostsDir);
- setPG(postsDir, iterIndexG(postsDir), l);
+ // determine if file is in _posts or _published
+ if (hasG(pubDir, l)) {
+ // file is in _published
+ prependG(l, publishedDir"/");
+ }
+ else {
+ prependG(l, PostsDir"/");
+ }
+ setPG(postsDir, iI(postsDir), l);
cleanAllocateSmallJson(postCfg);
cleanAllocateSmallArray(postf);
readFileWithFrontMatter(ssGet(l), postCfg, postf);
- cleanCharP(postDate) = copyRngG($(postCfg, "date"), 0, 10);
+ // select first publish date for html path: root/category/YY/MM/DD/postName.html
+ cleanCharP(postDate) = hasG(postCfg, "firstPublishDate") ? copyRngG($(postCfg, "firstPublishDate"), 0, 10) : copyRngG($(postCfg, "date"), 0, 10);
replaceG(&postDate, "-", "/", 0);
cleanSmallArrayP(postUrlA) = createSA($(cfg, "baseurl"),
$(postCfg, "categories"), postDate, postHtmlFile);
cleanCharP(postUrl) = joinSG(postUrlA, "/");
- $(postCfg, "date")[10] = 0;
+ // 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
+ cleanCharP(date);
+ if (hasG(postCfg, "firstPublishDate")) {
+ if (eqG($(postCfg, "date"), $(postCfg, "firstPublishDate"))) goto onlyPublishDate;
+ // there is an update date
+ $(postCfg, "date")[10] = 0;
+ $(postCfg, "firstPublishDate")[10] = 0;
+ date = catS($(postCfg, "firstPublishDate"), " updated on ", $(postCfg, "date"));
+ }
+ else {
+ onlyPublishDate:
+ $(postCfg, "date")[10] = 0;
+ date = dupG($(postCfg, "date"));
+ }
cleanCharP(s) = formatS("<li>\n"
" <span class=\"post-meta\">%s</span>\n"
"\n"
@@ -207,7 +295,7 @@ int main(int ARGC, char** ARGV) {
" <a class=\"post-link\" href=\"%s\">%s</a>\n"
" </h2>\n"
"</li>\n"
- ,$(postCfg, "date"), postUrl, $(postCfg, "title"));
+ ,date, postUrl, $(postCfg, "title"));
pushG(postsIndex, s);
}
//cleanCharP(postsIndexS) = joinSG(postsIndex, "\n");
@@ -321,6 +409,62 @@ int main(int ARGC, char** ARGV) {
// clean tmp files
if (isPath("tmp.html")) rmAll("tmp.html");
if (isPath("tmp.md")) rmAll("tmp.md");
+
+
+ // list all md files in _posts
+ cleanSmallArrayP(postFiles) = readDirG(rtSmallArrayt, PostsDir);
+ iter(postFiles, L) {
+ castS(l,L);
+ if (not endsWithG(l, ".markdown")) {
+ delElemG(postFiles, iterIndexG(postFiles));
+ }
+ }
+ //lv(postFiles);
+
+ // move posts from _posts to _published
+ // add first publish date if needed
+ iter(postFiles, L) {
+ castS(p,L);
+ cleanCharP(postPath) = catS(PostsDir"/", ssGet(p));
+ cleanAllocateSmallArray(post);
+ readFileG(post, postPath);
+
+ char *status = "search front matter";
+ i32 dateIdx = -1;
+ iter(post, L) {
+ castS(l,L);
+ if (not eqG(status, "search front matter") and startsWithG(l, "firstPublishDate:"))
+ // found first publish date. Stop.
+ break;
+ if (not eqG(status, "search front matter") and eqG(l, "---")) {
+ if (dateIdx == -1) {
+ logE("Date not found in front matter, post is %m", p);
+ break;
+ }
+ cleanCharP(pDate) = replaceG($(post, dateIdx), "date:", "firstPublishDate:", 1);
+ injectG(post, iI(post), pDate);
+ break;
+ }
+ if (eqG(status, "date") and startsWithG(l, "date:")) {
+ dateIdx = iI(post);
+ status = "publishDate";
+ }
+ if (eqG(status, "search front matter") and eqG(l, "---")) {
+ status = "date";
+ }
+ }
+
+ // move post to _published directory
+ cleanCharP(pubPath) = catS(publishedDir"/", ssGet(p));
+ if (not writeFileG(post, pubPath)) {
+ logE("Could not write '%s', post is: %m", pubPath, p);
+ continue;
+ }
+ if (not rmAllG(postPath)) {
+ logE("Could not remove: %s", postPath);
+ continue;
+ }
+ }
}
void help(void) {
@@ -335,6 +479,7 @@ void help(void) {
);
}
+
/**
* publish a post in _drafts to _posts
*/
@@ -367,8 +512,7 @@ void publish(const char *path) {
//lv(path);
// create destination filename
- char *filename = basename(path);
- cleanCharP(dest) = catS(PostsDir, "/", date, "-", filename);
+ cleanCharP(dest) = catS(PostsDir"/", date, "-", basename(path));
//lv(dest);
// create date string for front matter
@@ -414,6 +558,77 @@ void publish(const char *path) {
/**
+ * update a post in _published to _posts
+ */
+void update(const char *path) {
+ // steps
+ // check extension
+ // read post
+ // create destination filename
+ // create date string for front matter
+ // change date string in front matter
+ // failed to find front matter or date
+ // save posts directory
+ // remove path
+
+ // check extension
+ if (not endsWithG(path, ".markdown")) {
+ logE("'%s' doesn't have the markdown extension. Stop.");
+ XFailure;
+ }
+
+ // read post
+ cleanAllocateSmallArray(post);
+ readFileG(post, path);
+
+ // create destination filename
+ cleanCharP(dest) = catS(PostsDir"/", basename(path));
+ //lv(dest);
+
+ // create date string for front matter
+ cleanCharP(date) = getCurrentDateYMD();
+ date[10] = ' ';
+ prependG(&date, "date: ");
+ //lv(date);
+
+ // change date string in front matter
+ char *status = "search front matter";
+ iter(post, L) {
+ castS(l, L);
+ if (eqG(status, "stop at date") and startsWithG(l, "date:")) {
+ setG(post, iI(post), date);
+ goto publishPost;
+ }
+ if (eqG(status, "stop at date") and eqG(l, "---")) {
+ logE("date not found in front matter, path is: %s", path);
+ ret;
+ }
+ if (eqG(status, "search front matter") and eqG(l, "---")) {
+ status = "stop at date";
+ }
+ }
+
+ // failed to find front matter or date
+ logE("front matter or date not found, path is %s", path);
+ ret;
+
+ publishPost:
+ // save posts directory
+ if (not writeFileG(post, dest)) {
+ logE("Could not write '%s', path is: %s", dest, path);
+ ret;
+ }
+
+ // remove path
+ if (not rmAllG(path)) {
+ logE("Could not remove: %s", path);
+ }
+
+ logP("Update already published post in %s", dest);
+}
+
+
+/**
* read file filename with front matter and text (any type of text)
*
* \return
@@ -481,7 +696,11 @@ bool readFileWithFrontMatter(char *filename, smallJsont *kv, smallArrayt *lines)
* The url format is "/category/YYYY/MM/DD/filename.html"
*/
char *postToUrl(char *filename) {
- cleanCharP(fn) = catS(PostsDir,filename,".markdown");
+ cleanCharP(fn) = catS(PostsDir"/",filename,".markdown");
+ if (not isPath(fn)) {
+ free(fn);
+ fn = catS(publishedDir"/",filename,".markdown");
+ }
cleanAllocateSmallArray(pf);
cleanAllocateSmallJson(pCfg);
@@ -490,7 +709,8 @@ char *postToUrl(char *filename) {
cleanCharP(postHtmlFile) = copyRngG(fn, 18, 0);
replaceG(&postHtmlFile, ".markdown", ".html", 1);
- cleanCharP(postDate) = copyRngG($(pCfg, "date"), 0, 10);
+ // select first publish date for html path: root/category/YY/MM/DD/postName.html
+ cleanCharP(postDate) = hasG(pCfg, "firstPublishDate") ? copyRngG($(pCfg, "firstPublishDate"), 0, 10) : copyRngG($(pCfg, "date"), 0, 10);
replaceG(&postDate, "-", "/", 0);
char *pUrl = catS("/",$(pCfg,"categories"),"/",postDate,"/",postHtmlFile);
@@ -511,6 +731,7 @@ bool generateAPageOrAPost(char *filename, smallJsont *cfg, smallArrayt *postsFee
// add layout and html code in sub layout
// TODO list pages on first line of all pages
// configure destination path, post url, date and description
+ // detect if generating a post or a page to select path and parameters
// replace keys with values
// generate feed
// write post html page
@@ -849,22 +1070,37 @@ bool generateAPageOrAPost(char *filename, smallJsont *cfg, smallArrayt *postsFee
cleanAllocateSmallJson(aboutCfg);
readFileWithFrontMatter("about.md", aboutCfg, about);
- char *sitePages = catS("<a class=\"page-link\" href=\"", $(cfg, "baseurl"), $(aboutCfg, "permalink"), "\">", $(aboutCfg, "title"), "</a>");
+ char *sitePages = catS("<a class=\"page-link\" href=\"", $(cfg, "baseurl"), $(aboutCfg, "permalink"), "\">", $(aboutCfg, "title"), "</a>");
// configure destination path, post url, date and description
- cleanCharP(dst) = null;
- char *pUrl = null;
+ cleanCharP(dst) = null;
+ char *pUrl = null;
baset *description = null;
- if (startsWithG(filename, PostsDir)) {
- cleanCharP(postHtmlFile) = copyRngG(filename, 18, 0);
+ // 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
+ cleanCharP(date) = null;
+ // detect if generating a post or a page to select path and parameters
+ if (startsWithG(filename, PostsDir"/") or startsWithG(filename, publishedDir"/")) {
+ cleanCharP(postHtmlFile) = startsWithG(filename, PostsDir"/") ? copyRngG(filename, 18, 0) : copyRngG(filename, 22, 0);
replaceG(&postHtmlFile, ".markdown", ".html", 1);
- cleanCharP(postDate) = copyRngG($(pCfg, "date"), 0, 10);
+ // select first publish date for html path: root/category/YY/MM/DD/postName.html
+ cleanCharP(postDate) = hasG(pCfg, "firstPublishDate") ? copyRngG($(pCfg, "firstPublishDate"), 0, 10) : copyRngG($(pCfg, "date"), 0, 10);
replaceG(&postDate, "-", "/", 0);
dst = catS(siteDir,"/",$(pCfg, "categories"),"/",postDate,"/",postHtmlFile);
pUrl = catS($(cfg, "url"),$(cfg, "baseurl"),"/",
$(pCfg,"categories"),"/",postDate,"/",postHtmlFile);
- $(pCfg, "date")[10] = 0;
+ if (hasG(pCfg, "firstPublishDate")) {
+ if (eqG($(pCfg, "date"), $(pCfg, "firstPublishDate"))) goto onlyPublishDate;
+ // there is an update date
+ $(pCfg, "date")[10] = 0;
+ $(pCfg, "firstPublishDate")[10] = 0;
+ date = catS($(pCfg, "firstPublishDate"), " updated on ", $(pCfg, "date"));
+ }
+ else {
+ onlyPublishDate:
+ $(pCfg, "date")[10] = 0;
+ date = dupG($(pCfg, "date"));
+ }
iter(pf, L) {
castS(l,L);
@@ -897,8 +1133,8 @@ bool generateAPageOrAPost(char *filename, smallJsont *cfg, smallArrayt *postsFee
setNFreeG(pageValues, "_\%site.email", getNDupO(cfg, "email"));
setNFreeG(pageValues, "_\%site.github_username", getNDupO(cfg, "github_username"));
setNFreeG(pageValues, "_\%site.twitter_username", getNDupO(cfg, "twitter_username"));
- if (startsWithG(filename, PostsDir)) {
- setG(pageValues, "_\%date", $(pCfg, "date"));
+ if (startsWithG(filename, PostsDir"/") or startsWithG(filename, publishedDir"/")) {
+ setG(pageValues, "_\%date", date);
}
//lv(pageValues);
@@ -920,7 +1156,7 @@ bool generateAPageOrAPost(char *filename, smallJsont *cfg, smallArrayt *postsFee
" <guid isPermaLink=\"true\">%s</guid>\n"
" <category>%s</category>\n"
"</item>\n",
- $(pCfg, "title"), postContent, $(pCfg, "date"), $(pageValues, "_\%url"), $(pageValues, "_\%url"),
+ $(pCfg, "title"), postContent, date, $(pageValues, "_\%url"), $(pageValues, "_\%url"),
$(pCfg, "categories")));
}
diff --git a/forb.h b/forb.h
@@ -0,0 +1,7 @@
+
+#define draftTemplate "---\n"\
+ "layout: post\n"\
+ "title: \"$TITLE\"\n"\
+ "categories: default\n"\
+ "---\n"\
+ "Set a relevant category for your post above and write your post text here (after the YML front matter)."