sbrs

Simple blog and RSS system
git clone git://git.christosmarg.xyz/sbrs.git
Log | Files | Refs | README | LICENSE

commit 577b0ea3b45f656ada17603ad9aaf5d48b6cb893
parent f37f577089669a2247994ad70caefb8a51e2a2d9
Author: Christos Margiolis <christos@margiolis.net>
Date:   Thu, 14 Jan 2021 18:42:59 +0200

 bug fix and POSIX read function implementation

Diffstat:
MLICENSE | 24+++++++++++++-----------
MMakefile | 2+-
MREADME | 6+++---
Ablog.html | 12++++++++++++
Dblogindex.html | 12------------
Mindex.html | 2+-
Mrss.xml | 2+-
Msbrs | 230++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Msbrs.1 | 47++++++++++++++++++++++++-----------------------
Mtemplate.html | 2+-
10 files changed, 201 insertions(+), 138 deletions(-)

diff --git a/LICENSE b/LICENSE @@ -13,17 +13,19 @@ modification, are permitted provided that the following conditions are met: this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -* Neither the name of images.weserv.nl nor the names of its +* Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile @@ -21,7 +21,7 @@ all: ${BIN} dist: ${MKDIR} ${DIST} - ${CP} -R ${BIN} ${MAN1} blogindex.html index.html LICENSE Makefile\ + ${CP} -R ${BIN} ${MAN1} blog.html index.html LICENSE Makefile\ README.md rss.xml styles.css template.html ${DIST} ${TAR} ${DIST}.tar ${DIST} ${GZIP} ${DIST}.tar diff --git a/README b/README @@ -14,12 +14,12 @@ to exist with the same names. ├── blog | └── here reside your blog posts ├── index.html - ├── blogindex.html + ├── blog.html ├── template.html └── rss.xml -Inside 'index.html', 'blogindex.html' and 'rss.xml' sbrs will search for -<!--BLOG--> in order to put the blog post listings and RSS feed below it. +Inside 'index.html', 'blog.html' and 'rss.xml' sbrs will search for +<!--SBRS--> in order to put the blog post listings and RSS feed below it. The 'template.html' file is how you want your blog post's page to look like. See my own 'template.html' for more. The 'TITLE', 'HEADER' and 'AUTHOR' diff --git a/blog.html b/blog.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <link rel="stylesheet" href="styles.css"/> + <title>Blog Index</title> +</head> + +<body> + <!--SBRS--> +</body> +</html> diff --git a/blogindex.html b/blogindex.html @@ -1,12 +0,0 @@ -<!DOCTYPE html> -<html> -<head> - <meta charset="UTF-8"> - <link rel="stylesheet" href="styles.css"/> - <title>Blog Index</title> -</head> - -<body> - <!--BLOG--> -</body> -</html> diff --git a/index.html b/index.html @@ -9,7 +9,7 @@ <body> <h2>Recent blog posts</h2> <ul> - <!--BLOG--> + <!--SBRS--> </ul> </body> </html> diff --git a/rss.xml b/rss.xml @@ -6,7 +6,7 @@ <description>Sample Description</description> <link>https://yourwebsite.com/rss.xml</link> -<!--BLOG--> +<!--SBRS--> </channel> </rss> diff --git a/sbrs b/sbrs @@ -5,7 +5,7 @@ WEBSITE="https://christosmarg.xyz" AUTHOR="Christos Margiolis" BLOGDIR="blog" DRAFTDIR=".drafts" -BLOGINDEX="blogindex.html" +BLOGINDEX="blog.html" INDEX="index.html" RSSFILE="rss.xml" TEMPLATE="template.html" @@ -23,17 +23,16 @@ main() { fi case ${1} in - -n*) newpost ;; - -p*) listposts "${DRAFTDIR}" && publish ;; - -e*) listposts "${DRAFTDIR}" && ${EDITOR} "${DRAFTDIR}/${blogpost}.html" ;; - -v*) listposts "${DRAFTDIR}" && view ;; - -t*) listposts "${DRAFTDIR}" && delete "${DRAFTDIR}" ;; - # edit rss too - -r*) listposts "${BLOGDIR}" && ${EDITOR} "${BLOGDIR}/${blogpost}.html" ;; - -c*) listposts "${BLOGDIR}" && titlechange ;; - -o*) listposts "${BLOGDIR}" && ${BROWSER} "${BLOGDIR}/${blogpost}.html" ;; - -d*) listposts "${BLOGDIR}" && delete "${BLOGDIR}" ;; - -l*) listposts "${BLOGDIR}" ;; + -n) newpost ;; + -p) listposts "${DRAFTDIR}" && publish ;; + -e) listposts "${DRAFTDIR}" && ${EDITOR} "${DRAFTDIR}/${blogpost}.html" ;; + -v) listposts "${DRAFTDIR}" && view ;; + -t) listposts "${DRAFTDIR}" && delete "${DRAFTDIR}" ;; + #-r) listposts "${BLOGDIR}" && revise ;; + -c) listposts "${BLOGDIR}" && titlechange ;; + -o) listposts "${BLOGDIR}" && ${BROWSER} "${BLOGDIR}/${blogpost}.html" ;; + -d) listposts "${BLOGDIR}" && delete "${BLOGDIR}" ;; + -l) listposts "${BLOGDIR}" ;; *) usage ;; esac } @@ -42,12 +41,25 @@ err() { echo "${0##*/}: $@" && exit } +# We could be using `read -erp` but the -e and -p options don't work +# or exist in all shells (e.g OpenBSD's `/bin/sh`). +xread() { + printf "%s" "${1}" && read -r ${2} +} + +# This is a POSIX compliant version of `sed -i` since non-GNU +# implementations require a backup file with the -i option. +# In this case, we make the backup files and immidiately delete +# them afterwards because we probably don't need them. +# We're using the extension `.psedbak` since we want to delete +# only the files affected by `psed` and nothing else, and that's +# an easy way to make sure. psed() { - sed -i.orig "$@" && rm *.orig "${BLOGDIR}/*.orig" 2> /dev/null + sed -i.psedbak "$@" && rm *.psedbak ${BLOGDIR}/*.psedbak 2>/dev/null } confirmact() { - read -erp "${1}" act && test "${act}" = "${2}" || exit + xread "${1}" act && test "${act}" = "${2}" || exit 1 } list() { @@ -55,120 +67,166 @@ list() { awk -F/ '{print $NF}' } +# Create a proper file name for the post (e.g Hello world! -> hello-world). +titlefmt() { + echo "${1}" | iconv -cf UTF-8 -t ASCII//TRANSLIT | + tr -d '[:punct:]' | tr '[:upper:]' '[:lower:]' | tr ' ' '-' +} + +titleget() { + grep "<title>" "${1}" | sed "s/<title>//;s/<\/title>//;s/ *//;" +} + +titlecheck() { + # XXX: maybe should not have `BLOGDIR` hardcoded + test -f "${BLOGDIR}/${1}.html" && err "file exists already" + test -z "${1}" && err "empty title" +} + +rmcontents() { + ls "${1}" | grep -x "${blogpost}\...*" | sed "s/^/${1}\//" | xargs rm +} + +# -l option listposts() { - printf "Listing posts in: %s (total: %d)\n" "${1}" "$(list "${1}" | wc -l)" - list "${1}" | nl - nposts=$(expr $(list "${1}" | wc -l)) - test ${nposts} -eq 0 && err "no posts available in: ${1}" - read -erp "Choose a post to by number: " num + dir="${1}" + nposts=$(expr $(list "${dir}" | wc -l)) + + test ${nposts} -eq 0 && err "no posts available in: ${dir}" + printf "Listing posts in: %s (total: %d)\n" "${dir}" "${nposts}" + list "${dir}" | nl + + xread "Choose a post to by number: " num + if ! test -z "$(echo ${num} | grep -E "^[0-9]+$" | grep -v "^0")"; then test $(expr ${num}) -gt ${nposts} && err "no post selected" - blogpost=$(list "${1}" | nl | grep -w "${num}" | awk '{print $2}') + blogpost=$(list "${dir}" | nl | grep -w "${num}" | awk '{print $2}') blogpost=${blogpost%.*} else err "no post selected" fi } +# -n options newpost() { mkdir -p "${DRAFTDIR}" - read -erp "Title: " title + xread "Title: " title test -z "${title}" && err "please specify a title" + blogpost=$(titlefmt "${title}") - test -f "${BLOGDIR}/${blogpost}.html" && err "file exists already" + titlecheck ${blogpost} $EDITOR "${DRAFTDIR}/${blogpost}.html" + sed "s/TITLE/${title}/g;s/HEADER/${title}/g;s/AUTHOR/${AUTHOR}/g;" "${TEMPLATE}" \ > "${DRAFTDIR}/${blogpost}.final.html" } -titlefmt() { - echo "${1}" | iconv -cf UTF-8 -t ASCII//TRANSLIT | - tr -d '[:punct:]' | tr '[:upper:]' '[:lower:]' | tr ' ' '-' -} - -titleget() { - grep "<title>" "${1}" | sed "s/<title>//;s/<\/title>//;s/ *//;" -} - +# -c option titlechange() { - read -erp "Give post a new title: " newtitle + xread "New title: " newtitle confirmact "Are you sure (y/N)? " "y" + oldtitle=$(titleget "${BLOGDIR}/${blogpost}.html") newtitlefmt=$(titlefmt "${newtitle}") - test -f "${BLOGDIR}/${newtitle}fmt.html" && err "file exists already" + titlecheck ${newtitlefmt} - psed "s/${blogpost}/${newtitle}fmt/g;s/${oldtitle}/${newtitle}/g" \ + # Adding `.html` so that it doesn't edit every single + # instance of the title, because in case the title is + # something very generic like 'and', it'll change every + # single instance of `and` in the file, and we don't want that. :-) + # The < and > serve the same purpose, as `oldtitle` is + # probably going to be inside an <a> or <title> tag. + psed "s/${blogpost}.html/${newtitlefmt}.html/g;s/>${oldtitle}</>${newtitle}</g" \ "${BLOGDIR}/${blogpost}.html" "${INDEX}" "${BLOGINDEX}" "${RSSFILE}" && - mv "${BLOGDIR}/${blogpost}.html" "${BLOGDIR}/${newtitle}fmt.html" && + mv "${BLOGDIR}/${blogpost}.html" "${BLOGDIR}/${newtitlefmt}.html" && echo "Title changed successfully: ${oldtitle} -> ${newtitle}" } -rmcontents() { - # replace with find(1) - ls "${1}" | grep -x "${blogpost}\...*" | sed "s/^/${1}\//" | xargs rm -} - -rmlastentry() { - indexentries=$(sed "1,/<\!--BLOG-->/d" "${INDEX}" | grep "<li>") - if test $(expr $(echo "${indexentries}" | wc -l)) -gt 7; then - lastentry=$(echo "${indexentries}" | tail -2 | head -1) && - psed "s|${lastentry}||;" "${INDEX}" - fi -} - -blogindexupdate() { - dateid=$(date '+%b %Y' | sed 's/\ //' | tr '[:upper:]' '[:lower:]') - if test -z "$(grep "${dateid}" "${BLOGINDEX}")"; then - datename=$(date '+%B %Y') - psed "/<\!--BLOG-->/a\\ -\ <h2 id=\\"${dateid}\\">${datename}<\\/h2> \\ -\ <ul> \\ -\ <\\!--BLOG ${datename}--> \\ -\ <\\/ul> \\ -" "${BLOGINDEX}" - fi -} - +# -v option view() { cat "${DRAFTDIR}/${blogpost}.final.html" > "${DRAFTDIR}/${blogpost}.final-view.html" - psed "/<\!--BLOG-->/r ${DRAFTDIR}/${blogpost}.html" "${DRAFTDIR}/${blogpost}.final-view.html" + psed "/<\!--SBRS-->/r ${DRAFTDIR}/${blogpost}.html" "${DRAFTDIR}/${blogpost}.final-view.html" ${BROWSER} "${DRAFTDIR}/${blogpost}.final-view.html" } +# -d and -t options delete() { + dir="${1}" + confirmact "Are you sure you want to delete \"${blogpost}\" (y/N)? " "y" - if test "${1}" = "${BLOGDIR}"; then + if test "${dir}" = "${BLOGDIR}"; then psed "/${blogpost}/d" "${INDEX}" "${BLOGINDEX}" psed "/<\!--BEGIN ${blogpost}-->/,/<\!--END ${blogpost}-->/d" "${RSSFILE}" fi - rmcontents "${1}" && echo "Removed ${blogpost}." + rmcontents "${dir}" && echo "Removed ${blogpost}." } +# -p option +# XXX: needs a good cleanup publish() { confirmact "Publish post (y/N)? " "y" title=$(titleget "${DRAFTDIR}/${blogpost}.final.html") - # bad? + + # Make the final HTML file. psed "s/^/\ \ \ \ \ \ \ \ /" "${DRAFTDIR}/${blogpost}.html" - psed "/<\!--BLOG-->/r ${DRAFTDIR}/${blogpost}.html" "${DRAFTDIR}/${blogpost}.final.html" - sed "s/</\&lt;/g;s/>/\&gt;/g;" "${DRAFTDIR}/${blogpost}.html" > "${DRAFTDIR}/${blogpost}.xml" + psed "/<\!--SBRS-->/r ${DRAFTDIR}/${blogpost}.html" "${DRAFTDIR}/${blogpost}.final.html" + + # Convert HTML tags to XML format. We're also wrapping all <pre> tags + # in a CDATA block so that RSS readers can render it properly. + sed "s/</\&lt;/g;s/>/\&gt;/g;s/\&lt;pre\&gt;/\&lt;pre\&gt;<\![CDATA[/g;s/\&lt;\/pre\&gt;/]]>\&lt;\/pre\&gt;/g" \ + "${DRAFTDIR}/${blogpost}.html" > "${DRAFTDIR}/${blogpost}.xml" + cp "${DRAFTDIR}/${blogpost}.final.html" "${BLOGDIR}/${blogpost}.html" + # Prepare the blog entry for the index files. printf "\t\t<li>%s &ndash; <a href=\"%s\">%s</a></li>\n" \ "$(date '+%Y %b %d')" "${BLOGDIR}/${blogpost}.html" "${title}" | expand -t8 > "${DRAFTDIR}/${blogpost}.final-htmlentry" + + # Prepare the RSS entry. rsscreate | expand -t8 > "${DRAFTDIR}/${blogpost}.final-rssentry" - # using || because of psed - blogindexupdate - psed "/<\!--BLOG $(date '+%B %Y')-->/r ${DRAFTDIR}/${blogpost}.final-htmlentry" \ + # Update blog index to include the new post. + # In case there is not section for the current month, create that too. + dateid=$(date '+%b %Y' | sed 's/\ //' | tr '[:upper:]' '[:lower:]') + + if test -z "$(grep "${dateid}" "${BLOGINDEX}")"; then + datename=$(date '+%B %Y') + psed "/<\!--SBRS-->/a\\ +\ <h2 id=\\"${dateid}\\">${datename}<\\/h2> \\ +\ <ul> \\ +\ <\\!--SBRS ${datename}--> \\ +\ <\\/ul> \\ +" "${BLOGINDEX}" + fi + + # Using || because of `psed`. + psed "/<\!--SBRS $(date '+%B %Y')-->/r ${DRAFTDIR}/${blogpost}.final-htmlentry" \ "${BLOGINDEX}" || echo "Blogindex... done." - psed "/<\!--BLOG-->/r ${DRAFTDIR}/${blogpost}.final-htmlentry" "${INDEX}" || echo "Index... done" - psed "/<\!--BLOG-->/r ${DRAFTDIR}/${blogpost}.final-rssentry" "${RSSFILE}" || echo "RSS... done" - rmlastentry || echo "Removing last entry from index file... done" + + psed "/<\!--SBRS-->/r ${DRAFTDIR}/${blogpost}.final-htmlentry" "${INDEX}" || echo "Index... done" + psed "/<\!--SBRS-->/r ${DRAFTDIR}/${blogpost}.final-rssentry" "${RSSFILE}" || echo "RSS... done" + + # Remove the last blog entry from `index.html`. + # Get only what's inside <!--SBRS--> and </ul>. Will not work on anything except + # my setup. FIXME + indexentries=$(sed '1,/<\!--SBRS-->/d;/<\/ul>/,$d' "${INDEX}" | grep "<li>") + + if test $(expr $(echo "${indexentries}" | wc -l)) -gt 7; then + lastentry=$(echo "${indexentries}" | tail -2 | head -1) && + psed "s|${lastentry}||;" "${INDEX}" + fi + rmcontents "${DRAFTDIR}" && echo "Cleaning up ${DRAFTDIR}... done" echo "Published ${blogpost}." } +# -r option +#revise() { + #${EDITOR} "${BLOGDIR}/${blogpost}.html" + #psed "/<\!--BEGIN ${blogpost}-->/,/<\!--END ${blogpost}-->/d" "${RSSFILE}" +#} + rsscreate() { printf "<!--BEGIN %s-->\n" "${blogpost}" printf "<item>" @@ -183,18 +241,20 @@ rsscreate() { } usage() { - printf "Usage: ${0##*/} OPTION\n\n" - printf "options:\n" - printf " -n\tnew post\n" - printf " -p\tpublish draft post\n" - printf " -e\tedit draft post\n" - printf " -v\tview draft post in browser\n" - printf " -t\tdelete draft post\n" - printf " -r\trevise published post\n" - printf " -c\tchange title\n" - printf " -o\tview published post in browser\n" - printf " -d\tdelete published post\n" - printf " -l\tlist all published posts\n" + printf "usage: ${0##*/} {-n | -p | -e | -v | -t | -r | -c | -o | -d | -l}\n\n" 1>&2 + printf "options:\n" 1>&2 + printf " -n\tnew post\n" 1>&2 + printf " -p\tpublish draft post\n" 1>&2 + printf " -e\tedit draft post\n" 1>&2 + printf " -v\tview draft post in browser\n" 1>&2 + printf " -t\tdelete draft post\n" 1>&2 + printf " -r\trevise published post\n" 1>&2 + printf " -c\tchange title\n" 1>&2 + printf " -o\tview published post in browser\n" 1>&2 + printf " -d\tdelete published post\n" 1>&2 + printf " -l\tlist all published posts\n" 1>&2 + + exit 1 } main "${1}" diff --git a/sbrs.1 b/sbrs.1 @@ -6,36 +6,37 @@ .Nd simple blog and RSS system .Sh SYNOPSIS .Nm -.Op Fl n Ar new post -.Op Fl p Ar publish draft -.Op Fl e Ar edit draft -.Op Fl v Ar view draft -.Op Fl t Ar delete draft -.Op Fl r Ar revise published -.Op Fl c Ar change title -.Op Fl o Ar view published -.Op Fl d Ar delete published -.Op Fl l Ar list all published +.Bro +.Fl n | +.Fl p | +.Fl e | +.Fl v | +.Fl t | +.Fl r | +.Fl c | +.Fl o | +.Fl d | +.Fl l .Sh DESCRIPTION .Nm generates a blog post page and RSS feed. .Sh OPTIONS The options are as follows: .Bl -tag -width Ds -.It Fl n Ar new post +.It Fl n Prompt the user to write a new blog post in HTML. .Nm will open the editor set in the 'EDITOR' environmental variable. In case it is not set, it will set 'EDITOR' .Xr Vim 1 -.It Fl p Ar publish draft +.It Fl p Publish one of the posts stored in the '.drafts' directory. After a post has been published, it will be deleted from '.drafts' along with all the files related to it and will be moved to 'blog'. This option also automatically creates an RSS feed for the post. -.It Fl e Ar edit draft +.It Fl e Edit one of the posts stored in the '.drafts' directory. -.It Fl v Ar view draft +.It Fl v View one of the posts stored in the '.drafts' directory. .Nm creates a temporary file that looks like the finished post, but is deleted @@ -43,26 +44,26 @@ after .Nm has exited. The 'BROWSER' environmental variable has to be set in order for this option to work. -.It Fl t Ar delete draft +.It Fl t Delete one of the posts stored in the '.drafts' directory. -.It Fl r Ar revise published +.It Fl r Revise a published post stored in the 'blog' directory. After you're done revising you don't need to re-publish the post again, since .Nm edits the post directly. -.It Fl c Ar change title +.It Fl c Change a published post's title. This option will replace every instance of the old title with the new one both. The filename will also be changed. -.It Fl o Ar view published +.It Fl o View a published post stored in the 'blog' directory on your default browser. Same with the -v option; the 'BROWSER' environmental variable has to be set in order for this option to work. -.It Fl d Ar delete published +.It Fl d Delete a published post stored in the 'blog' directory. This option will delete the post from the blog's index file, the RSS feed, as well as the main page in case it is found there too. -.It Fl l Ar list all published +.It Fl l List all posts stored in the 'blog' directory. .El .Pp @@ -81,14 +82,14 @@ names. ├── blog | └── here reside your blog posts ├── index.html - ├── blogindex.html + ├── blog.html ├── template.html └── rss.xml .Pp -Inside 'index.html', 'blogindex.html' and 'rss.xml' +Inside 'index.html', 'blog.html' and 'rss.xml' .Nm will search for -<!--BLOG--> in order to put the blog post listings and RSS feed below it. +<!--SBRS--> in order to put the blog post listings and RSS feed below it. .Pp The 'template.html' file is how you want your blog post's page to look like. See my own 'template.html' for more. The 'TITLE', 'HEADER' and 'AUTHOR' diff --git a/template.html b/template.html @@ -9,7 +9,7 @@ <body> <h1>HEADER<h1> - <!--BLOG--> + <!--SBRS--> <footer>by <a href="../index.html">AUTHOR</a></footer> </body>