diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d7ce61d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +coverage +build \ No newline at end of file diff --git a/.shellspec b/.shellspec new file mode 100644 index 0000000..0d6340f --- /dev/null +++ b/.shellspec @@ -0,0 +1,5 @@ +--require spec_helper +--kcov-options "--include-path=. --path-strip-level=1" +--kcov-options "--exclude-pattern=/.shellspec,/spec/,/coverage/,/report/,/build/,/.git/" +--kcov-options "--include-pattern=.sh" +--shell "/bin/bash" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6afba49 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,75 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Three awk scripts to create a version of page for web/gemini/gopher without the need of any external tool + +### Changed + +- The documentation is now split in many files (Changelog, License, Contributing...) +- The funcions is now split in many files for easy of editing +- The Makefile script concatenate all the functions in a single file + +### Deprecated + +### Removed + +### Fixed + +### Security + +- The scripts now are shellcheck validated +- Unit tests based on shellspec + + +# Old Changelog + +- 2.8 Bugfixes
+ Slavic language support thanks to Tomasz Jadowski
+ Removed the now defunct Twitter JSON API share count
+ Support for static, not managed by bashblog html files
+- 2.7 Store post date on a comment in the html file (#96).
+ On rebuild, the post date will be synchronised between comment date and file date, with precedence for comment date. +- 2.6 Support for multiple authors, use a different `.config` for each one +- 2.5 Massive code cleanup by Martijn Dekker
+ 'tags' command
+ The word 'posts' in the tag list (both website and command) now has a singular form, check out `template_tags_posts_singular` +- 2.4 Added Twitter summaries metadata for posts (#36) +- 2.3.3 Removed big comment header.
+ Added option to display tags for cut articles on index pages (#61)
+ Cleaned up "all posts" page (#57) +- 2.3.2 Option to use topsy instead of twitter for references +- 2.3.1 Cookieless Twitter option +- 2.3 Intelligent tag rebuilding and Markdown by default +- 2.2 Flexible post title -> filename conversion +- 2.1 Support for tags/categories.
+ 'delete' command +- 2.0.3 Support for other analytics code, via external file +- 2.0.2 Fixed bug when $body_begin_file was empty.
+ Added extra line in the footer linking to the github project +- 2.0.1 Allow personalized header/footer files +- 2.0 Added Markdown support.
+ Fully support BSD date +- 1.6.4 Fixed bug in localized dates +- 1.6.3 Now supporting BSD date +- 1.6.2 Simplified some functions and variables to avoid duplicated information +- 1.6.1 'date' fix when hours are 1 digit. +- 1.6.0 Disqus comments. External configuration file. Check of 'date' command version. +- 1.5.1 Misc bugfixes and parameter checks +- 1.5 Đurađ Radojičić (djura-san) refactored some code and added flexibility and i18n +- 1.4.2 Now issues are handled at Github +- 1.4.1 Some code refactoring +- 1.4 Using twitter for comments, improved 'rebuild' command +- 1.3 'edit' command +- 1.2.2 Feedburner support +- 1.2.1 Fixed the timestamps bug +- 1.2 'list' command +- 1.1 Draft and preview support +- 1.0 Read http://is.gd/Bkdoru diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4c809a3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +License +------- + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . \ No newline at end of file diff --git a/Makefile b/Makefile index fc5c215..07a442b 100644 --- a/Makefile +++ b/Makefile @@ -2,11 +2,27 @@ PREFIX ?= /usr/local BINDIR ?= $(PREFIX)/bin install: - $(info Installing the executable to $(BINDIR)) - @install -Dm755 bb.sh $(BINDIR)/bb + $(info Generating the main executable) + @mkdir -p build + @awk '1{ if (NR > 1) { print x }};/### BEGIN SOURCEFILES/{exit};{x=$$0}' tildelog.sh > ./build/tildelog + @grep -hv '^[[:space:]]*declare\|^#!' ./lib/*.sh >> ./build/tildelog + @awk 'p;/### END SOURCEFILES/{p=1}' tildelog.sh >> ./build/tildelog + $(info Installing the executables to $(BINDIR)) + @install -Dm755 ./build/tildelog $(BINDIR)/tildelog + @install -Dm755 md2html.awk $(BINDIR)/md2html.awk + @install -Dm755 md2gemini.awk $(BINDIR)/md2gemini.awk + @install -Dm755 md2gopher.awk $(BINDIR)/md2gopher.awk uninstall: $(info Removing the executable from $(BINDIR)) - @rm -f $(BINDIR)/bb + @rm -f $(BINDIR)/tildelog + @rm -f $(BINDIR)/md2html.awk + @rm -f $(BINDIR)/md2gemini.awk + @rm -f $(BINDIR)/md2gopher.awk -.PHONY: install uninstall +test: + $(info Running shellspec tests) + @rm -rf ./coverage + @shellspec --kcov + +.PHONY: install uninstall test diff --git a/README.md b/README.md index ad77f69..0c5791a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -bashblog +Tildelog ======== This is the source for the customized bashblog used on [tilde.team](https://tilde.team). We have a [wiki page](https://tilde.team/wiki/?page=tildeblogs) with more details on our specific setup. @@ -117,86 +117,3 @@ Detailed features Read the Changelog section for more updates or [check out the news on my blog](http://cfenollosa.com/blog/tag_bashblog.html) -Contributing ------------- - -Bashblog started at 500 SLOC and it now has hit the 1000 SLOC barrier. -If we want to keep the code minimal and understandable, we need to make the difficult effort to restrain ourselves -from adding too many features. - -All bugfixes are welcome, but brand new features need to be strongly justified to get into the main tree. -Every new request will be honestly and civilly discussed on the comments. -As a guideline, pull requests should: - -- Fix a use case for some people (e.g. internationalization) -- Add a use case which is arguably very common (e.g. disqus integration for comments) -- Be very small when possible (a couple lines of code) -- Don't require a significant rewrite of the code (Don't break `create_html_file()` or `write_entry()`, etc) -- It must work on Linux, BSD and Mac. Beware of using GNU coreutils with non-POSIX flags (i.e. `date` or `grep`) -- Follow the UNIX philosophy: do one thing and do it well, rely on third party software for external features, etc -- **Always** keep backwards compatibility when using the default configuration - - -Changelog ---------- - -- 2.8 Bugfixes
- Slavic language support thanks to Tomasz Jadowski
- Removed the now defunct Twitter JSON API share count
- Support for static, not managed by bashblog html files
-- 2.7 Store post date on a comment in the html file (#96).
- On rebuild, the post date will be synchronised between comment date and file date, with precedence for comment date. -- 2.6 Support for multiple authors, use a different `.config` for each one -- 2.5 Massive code cleanup by Martijn Dekker
- 'tags' command
- The word 'posts' in the tag list (both website and command) now has a singular form, check out `template_tags_posts_singular` -- 2.4 Added Twitter summaries metadata for posts (#36) -- 2.3.3 Removed big comment header.
- Added option to display tags for cut articles on index pages (#61)
- Cleaned up "all posts" page (#57) -- 2.3.2 Option to use topsy instead of twitter for references -- 2.3.1 Cookieless Twitter option -- 2.3 Intelligent tag rebuilding and Markdown by default -- 2.2 Flexible post title -> filename conversion -- 2.1 Support for tags/categories.
- 'delete' command -- 2.0.3 Support for other analytics code, via external file -- 2.0.2 Fixed bug when $body_begin_file was empty.
- Added extra line in the footer linking to the github project -- 2.0.1 Allow personalized header/footer files -- 2.0 Added Markdown support.
- Fully support BSD date -- 1.6.4 Fixed bug in localized dates -- 1.6.3 Now supporting BSD date -- 1.6.2 Simplified some functions and variables to avoid duplicated information -- 1.6.1 'date' fix when hours are 1 digit. -- 1.6.0 Disqus comments. External configuration file. Check of 'date' command version. -- 1.5.1 Misc bugfixes and parameter checks -- 1.5 Đurađ Radojičić (djura-san) refactored some code and added flexibility and i18n -- 1.4.2 Now issues are handled at Github -- 1.4.1 Some code refactoring -- 1.4 Using twitter for comments, improved 'rebuild' command -- 1.3 'edit' command -- 1.2.2 Feedburner support -- 1.2.1 Fixed the timestamps bug -- 1.2 'list' command -- 1.1 Draft and preview support -- 1.0 Read http://is.gd/Bkdoru - - -License -------- - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - diff --git a/bb.sh b/bb.sh deleted file mode 100755 index d5008d4..0000000 --- a/bb.sh +++ /dev/null @@ -1,1196 +0,0 @@ -#!/usr/bin/env bash - -# BashBlog, a simple blog system written in a single bash script -# (C) Carlos Fenollosa , 2011-2016 and contributors -# https://github.com/carlesfe/bashblog/contributors -# Check out README.md for more details - -# Global variables -# It is recommended to perform a 'rebuild' after changing any of this in the code - -# Config file. Any settings "key=value" written there will override the -# global_variables defaults. Useful to avoid editing bb.sh and having to deal -# with merges in VCS -global_config=".config" - -# This function will load all the variables defined here. They might be overridden -# by the 'global_config' file contents -global_variables() { - global_software_name="bashblog" - global_software_version="2.8" - - # Blog title - global_title="my tildelog" - # The typical subtitle for each blog - global_description="a blog about tildes" - # The public base URL for this blog - global_url="https://tilde.team/~$USER/blog" - - # Your name - global_author="~$USER" - # You can use twitter or facebook or anything for global_author_url - global_author_url="https://tilde.team/~$USER/" - # Your email - global_email="$USER@tilde.team" - - # CC by-nc-nd is a good starting point, you can change this to "©" for Copyright - global_license="CC by-nc-nd" - - - # Leave this empty (i.e. "") if you don't want to use feedburner, - # or change it to your own URL - global_feedburner="" - - # Change this to your username if you want to use twitter for comments - global_twitter_username="" - # Set this to false for a Twitter button with share count. The cookieless version - # is just a link. - global_twitter_cookieless="true" - - # Blog generated files - # index page of blog (it is usually good to use "index.html" here) - index_file="index.html" - number_of_index_articles="10" - # global archive - archive_index="all_posts.html" - tags_index="all_tags.html" - - # ignore gophermap file - gophermap="gophermap" - - # ignore gemini generation script and gemini index - gemini_index="index.gmi" - - # Non blogpost files. Bashblog will ignore these. Useful for static pages and custom content - # Add them as a bash array, e.g. non_blogpost_files=("news.html" "test.html") - non_blogpost_files=() - - # feed file (rss in this case) - blog_feed="feed.rss" - number_of_feed_articles="50" - # "cut" blog entry when putting it to index page. Leave blank for full articles in front page - # i.e. include only up to first '
', or '----' in markdown - cut_do="cut" - # When cutting, cut also tags? If "no", tags will appear in index page for cut articles - cut_tags="yes" - # Regexp matching the HTML line where to do the cut - # note that slash is regexp separator so you need to prepend it with backslash - cut_line='
' - # save markdown file when posting with "bb post -m". Leave blank to discard it. - save_markdown="yes" - # prefix for tags/categories files - # please make sure that no other html file starts with this prefix - prefix_tags="tag_" - # personalized header and footer (only if you know what you're doing) - # DO NOT name them .header.html, .footer.html or they will be overwritten - # leave blank to generate them, recommended - header_file="" - footer_file="" - # extra content to add just after we open the tag - # and before the actual blog content - body_begin_file="" - # extra content to add just before we cloese ) - body_end_file="" - # CSS files to include on every page, f.ex. css_include=('main.css' 'blog.css') - # leave empty to use generated - css_include=() - # HTML files to exclude from index, f.ex. post_exclude=('imprint.html 'aboutme.html') - html_exclude=() - - # Localization and i18n - # "Comments?" (used in twitter link after every post) - template_comments="comments?" - # "Read more..." (link under cut article on index page) - template_read_more="read more..." - # "View more posts" (used on bottom of index page as link to archive) - template_archive="archive" - # "All posts" (title of archive page) - template_archive_title="all posts" - # "All tags" - template_tags_title="all tags" - # "posts" (on "All tags" page, text at the end of each tag line, like "2. Music - 15 posts") - template_tags_posts="posts" - template_tags_posts_2_4="posts" # Some slavic languages use a different plural form for 2-4 items - template_tags_posts_singular="post" - # "Posts tagged" (text on a title of a page with index of one tag, like "My Blog - Posts tagged "Music"") - template_tag_title="posts tagged" - # "Tags:" (beginning of line in HTML file with list of all tags for this article) - template_tags_line_header="tags:" - # "Back to the index page" (used on archive page, it is link to blog index) - template_archive_index_page="back home" - # "Subscribe" (used on bottom of index page, it is link to RSS feed) - template_subscribe="rss" - # "Subscribe to this page..." (used as text for browser feed button that is embedded to html) - template_subscribe_browser_button="subscribe to this page..." - # "Tweet" (used as twitter text button for posting to twitter) - template_twitter_button="tweet" - template_twitter_comment="<type your comment here but please leave the URL so that other people can follow the comments>" - - # The locale to use for the dates displayed on screen - date_format="%B %d, %Y" - date_locale="C" - date_inpost="bashblog_timestamp" - # Don't change these dates - date_format_full="%a, %d %b %Y %H:%M:%S %z" - date_format_timestamp="%Y%m%d%H%M.%S" - date_allposts_header="%B %Y" - - # Perform the post title -> filename conversion - # Experts only. You may need to tune the locales too - # Leave empty for no conversion, which is not recommended - # This default filter respects backwards compatibility - convert_filename="iconv -f utf-8 -t ascii//translit | sed 's/^-*//' | tr [:upper:] [:lower:] | tr ' ' '-' | tr -dc '[:alnum:]-'" - - # URL where you can view the post while it's being edited - # same as global_url by default - # You can change it to path on your computer, if you write posts locally - # before copying them to the server - preview_url="" - - # Markdown location. Trying to autodetect by default. - # The invocation must support the signature 'markdown_bin in.md > out.html' - - if [[ -f "./Markdown.pl" ]]; then - markdown_bin="./Markdown.pl" - else - markdown_bin=$(command -v Markdown.pl 2>/dev/null) - if [ -z "$markdown_bin" ]; then - markdown_bin=$(command -v markdown 2>/dev/null) - fi - fi -} - -# Check for the validity of some variables -# DO NOT EDIT THIS FUNCTION unless you know what you're doing -global_variables_check() { - [[ $header_file == .header.html ]] && \ - echo "Please check your configuration. '.header.html' is not a valid value for the setting 'header_file'" && \ - exit - [[ $footer_file == .footer.html ]] && \ - echo "Please check your configuration. '.footer.html' is not a valid value for the setting 'footer_file'" && \ - exit -} - - -# Test if the markdown script is working correctly -test_markdown() { - if [[ -n $markdown_bin ]]; then - local m1; m1=$("$markdown_bin" <<< $'line 1\n\nline 2') - local m2; m2=$("$markdown_bin" <<< $'line 1\n\nline 2') - local c1=$'

line 1

\n\n

line 2

' - local c2=$'

line 1

\n

line 2

' - # shellcheck disable=SC2235 # must enforce order of operations - [[ "$m1" == "$c1" ]] || [[ "$m2" == "$c2" ]] - else - return 1 - fi -} - - -# Parse a Markdown file into HTML and return the generated file -mrkdwn() { - out=${1%.md}.html - while [[ -f $out ]]; do out=${out%.html}.$RANDOM.html; done - $markdown_bin "$1" > "$out" - echo "$out" -} - -# Reads HTML file from stdin, prints its content to stdout -# $1 where to start ("text" or "entry") -# $2 where to stop ("text" or "entry") -# $3 "cut" to remove text from
to -# note that this does not remove
line itself, -# so you can see if text was cut or not -get_html_file_content() { - awk "//, //{ - if (!// && !//) print - if (\"$3\" == \"cut\" && /$cut_line/){ - if (\"$2\" == \"text\") exit # no need to read further - while (getline > 0 && !//) { - if (\"$cut_tags\" == \"no\" && /^

$template_tags_line_header/ ) print - } - } - }" -} - -# Edit an existing, published .html file while keeping its original timestamp -# Please note that this function does not automatically republish anything, as -# it is usually called from 'main'. -# -# Note that it edits HTML file, even if you wrote the post as markdown originally -# Note that if you edit title then filename might also change -# -# $1 the file to edit -# $2 (optional) edit mode: -# "keep" to keep old filename -# "full" to edit full HTML, and not only text part (keeps old filename) -# leave empty for default behavior (edit only text part and change name) -edit() { - [[ ! -f "${1%%.*}.html" ]] && \ - printf "Can't edit post \"%s.html\", did you mean to use \"bb.sh post \"?\\n" "${1%%.*}" && exit 1 - - # Original post timestamp - edit_timestamp=$(LC_ALL=C date -r "${1%%.*}.html" +"$date_format_full" ) - touch_timestamp=$(LC_ALL=C date -r "${1%%.*}.html" +"$date_format_timestamp") - tags_before=$(tags_in_post "${1%%.*}.html") - if [[ $2 == full ]]; then - $EDITOR "$1" - filename=$1 - else - if [[ ${1##*.} == md ]]; then - if test_markdown; then - echo "Markdown is not working, please edit HTML file directly." - exit - fi - # editing markdown file - $EDITOR "$1" - TMPFILE=$(mrkdwn "$1") - filename=${1%%.*}.html - else - # Create the content file - TMPFILE=$(basename "$1").$RANDOM.html - # Title - get_post_title "$1" > "$TMPFILE" - # Post text with plaintext tags - get_html_file_content 'text' 'text' <"$1" | sed "/^

$template_tags_line_header/s|\\1|\\1|g" >> "$TMPFILE" - $EDITOR "$TMPFILE" - filename=$1 - fi - rm "$filename" - if [[ $2 == keep ]]; then - parse_file "$TMPFILE" "$edit_timestamp" "$filename" - else - parse_file "$TMPFILE" "$edit_timestamp" # this command sets $filename as the html processed file - [[ ${1##*.} == md ]] && mv "$1" "${filename%%.*}.md" 2>/dev/null - fi - rm "$TMPFILE" - fi - touch -t "$touch_timestamp" "$filename" - touch -t "$touch_timestamp" "$1" - chmod 644 "$filename" - echo "Posted $filename" - tags_after=$(tags_in_post "$filename") - relevant_tags=$(echo "$tags_before $tags_after" | tr ',' ' ' | tr ' ' '\n' | sort -u | tr '\n' ' ') - if [[ -n $relevant_tags ]]; then - # shellcheck disable=SC2086 # Intended splitting of $relevant_tags - relevant_posts="$(posts_with_tags $relevant_tags) $filename" - rebuild_tags "$relevant_posts" "$relevant_tags" - fi -} - -# Create a Twitter summary (twitter "card") for the post -# -# $1 the post file -# $2 the title -twitter_card() { - [[ -z $global_twitter_username ]] && return - - echo "" - echo "" - echo "" # Twitter truncates at 70 char - description=$(grep -v "^

$template_tags_line_header" "$1" | sed -e 's/<[^>]*>//g' | head -c 250 | tr '\n' ' ' | sed "s/\"/'/g") - echo "" - image=$(sed -n 's/.*" -} - -# Adds the code needed by the twitter button -# -# $1 the post URL -twitter() { - [[ -z $global_twitter_username ]] && return - - if [[ $global_twitter_cookieless == true ]]; then - id=$RANDOM - - search_engine="https://twitter.com/search?q=" - - echo "

$template_comments $template_twitter_button " - echo " 

" - return; - else - echo "

$template_comments "; - fi - - echo "$template_twitter_button " - echo "

" -} - -# Check if the file is a 'boilerplate' (i.e. not a post) -# The return values are designed to be used like this inside a loop: -# is_boilerplate_file && continue -# -# $1 the file -# -# Return 0 (bash return value 'true') if the input file is an index, feed, etc -# or 1 (bash return value 'false') if it is a blogpost -is_boilerplate_file() { - name=${1#./} - # First check against user-defined non-blogpost pages - for item in "${non_blogpost_files[@]}"; do - [[ "$name" == "$item" ]] && return 0 - done - - case $name in - ( "$index_file" | "$archive_index" | "$gophermap" | "$gemini_index" | "$tags_index" | "$footer_file" | "$header_file" | "$prefix_tags"* ) - return 0 ;; - ( * ) # Check for excluded - for excl in "${html_exclude[@]}"; do - [[ $name == "$excl" ]] && return 0 - done - return 1 ;; - esac -} - -# Adds all the bells and whistles to format the html page -# Every blog post is marked with a and -# which is parsed afterwards in the other functions. There is also a marker -# to determine just the beginning of the text body of the post -# -# $1 a file with the body of the content -# $2 the output file -# $3 "yes" if we want to generate the index.html, -# "no" to insert new blog posts -# $4 title for the html header -# $5 original blog timestamp -# $6 post author -create_html_page() { - content=$1 - filename=$2 - index=$3 - title=$4 - timestamp=$5 - author=$6 - - # Create the actual blog post - # html, head - { - cat ".header.html" - echo "$title" - twitter_card "$content" "$title" - echo "" - # stuff to add before the actual body content - [[ -n $body_begin_file ]] && cat "$body_begin_file" - # body divs - echo '
' - echo '
' - # blog title - echo '
' - cat .title.html - echo '
' # title, header, headerholder - echo '
' - - file_url=${filename#./} - file_url=${file_url%.rebuilt} # Get the correct URL when rebuilding - # one blog entry - if [[ $index == no ]]; then - echo '' # marks the beginning of the whole post - echo "

" - # remove possible

's on the title because of markdown conversion - title=${title//

/} - title=${title//<\/p>/} - echo "$title" - echo '

' - if [[ -z $timestamp ]]; then - echo "" - else - echo "" - fi - if [[ -z $timestamp ]]; then - echo -n "
$(LC_ALL=$date_locale date +"$date_format")" - else - echo -n "
$(LC_ALL=$date_locale date +"$date_format" --date="$timestamp")" - fi - [[ -n $author ]] && echo -e " — \n$author" - echo "
" - echo '' # This marks the text body, after the title, date... - fi - cat "$content" # Actual content - if [[ $index == no ]]; then - echo -e '\n' - - twitter "$global_url/$file_url" - - echo '' # absolute end of the post - fi - - echo '
' # content - - # page footer - cat .footer.html - # close divs - echo '
' # divbody and divbodyholder - [[ -n $body_end_file ]] && cat "$body_end_file" - echo '' - } > "$filename" -} - -# Parse the plain text file into an html file -# -# $1 source file name -# $2 (optional) timestamp for the file -# $3 (optional) destination file name -# note that although timestamp is optional, something must be provided at its -# place if destination file name is provided, i.e: -# parse_file source.txt "" destination.html -parse_file() { - # Read for the title and check that the filename is ok - title="" - while IFS='' read -r line; do - if [[ -z $title ]]; then - # remove extra

and

added by markdown - title=$(echo "$line" | sed 's/<\/*p>//g') - if [[ -n $3 ]]; then - filename=$3 - else - filename=$title - [[ -n $convert_filename ]] && filename=$(echo "$title" | eval "$convert_filename") - [[ -n $filename ]] || filename=$RANDOM # don't allow empty filenames - filename=$filename.html - - # Check for duplicate file names - while [[ -f $filename ]]; do - filename=${filename%.html}$RANDOM.html - done - fi - content=$filename.tmp - # Parse possible tags - elif [[ $line == "

$template_tags_line_header"* ]]; then - tags=$(echo "$line" | cut -d ":" -f 2- | sed -e 's/<\/p>//g' -e 's/^ *//' -e 's/ *$//' -e 's/, /,/g') - IFS=, read -r -a array <<< "$tags" - - echo -n "

$template_tags_line_header " >> "$content" - for item in "${array[@]}"; do - echo -n "$item, " - done | sed 's/, $/<\/p>/g' >> "$content" - else - echo "$line" >> "$content" - fi - done < "$1" - - # Create the actual html page - create_html_page "$content" "$filename" no "$title" "$2" "$global_author" - rm "$content" -} - -# Manages the creation of the text file and the parsing to html file -# also the drafts -write_entry() { - test_markdown && fmt=md || fmt=html - f=$2 - [[ $2 == -html ]] && fmt=html && f=$3 - - if [[ -n $f ]]; then - TMPFILE=$f - if [[ ! -f $TMPFILE ]]; then - echo "The file doesn't exist" - delete_includes - exit - fi - # guess format from TMPFILE - extension=${TMPFILE##*.} - [[ $extension == md || $extension == html ]] && fmt=$extension - # but let user override it (`bb.sh post -html file.md`) - [[ $2 == -html ]] && fmt=html - # Test if Markdown is working before re-posting a .md file - if [[ $extension == md ]]; then - if test_markdown; then - echo "Markdown is not working, please edit HTML file directly." - exit - fi - fi - else - TMPFILE=.entry-$RANDOM.$fmt - echo -e "Title on this line\n" >> "$TMPFILE" - - [[ $fmt == html ]] && cat << EOF >> "$TMPFILE" -

The rest of the text file is an html blog post. The process will continue as soon -as you exit your editor.

- -

$template_tags_line_header keep-this-tag-format, tags-are-optional, example

-EOF - - [[ $fmt == md ]] && cat << EOF >> "$TMPFILE" -The rest of the text file is a **Markdown** blog post. The process will continue -as soon as you exit your editor. - -$template_tags_line_header keep-this-tag-format, tags-are-optional, beware-with-underscores-in-markdown, example -EOF - fi - chmod 600 "$TMPFILE" - - post_status="E" - filename="" - while [[ $post_status != "p" && $post_status != "P" ]]; do - [[ -n $filename ]] && rm "$filename" # Delete the generated html file, if any - $EDITOR "$TMPFILE" - if [[ $fmt == md ]]; then - html_from_md=$(mrkdwn "$TMPFILE") - parse_file "$html_from_md" - rm "$html_from_md" - else - parse_file "$TMPFILE" # this command sets $filename as the html processed file - fi - - chmod 644 "$filename" - [[ -n $preview_url ]] || preview_url=$global_url - echo "To preview the entry, open $preview_url/$filename in your browser" - - echo -n "[P]ost this entry, [E]dit again, [D]raft for later? (p/E/d) " - read -r post_status - if [[ $post_status == d || $post_status == D ]]; then - mkdir -p "drafts/" - chmod 700 "drafts/" - - title=$(head -n 1 $TMPFILE) - [[ -n $convert_filename ]] && title=$(echo "$title" | eval "$convert_filename") - [[ -n $title ]] || title=$RANDOM - - draft=drafts/$title.$fmt - mv "$TMPFILE" "$draft" - chmod 600 "$draft" - rm "$filename" - delete_includes - echo "Saved your draft as '$draft'" - exit - fi - done - - if [[ $fmt == md && -n $save_markdown ]]; then - mv "$TMPFILE" "${filename%%.*}.md" - else - rm "$TMPFILE" - fi - chmod 644 "$filename" - echo "Posted $filename" - relevant_tags=$(tags_in_post $filename) - if [[ -n $relevant_tags ]]; then - # shellcheck disable=SC2086 # Intended splitting of $relevant_tags - relevant_posts="$(posts_with_tags $relevant_tags) $filename" - rebuild_tags "$relevant_posts" "$relevant_tags" - fi -} - -# Create an index page with all the posts -all_posts() { - echo -n "Creating an index page with all the posts " - contentfile=$archive_index.$RANDOM - while [[ -f $contentfile ]]; do - contentfile=$archive_index.$RANDOM - done - - { - echo "

$template_archive_title

" - prev_month="" - while IFS='' read -r i; do - is_boilerplate_file "$i" && continue - echo -n "." 1>&3 - # Month headers - month=$(LC_ALL=$date_locale date -r "$i" +"$date_allposts_header") - if [[ $month != "$prev_month" ]]; then - [[ -n $prev_month ]] && echo "" # Don't close ul before first header - echo "

$month

" - echo "
    " - prev_month=$month - fi - # Title - title=$(get_post_title "$i") - echo -n "
  • $title —" - # Date - date=$(LC_ALL=$date_locale date -r "$i" +"$date_format") - echo " $date
  • " - done < <(ls -t ./*.html) - echo "" 1>&3 - echo "
" - echo "" - } 3>&1 >"$contentfile" - - create_html_page "$contentfile" "$archive_index.tmp" yes "$global_title — $template_archive_title" "$global_author" - mv "$archive_index.tmp" "$archive_index" - chmod 644 "$archive_index" - rm "$contentfile" -} - -# Create an index page with all the tags -all_tags() { - echo -n "Creating an index page with all the tags " - contentfile=$tags_index.$RANDOM - while [[ -f $contentfile ]]; do - contentfile=$tags_index.$RANDOM - done - - { - echo "

$template_tags_title

" - echo "
    " - # shellcheck disable=SC2231 # Intended splitting of $prefix_tags - for i in $prefix_tags*.html; do - [[ -f "$i" ]] || break - echo -n "." 1>&3 - nposts=$(grep -c "<\!-- text begin -->" "$i") - tagname=${i#"$prefix_tags"} - tagname=${tagname%.html} - case $nposts in - 1) word=$template_tags_posts_singular;; - 2|3|4) word=$template_tags_posts_2_4;; - *) word=$template_tags_posts;; - esac - echo "
  • $tagname — $nposts $word
  • " - done - echo "" 1>&3 - echo "
" - echo "" - } 3>&1 > "$contentfile" - - create_html_page "$contentfile" "$tags_index.tmp" yes "$global_title — $template_tags_title" "$global_author" - mv "$tags_index.tmp" "$tags_index" - chmod 644 "$tags_index" - rm "$contentfile" -} - -# Generate the index.html with the content of the latest posts -rebuild_index() { - echo -n "Rebuilding the index " - newindexfile=$index_file.$RANDOM - contentfile=$newindexfile.content - while [[ -f $newindexfile ]]; do - newindexfile=$index_file.$RANDOM - contentfile=$newindexfile.content - done - - # Create the content file - { - n=0 - while IFS='' read -r i; do - is_boilerplate_file "$i" && continue; - if ((n >= number_of_index_articles)); then break; fi - if [[ -n $cut_do ]]; then - get_html_file_content 'entry' 'entry' 'cut' <"$i" | awk "/$cut_line/ { print \"

$template_read_more

\" ; next } 1" - else - get_html_file_content 'entry' 'entry' <"$i" - fi - echo -n "." 1>&3 - n=$(( n + 1 )) - done < <(ls -t ./*.html) # sort by date, newest first - - feed=$blog_feed - if [[ -n $global_feedburner ]]; then feed=$global_feedburner; fi - echo "" - } 3>&1 >"$contentfile" - - echo "" - - create_html_page "$contentfile" "$newindexfile" yes "$global_title" "$global_author" - rm "$contentfile" - mv "$newindexfile" "$index_file" - chmod 644 "$index_file" -} - -# Finds all tags referenced in one post. -# Accepts either filename as first argument, or post content at stdin -# Prints one line with space-separated tags to stdout -tags_in_post() { - sed -n "/^

$template_tags_line_header/{s/^

$template_tags_line_header//;s/<[^>]*>//g;s/[ ,]\+/ /g;p;}" "$1" | tr ', ' ' ' -} - -# Finds all posts referenced in a number of tags. -# Arguments are tags -# Prints one line with space-separated tags to stdout -posts_with_tags() { - (($# < 1)) && return - set -- "${@/#/$prefix_tags}" - set -- "${@/%/.html}" - sed -n '/^

/{s/.*href="\([^"]*\)">.*/\1/;p;}' "$@" 2> /dev/null -} - -# Rebuilds tag_*.html files -# if no arguments given, rebuilds all of them -# if arguments given, they should have this format: -# "FILE1 [FILE2 [...]]" "TAG1 [TAG2 [...]]" -# where FILEn are files with posts which should be used for rebuilding tags, -# and TAGn are names of tags which should be rebuilt. -# example: -# rebuild_tags "one_post.html another_article.html" "example-tag another-tag" -# mind the quotes! -rebuild_tags() { - if (($# < 2)); then - # will process all files and tags - files=$(ls -t ./*.html) - all_tags=yes - else - # will process only given files and tags - files=$(printf '%s\n' "$1" | sort -u) - # shellcheck disable=SC2086 # Intended splitting of $files - files=$(ls -t $files) - tags=$2 - fi - echo -n "Rebuilding tag pages " - n=0 - if [[ -n $all_tags ]]; then - rm ./"$prefix_tags"*.html &> /dev/null - else - for i in $tags; do - rm "./$prefix_tags$i.html" &> /dev/null - done - fi - # First we will process all files and create temporal tag files - # with just the content of the posts - tmpfile=tmp.$RANDOM - while [[ -f $tmpfile ]]; do tmpfile=tmp.$RANDOM; done - while IFS='' read -r i; do - is_boilerplate_file "$i" && continue; - echo -n "." - if [[ -n $cut_do ]]; then - get_html_file_content 'entry' 'entry' 'cut' <"$i" | awk "/$cut_line/ { print \"

$template_read_more

\" ; next } 1" - else - get_html_file_content 'entry' 'entry' <"$i" - fi >"$tmpfile" - for tag in $(tags_in_post "$i"); do - if [[ -n $all_tags || " $tags " == *" $tag "* ]]; then - cat "$tmpfile" >> "$prefix_tags$tag".tmp.html - fi - done - done <<< "$files" - rm "$tmpfile" - # Now generate the tag files with headers, footers, etc - while IFS='' read -r i; do - tagname=${i#./"$prefix_tags"} - tagname=${tagname%.tmp.html} - create_html_page "$i" "$prefix_tags$tagname.html" yes "$global_title — $template_tag_title \"$tagname\"" "$global_author" - rm "$i" - done < <(ls -t ./"$prefix_tags"*.tmp.html 2>/dev/null) - echo -} - -# Return the post title -# -# $1 the html file -get_post_title() { - awk '/

/, /<\/a><\/h3>/{if (!/

/ && !/<\/a><\/h3>/) print}' "$1" -} - -# Return the post author -# -# $1 the html file -get_post_author() { - awk '/
.+/, //{if (!/
.+/ && !//) print}' "$1" | sed 's/<\/div>//g' -} - -# Displays a list of the tags -# -# $2 if "-n", tags will be sorted by number of posts -list_tags() { - if [[ $2 == -n ]]; then do_sort=1; else do_sort=0; fi - - if ls ./$prefix_tags*.html > /dev/null; then - echo "No posts yet. Use 'bb.sh post' to create one" && return - fi - - lines="" - # shellcheck disable=SC2231 # Intended splitting of $prefix_tags - for i in $prefix_tags*.html; do - [[ -f "$i" ]] || break - nposts=$(grep -c "<\!-- text begin -->" "$i") - tagname=${i#"$prefix_tags"} - tagname=${tagname#.html} - ((nposts > 1)) && word=$template_tags_posts || word=$template_tags_posts_singular - line="$tagname # $nposts # $word" - lines+=$line\\n - done - - if (( do_sort == 1 )); then - echo -e "$lines" | column -t -s "#" | sort -nrk 2 - else - echo -e "$lines" | column -t -s "#" - fi -} - -# Displays a list of the posts -list_posts() { - if ls ./*.html > /dev/null; then - echo "No posts yet. Use 'bb.sh post' to create one" && return - fi - - lines="" - n=1 - while IFS='' read -r i; do - is_boilerplate_file "$i" && continue - line="$n # $(get_post_title "$i") # $(LC_ALL=$date_locale date -r "$i" +"$date_format")" - lines+=$line\\n - n=$(( n + 1 )) - done < <(ls -t ./*.html) - - echo -e "$lines" | column -t -s "#" -} - -# Generate the feed file -make_rss() { - echo -n "Making RSS " - - rssfile=$blog_feed.$RANDOM - while [[ -f $rssfile ]]; do rssfile=$blog_feed.$RANDOM; done - - { - pubdate=$(LC_ALL=C date +"$date_format_full") - echo '' - echo '' - echo "$global_title$global_url/$index_file" - echo "$global_descriptionen" - echo "$pubdate" - echo "$pubdate" - echo "" - - n=0 - while IFS='' read -r i; do - is_boilerplate_file "$i" && continue - ((n >= number_of_feed_articles)) && break # max 10 items - echo -n "." 1>&3 - echo '' - get_post_title "$i" - echo '$global_url/${i#./}" - echo "$global_url/$i" - echo "$(get_post_author "$i")" - echo "$(LC_ALL=C date -r "$i" +"$date_format_full")" - - n=$(( n + 1 )) - done < <(ls -t ./*.html) - - echo '' - } 3>&1 >"$rssfile" - echo "" - - mv "$rssfile" "$blog_feed" - chmod 644 "$blog_feed" -} - -make_gophermap() { - if [ ! -d "${HOME}/public_gopher" ]; then - printf "Creating gopher hole\\n" - mkdir "${HOME}/public_gopher" - fi - - if [ ! -L "${HOME}/public_gopher/blog" ]; then - ln -sf "${HOME}/public_html/blog/" "${HOME}/public_gopher/blog" - fi - - if [ ! -f "${HOME}/public_gopher/blog/$gophermap" ]; then - cat <<- 'EOF' > "${HOME}/public_html/blog/${gophermap}" - #!/usr/bin/env sh - printf "my bashblog posts\n" - user=$(stat -c '%U' .) - for post in $(ls -t *.md); do - post=$(basename $post) - printf "0$post\t/~$user/blog/$post\ttilde.team\t70\n" - done -EOF - chmod +x "${HOME}/public_html/blog/${gophermap}" - fi - chmod 644 "./*.md" -} - -make_gemini() { - if [ ! -d "${HOME}/public_gemini" ]; then - printf "Creating ~/public_gemini\\n" - mkdir "${HOME}/public_gemini" - fi - - if [ ! -L "${HOME}/public_gemini/blog" ]; then - ln -sf "${HOME}/public_html/blog/" "${HOME}/public_gemini/blog" - fi - - if [ ! -f "${HOME}/public_gemini/blog/$gemini_index" ]; then - cat <<- 'EOF' > "${HOME}/public_gemini/blog/${gemini_index}" - #!/usr/bin/env sh - printf "20 text/gemini\r\n" - printf "my bashblog posts\r\n" - user=$(stat -c '%U' $0) - for post in $(ls -t /home/$user/public_gemini/blog/*.md); do - post=$(basename $post) - printf "=> /~$user/blog/$post $post\r\n" - done -EOF - chmod +x "${HOME}/public_gemini/blog/${gemini_index}" - fi -} - -# generate headers, footers, etc -create_includes() { - { - echo "

$global_title

" - echo "
$global_description
" - } > ".title.html" - - if [[ -f $header_file ]]; then - cp "$header_file" .header.html - else - { - echo '' - echo '' - echo '' - echo '' - printf '\n' "${css_include[@]}" - if [[ -z $global_feedburner ]]; then - echo "" - else - echo "" - fi - } > ".header.html" - fi - - if [[ -f $footer_file ]]; then - cp "$footer_file" .footer.html - else - { - protected_mail=${global_email//@/@} - protected_mail=${protected_mail//./.} - echo "
$global_license $global_author$protected_mail
" - echo 'generated with bashblog, a single bash script to easily create blogs like this one
' - } >> ".footer.html" - fi -} - -# Delete the temporarily generated include files -delete_includes() { - rm ".title.html" ".footer.html" ".header.html" -} - -# Create the css file from scratch -create_css() { - # To avoid overwriting manual changes. However it is recommended that - # this function is modified if the user changes the blog.css file - (( ${#css_include[@]} > 0 )) && return || css_include=('main.css' 'blog.css') - if [[ ! -f blog.css ]]; then - # blog.css directives will be loaded after main.css and thus will prevail - echo '#title{font-size: x-large;} - a.ablack{color:black !important;} - li{margin-bottom:8px;} - ul,ol{margin-left:24px;margin-right:24px;} - #all_posts{margin-top:24px;text-align:center;} - .subtitle{font-size:small;margin:12px 0px;} - .content p{margin-left:24px;margin-right:24px;} - h1{margin-bottom:12px !important;} - #description{font-size:large;margin-bottom:12px;} - h3{margin-top:42px;margin-bottom:8px;} - h4{margin-left:24px;margin-right:24px;} - img{max-width:100%;} - #twitter{line-height:20px;vertical-align:top;text-align:right;font-style:italic;color:#333;margin-top:24px;font-size:14px;}' > blog.css - fi - - # If there is a style.css from the parent page (i.e. some landing page) - # then use it. This directive is here for compatibility with my own - # home page. Feel free to edit it out, though it doesn't hurt - if [[ -f ../style.css ]] && [[ ! -f main.css ]]; then - ln -s "../style.css" "main.css" - elif [[ ! -f main.css ]]; then - echo 'body{font-family:Georgia,"Times New Roman",Times,serif;margin:0;padding:0;background-color:#F3F3F3;} - #divbodyholder{padding:5px;background-color:#DDD;width:100%;max-width:874px;margin:24px auto;} - #divbody{border:solid 1px #ccc;background-color:#fff;padding:0px 48px 24px 48px;top:0;} - .headerholder{background-color:#f9f9f9;border-top:solid 1px #ccc;border-left:solid 1px #ccc;border-right:solid 1px #ccc;} - .header{width:100%;max-width:800px;margin:0px auto;padding-top:24px;padding-bottom:8px;} - .content{margin-bottom:5%;} - .nomargin{margin:0;} - .description{margin-top:10px;border-top:solid 1px #666;padding:10px 0;} - h3{font-size:20pt;width:100%;font-weight:bold;margin-top:32px;margin-bottom:0;} - .clear{clear:both;} - #footer{padding-top:10px;border-top:solid 1px #666;color:#333333;text-align:center;font-size:small;font-family:"Courier New","Courier",monospace;} - a{text-decoration:none;color:#003366 !important;} - a:visited{text-decoration:none;color:#336699 !important;} - blockquote{background-color:#f9f9f9;border-left:solid 4px #e9e9e9;margin-left:12px;padding:12px 12px 12px 24px;} - blockquote img{margin:12px 0px;} - blockquote iframe{margin:12px 0px;}' > main.css - fi -} - -# Regenerates all the single post entries, keeping the post content but modifying -# the title, html structure, etc -rebuild_all_entries() { - echo -n "Rebuilding all entries " - - for i in ./*.html; do - is_boilerplate_file "$i" && continue; - contentfile=.tmp.$RANDOM - while [[ -f $contentfile ]]; do contentfile=.tmp.$RANDOM; done - - echo -n "." - # Get the title and entry, and rebuild the html structure from scratch (divs, title, description...) - title=$(get_post_title "$i") - - get_html_file_content 'text' 'text' <"$i" >> "$contentfile" - - # Read timestamp from post, if present, and sync file timestamp - timestamp=$(awk '// { print }' "$i" | cut -d '#' -f 2) - [[ -n $timestamp ]] && touch -t "$timestamp" "$i" - # Read timestamp from file in correct format for 'create_html_page' - timestamp=$(LC_ALL=C date -r "$i" +"$date_format_full") - - create_html_page "$contentfile" "$i.rebuilt" no "$title" "$timestamp" "$(get_post_author "$i")" - # keep the original timestamp! - timestamp=$(LC_ALL=C date -r "$i" +"$date_format_timestamp") - mv "$i.rebuilt" "$i" - chmod 644 "$i" - touch -t "$timestamp" "$i" - rm "$contentfile" - done - echo "" -} - -# Displays the help -usage() { - echo "$global_software_name v$global_software_version" - echo "usage: bb command [filename]" - echo "" - echo "Commands:" - echo " post [-html] [filename] insert a new blog post, or the filename of a draft to continue editing it" - echo " it tries to use markdown by default, and falls back to HTML if it's not available." - echo " use '-html' to override it and edit the post as HTML even when markdown is available" - echo " edit [-n|-f] [filename] edit an already published .html or .md file. **NEVER** edit manually a published .html file," - echo " always use this function as it keeps internal data and rebuilds the blog" - echo " use '-n' to give the file a new name, if title was changed" - echo " use '-f' to edit full html file, instead of just text part (also preserves name)" - echo " delete [filename] deletes the post and rebuilds the blog" - echo " rebuild regenerates all the pages and posts, preserving the content of the entries" - echo " reset deletes everything except this script. Use with a lot of caution and back up first!" - echo " list list all posts" - echo " tags [-n] list all tags in alphabetical order" - echo " use '-n' to sort list by number of posts" - echo "" - echo "for more information please see https://tilde.team/wiki/?page=tildeblogs" - echo "source here: https://tildegit.org/team/bashblog" -} - -# Delete all generated content, leaving only this script -reset() { - echo "Are you sure you want to delete all blog entries? Please write \"Yes, I am!\" " - read -r line - if [[ $line == "Yes, I am!" ]]; then - rm .*.html ./*.html ./*.css ./*.rss &> /dev/null - echo - echo "Deleted all posts, stylesheets and feeds." - echo "Kept your old '.backup.tar.gz' just in case, please delete it manually if needed." - else - echo "Phew! You dodged a bullet there. Nothing was modified." - fi -} - -# Detects if GNU date is installed -date_version_detect() { - if ! stat -c"%U" /dev/null >/dev/null 2>&1 ; then - # BSD environment - if command -v gdate >/dev/null 2>&1 ; then - date() { - gdate "$@" - } - else - date() { - if [[ $1 == -r ]]; then - # Fall back to using stat for 'date -r' - format=${3//+/} - stat -f "%Sm" -t "$format" "$2" - elif [[ $2 == --date* ]]; then - # convert between dates using BSD date syntax - command date -j -f "$date_format_full" "${2#--date=}" "$1" - else - # acceptable format for BSD date - command date -j "$@" - fi - } - fi - fi -} - -# Main function -# Encapsulated on its own function for readability purposes -# -# $1 command to run -# $2 file name of a draft to continue editing (optional) -do_main() { - # make sure we're in the right directory - if [ "$(pwd)" != "$HOME/public_html/blog" ]; then - echo "you're not in your blog directory. moving you there now" - mkdir -p "$HOME/public_html/blog" - cd "$HOME/public_html/blog" || exit 1 - fi - - # Detect if using BSD date or GNU date - date_version_detect - # Load default configuration, then override settings with the config file - global_variables - # shellcheck disable=SC1090 # variable config file - [[ -f "$global_config" ]] && source "$global_config" &> /dev/null - global_variables_check - - # Check for $EDITOR - [[ -z $EDITOR ]] && - echo "Please set your \$EDITOR environment variable. For example, to use nano, add the line 'export EDITOR=nano' to your \$HOME/.bashrc file" && exit - - # Check for validity of argument - [[ $1 != "reset" && $1 != "post" && $1 != "rebuild" && $1 != "list" && $1 != "edit" && $1 != "delete" && $1 != "tags" ]] && - usage && exit - - [[ $1 == list ]] && - list_posts && exit - - [[ $1 == tags ]] && - list_tags "$@" && exit - - if [[ $1 == edit ]]; then - if (($# < 2)) || [[ ! -f ${!#} ]]; then - echo "Please enter a valid .md or .html file to edit" - exit - fi - fi - - # Test for existing html files - if ls ./*.html &> /dev/null; then - # We're going to back up just in case - tar -c -z -f ".backup.tar.gz" -- *.html && - chmod 600 ".backup.tar.gz" - elif [[ $1 == rebuild ]]; then - echo "Can't find any html files, nothing to rebuild" - exit - fi - - # Keep first backup of this day containing yesterday's version of the blog - [[ ! -f .yesterday.tar.gz || $(date -r .yesterday.tar.gz +'%d') != "$(date +'%d')" ]] && - cp .backup.tar.gz .yesterday.tar.gz &> /dev/null - - [[ $1 == reset ]] && - reset && exit - - create_css - create_includes - [[ $1 == post ]] && write_entry "$@" - [[ $1 == rebuild ]] && rebuild_all_entries && rebuild_tags - [[ $1 == delete ]] && rm "$2" &> /dev/null && rebuild_tags - if [[ $1 == edit ]]; then - if [[ $2 == -n ]]; then - edit "$3" - elif [[ $2 == -f ]]; then - edit "$3" full - else - edit "$2" keep - fi - fi - rebuild_index - all_posts - all_tags - make_rss - make_gophermap - make_gemini - delete_includes -} - - -# -# MAIN -# Do not change anything here. If you want to modify the code, edit do_main() -# -do_main "$@" - -# vim: set shiftwidth=4 tabstop=4 expandtab: diff --git a/lib/all_posts.sh b/lib/all_posts.sh new file mode 100644 index 0000000..20a4a0c --- /dev/null +++ b/lib/all_posts.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +# Create an index page with all the posts +all_posts() { + declare archive_index + declare template_archive_index_page + declare template_archive_title + declare date_allposts_header + declare date_format + declare date_locale + declare index_file + declare global_title + declare global_author + echo -n "Creating an index page with all the posts " + contentfile=$archive_index.$RANDOM + while [[ -f $contentfile ]]; do + contentfile=$archive_index.$RANDOM + done + + { + echo "

$template_archive_title

" + prev_month="" + while IFS='' read -r i; do + is_boilerplate_file "$i" && continue + echo -n "." 1>&3 + # Month headers + month=$(LC_ALL=$date_locale date -r "$i" +"$date_allposts_header") + if [[ $month != "$prev_month" ]]; then + [[ -n $prev_month ]] && echo "" # Don't close ul before first header + echo "

$month

" + echo "
    " + prev_month=$month + fi + # Title + title=$(get_post_title "$i") + echo -n "
  • $title —" + # Date + date=$(LC_ALL=$date_locale date -r "$i" +"$date_format") + echo " $date
  • " + done < <(ls -t ./*.html) + echo "" 1>&3 + echo "
" + echo "" + } 3>&1 >"$contentfile" + + create_html_page "$contentfile" "$archive_index.tmp" yes "$global_title — $template_archive_title" "$global_author" + mv "$archive_index.tmp" "$archive_index" + chmod 644 "$archive_index" + rm "$contentfile" +} \ No newline at end of file diff --git a/lib/all_tags.sh b/lib/all_tags.sh new file mode 100644 index 0000000..0090c17 --- /dev/null +++ b/lib/all_tags.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +# Create an index page with all the tags +all_tags() { + declare tags_index + declare template_tags_posts + declare template_tags_posts_singular + declare template_tags_posts_2_4 + declare template_tags_title + declare prefix_tags + declare index_file + declare template_archive_index_page + declare global_author + declare global_title + echo -n "Creating an index page with all the tags " + contentfile=$tags_index.$RANDOM + while [[ -f $contentfile ]]; do + contentfile=$tags_index.$RANDOM + done + + { + echo "

$template_tags_title

" + echo "
    " + # shellcheck disable=SC2231 # Intended splitting of $prefix_tags + for i in $prefix_tags*.html; do + [[ -f "$i" ]] || break + echo -n "." 1>&3 + nposts=$(grep -c "<\!-- text begin -->" "$i") + tagname=${i#"$prefix_tags"} + tagname=${tagname%.html} + case $nposts in + 1) word=$template_tags_posts_singular;; + 2|3|4) word=$template_tags_posts_2_4;; + *) word=$template_tags_posts;; + esac + echo "
  • $tagname — $nposts $word
  • " + done + echo "" 1>&3 + echo "
" + echo "" + } 3>&1 > "$contentfile" + + create_html_page "$contentfile" "$tags_index.tmp" yes "$global_title — $template_tags_title" "$global_author" + mv "$tags_index.tmp" "$tags_index" + chmod 644 "$tags_index" + rm "$contentfile" +} \ No newline at end of file diff --git a/lib/create_css.sh b/lib/create_css.sh new file mode 100644 index 0000000..506f6c6 --- /dev/null +++ b/lib/create_css.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +# Create the css file from scratch +create_css() { + # To avoid overwriting manual changes. However it is recommended that + # this function is modified if the user changes the blog.css file + (( ${#css_include[@]} > 0 )) && return || css_include=('main.css' 'blog.css') + if [[ ! -f blog.css ]]; then + # blog.css directives will be loaded after main.css and thus will prevail + echo '#title{font-size: x-large;} + a.ablack{color:black !important;} + li{margin-bottom:8px;} + ul,ol{margin-left:24px;margin-right:24px;} + #all_posts{margin-top:24px;text-align:center;} + .subtitle{font-size:small;margin:12px 0px;} + .content p{margin-left:24px;margin-right:24px;} + h1{margin-bottom:12px !important;} + #description{font-size:large;margin-bottom:12px;} + h3{margin-top:42px;margin-bottom:8px;} + h4{margin-left:24px;margin-right:24px;} + img{max-width:100%;} + #twitter{line-height:20px;vertical-align:top;text-align:right;font-style:italic;color:#333;margin-top:24px;font-size:14px;}' > blog.css + fi + + # If there is a style.css from the parent page (i.e. some landing page) + # then use it. This directive is here for compatibility with my own + # home page. Feel free to edit it out, though it doesn't hurt + if [[ -f ../style.css ]] && [[ ! -f main.css ]]; then + ln -s "../style.css" "main.css" + elif [[ ! -f main.css ]]; then + echo 'body{font-family:Georgia,"Times New Roman",Times,serif;margin:0;padding:0;background-color:#F3F3F3;} + #divbodyholder{padding:5px;background-color:#DDD;width:100%;max-width:874px;margin:24px auto;} + #divbody{border:solid 1px #ccc;background-color:#fff;padding:0px 48px 24px 48px;top:0;} + .headerholder{background-color:#f9f9f9;border-top:solid 1px #ccc;border-left:solid 1px #ccc;border-right:solid 1px #ccc;} + .header{width:100%;max-width:800px;margin:0px auto;padding-top:24px;padding-bottom:8px;} + .content{margin-bottom:5%;} + .nomargin{margin:0;} + .description{margin-top:10px;border-top:solid 1px #666;padding:10px 0;} + h3{font-size:20pt;width:100%;font-weight:bold;margin-top:32px;margin-bottom:0;} + .clear{clear:both;} + #footer{padding-top:10px;border-top:solid 1px #666;color:#333333;text-align:center;font-size:small;font-family:"Courier New","Courier",monospace;} + a{text-decoration:none;color:#003366 !important;} + a:visited{text-decoration:none;color:#336699 !important;} + blockquote{background-color:#f9f9f9;border-left:solid 4px #e9e9e9;margin-left:12px;padding:12px 12px 12px 24px;} + blockquote img{margin:12px 0px;} + blockquote iframe{margin:12px 0px;}' > main.css + fi +} \ No newline at end of file diff --git a/lib/create_html_page.sh b/lib/create_html_page.sh new file mode 100644 index 0000000..51759bd --- /dev/null +++ b/lib/create_html_page.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash + +# Adds all the bells and whistles to format the html page +# Every blog post is marked with a and +# which is parsed afterwards in the other functions. There is also a marker +# to determine just the beginning of the text body of the post +# +# $1 a file with the body of the content +# $2 the output file +# $3 "yes" if we want to generate the index.html, +# "no" to insert new blog posts +# $4 title for the html header +# $5 original blog timestamp +# $6 post author +create_html_page() { + declare body_begin_file + declare date_inpost + declare date_locale + declare date_format + declare date_format_timestamp + declare global_url + declare body_end_file + content=$1 + filename=$2 + index=$3 + title=$4 + timestamp=$5 + author=$6 + + # Create the actual blog post + # html, head + { + cat ".header.html" + echo "$title" + twitter_card "$content" "$title" + echo "" + # stuff to add before the actual body content + [[ -n $body_begin_file ]] && cat "$body_begin_file" + # body divs + echo '
' + echo '
' + # blog title + echo '
' + cat .title.html + echo '
' # title, header, headerholder + echo '
' + + file_url=${filename#./} + file_url=${file_url%.rebuilt} # Get the correct URL when rebuilding + # one blog entry + if [[ $index == no ]]; then + echo '' # marks the beginning of the whole post + echo "

" + # remove possible

's on the title because of markdown conversion + title=${title//

/} + title=${title//<\/p>/} + echo "$title" + echo '

' + if [[ -z $timestamp ]]; then + echo "" + else + echo "" + fi + if [[ -z $timestamp ]]; then + echo -n "
$(LC_ALL=$date_locale date +"$date_format")" + else + echo -n "
$(LC_ALL=$date_locale date +"$date_format" --date="$timestamp")" + fi + [[ -n $author ]] && echo -e " — \n$author" + echo "
" + echo '' # This marks the text body, after the title, date... + fi + cat "$content" # Actual content + if [[ $index == no ]]; then + echo -e '\n' + + twitter "$global_url/$file_url" + + echo '' # absolute end of the post + fi + + echo '
' # content + + # page footer + cat .footer.html + # close divs + echo '
' # divbody and divbodyholder + [[ -n $body_end_file ]] && cat "$body_end_file" + echo '' + } > "$filename" +} \ No newline at end of file diff --git a/lib/create_includes.sh b/lib/create_includes.sh new file mode 100644 index 0000000..6c2dceb --- /dev/null +++ b/lib/create_includes.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# generate headers, footers, etc +create_includes() { + declare global_author + declare global_author_url + declare global_description + declare global_email + declare global_feedburner + declare global_license + declare global_title + declare global_url + declare index_file + declare header_file + declare css_include + declare template_subscribe_browser_button + declare blog_feed + declare footer_file + { + echo "

$global_title

" + echo "
$global_description
" + } > ".title.html" + + if [[ -f $header_file ]]; then + cp "$header_file" .header.html + else + { + echo '' + echo '' + echo '' + echo '' + printf '\n' "${css_include[@]}" + if [[ -z $global_feedburner ]]; then + echo "" + else + echo "" + fi + } > ".header.html" + fi + + if [[ -f $footer_file ]]; then + cp "$footer_file" .footer.html + else + { + protected_mail=${global_email//@/@} + protected_mail=${protected_mail//./.} + echo "
$global_license $global_author$protected_mail
" + echo 'generated with bashblog, a single bash script to easily create blogs like this one
' + } >> ".footer.html" + fi +} diff --git a/lib/date_version_detect.sh b/lib/date_version_detect.sh new file mode 100644 index 0000000..382fef3 --- /dev/null +++ b/lib/date_version_detect.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# Detects if GNU date is installed +date_version_detect() { + declare date_format_full + if ! stat -c"%U" /dev/null >/dev/null 2>&1 ; then + # BSD environment + if command -v gdate >/dev/null 2>&1 ; then + date() { + gdate "$@" + } + else + date() { + if [[ $1 == -r ]]; then + # Fall back to using stat for 'date -r' + format=${3//+/} + stat -f "%Sm" -t "$format" "$2" + elif [[ $2 == --date* ]]; then + # convert between dates using BSD date syntax + command date -j -f "$date_format_full" "${2#--date=}" "$1" + else + # acceptable format for BSD date + command date -j "$@" + fi + } + fi + fi +} \ No newline at end of file diff --git a/lib/delete_includes.sh b/lib/delete_includes.sh new file mode 100644 index 0000000..ac60fe2 --- /dev/null +++ b/lib/delete_includes.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# Delete the temporarily generated include files +delete_includes() { + rm ".title.html" ".footer.html" ".header.html" +} \ No newline at end of file diff --git a/lib/do_main.sh b/lib/do_main.sh new file mode 100644 index 0000000..ebbc9d5 --- /dev/null +++ b/lib/do_main.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash + +# Main function +# Encapsulated on its own function for readability purposes +# +# $1 command to run +# $2 file name of a draft to continue editing (optional) +declare global_config +do_main() { + # make sure we're in the right directory + if [ "$(pwd)" != "$HOME/public_html/blog" ]; then + echo "You're not in your blog directory. Moving you there now" + mkdir -p "$HOME/public_html/blog" + cd "$HOME/public_html/blog" || exit 1 + fi + # Detect if using BSD date or GNU date + date_version_detect + # Load default configuration, then override settings with the config file + global_variables + # shellcheck disable=SC1090 # variable config file + [[ -f "$global_config" ]] && source "$global_config" &> /dev/null + global_variables_check + + # Check for $EDITOR + [[ -z $EDITOR ]] && + echo "Please set your \$EDITOR environment variable. For example, to use nano, add the line 'export EDITOR=nano' to your \$HOME/.bashrc file" && exit + + # Check for validity of argument + [[ $1 != "reset" && $1 != "post" && $1 != "rebuild" && $1 != "list" && $1 != "edit" && $1 != "delete" && $1 != "tags" ]] && + usage && return + + [[ $1 == list ]] && + list_posts && return + + [[ $1 == tags ]] && + list_tags "$@" && return + + if [[ $1 == edit ]]; then + if (($# < 2)) || [[ ! -f ${!#} ]]; then + echo "Please enter a valid .md or .html file to edit" + exit + fi + fi + + # Test for existing html files + if ls ./*.html &> /dev/null; then + # We're going to back up just in case + tar -c -z -f ".backup.tar.gz" -- *.html && + chmod 600 ".backup.tar.gz" + elif [[ $1 == rebuild ]]; then + echo "Can't find any html files, nothing to rebuild" + return + fi + + # Keep first backup of this day containing yesterday's version of the blog + [[ ! -f .yesterday.tar.gz || $(date -r .yesterday.tar.gz +'%d') != "$(date +'%d')" ]] && + cp .backup.tar.gz .yesterday.tar.gz &> /dev/null + + [[ $1 == reset ]] && + reset && return + + create_css + create_includes + [[ $1 == post ]] && write_entry "$@" + [[ $1 == rebuild ]] && rebuild_all_entries && rebuild_tags + [[ $1 == delete ]] && rm "$2" &> /dev/null && rebuild_tags + if [[ $1 == edit ]]; then + if [[ $2 == -n ]]; then + edit "$3" + elif [[ $2 == -f ]]; then + edit "$3" full + else + edit "$2" keep + fi + fi + rebuild_index + all_posts + all_tags + make_rss + make_gophermap + make_gemini + delete_includes +} \ No newline at end of file diff --git a/lib/edit.sh b/lib/edit.sh new file mode 100644 index 0000000..885f4ef --- /dev/null +++ b/lib/edit.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +# Edit an existing, published .html file while keeping its original timestamp +# Please note that this function does not automatically republish anything, as +# it is usually called from 'main'. +# +# Note that it edits HTML file, even if you wrote the post as markdown originally +# Note that if you edit title then filename might also change +# +# $1 the file to edit +# $2 (optional) edit mode: +# "keep" to keep old filename +# "full" to edit full HTML, and not only text part (keeps old filename) +# leave empty for default behavior (edit only text part and change name) +edit() { + declare date_format_full + declare date_format_timestamp + declare template_tags_line_header + declare prefix_tags + [[ ! -f "${1%%.*}.html" ]] && \ + printf "Can't edit post \"%s.html\", did you mean to use \"bb.sh post \"?\\n" "${1%%.*}" && exit 1 + + # Original post timestamp + edit_timestamp=$(LC_ALL=C date -r "${1%%.*}.html" +"$date_format_full" ) + touch_timestamp=$(LC_ALL=C date -r "${1%%.*}.html" +"$date_format_timestamp") + tags_before=$(tags_in_post "${1%%.*}.html") + if [[ $2 == full ]]; then + $EDITOR "$1" + filename=$1 + else + if [[ ${1##*.} == md ]]; then + if test_markdown; then + echo "Markdown is not working, please edit HTML file directly." + exit + fi + # editing markdown file + $EDITOR "$1" + TMPFILE=$(mrkdwn "$1") + filename=${1%%.*}.html + else + # Create the content file + TMPFILE=$(basename "$1").$RANDOM.html + # Title + get_post_title "$1" > "$TMPFILE" + # Post text with plaintext tags + get_html_file_content 'text' 'text' <"$1" | sed "/^

$template_tags_line_header/s|\\1|\\1|g" >> "$TMPFILE" + $EDITOR "$TMPFILE" + filename=$1 + fi + rm "$filename" + if [[ $2 == keep ]]; then + parse_file "$TMPFILE" "$edit_timestamp" "$filename" + else + parse_file "$TMPFILE" "$edit_timestamp" # this command sets $filename as the html processed file + [[ ${1##*.} == md ]] && mv "$1" "${filename%%.*}.md" 2>/dev/null + fi + rm "$TMPFILE" + fi + touch -t "$touch_timestamp" "$filename" + touch -t "$touch_timestamp" "$1" + chmod 644 "$filename" + echo "Posted $filename" + tags_after=$(tags_in_post "$filename") + relevant_tags=$(echo "$tags_before $tags_after" | tr ',' ' ' | tr ' ' '\n' | sort -u | tr '\n' ' ') + if [[ -n $relevant_tags ]]; then + # shellcheck disable=SC2086 # Intended splitting of $relevant_tags + relevant_posts="$(posts_with_tags $relevant_tags) $filename" + rebuild_tags "$relevant_posts" "$relevant_tags" + fi +} diff --git a/lib/get_html_file_content.sh b/lib/get_html_file_content.sh new file mode 100644 index 0000000..28cf6b3 --- /dev/null +++ b/lib/get_html_file_content.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +# Reads HTML file from stdin, prints its content to stdout +# $1 where to start ("text" or "entry") +# $2 where to stop ("text" or "entry") +# $3 "cut" to remove text from


to +# note that this does not remove
line itself, +# so you can see if text was cut or not +get_html_file_content() { + declare cut_line cut_tags + declare template_tags_line_header + awk "//, //{ + if (!// && !//) print + if (\"$3\" == \"cut\" && /$cut_line/){ + if (\"$2\" == \"text\") exit # no need to read further + while (getline > 0 && !//) { + if (\"$cut_tags\" == \"no\" && /^

$template_tags_line_header/ ) print + } + } + }" +} \ No newline at end of file diff --git a/lib/get_post_author.sh b/lib/get_post_author.sh new file mode 100644 index 0000000..a086bd9 --- /dev/null +++ b/lib/get_post_author.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +# Return the post author +# +# $1 the html file +get_post_author() { + awk '/

.+/, //{if (!/
.+/ && !//) print}' "$1" | sed 's/<\/div>//g' +} \ No newline at end of file diff --git a/lib/get_post_title.sh b/lib/get_post_title.sh new file mode 100644 index 0000000..5a7b5b4 --- /dev/null +++ b/lib/get_post_title.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +# Return the post title +# +# $1 the html file +get_post_title() { + awk '/

/, /<\/a><\/h3>/{if (!/

/ && !/<\/a><\/h3>/) print}' "$1" +} \ No newline at end of file diff --git a/lib/global_variables.sh b/lib/global_variables.sh new file mode 100644 index 0000000..0e56d37 --- /dev/null +++ b/lib/global_variables.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2034 +# Global variables +# It is recommended to perform a 'rebuild' after changing any of this in the code + +# Config file. Any settings "key=value" written there will override the +# global_variables defaults. Useful to avoid editing bb.sh and having to deal +# with merges in VCS +global_config=".tildelog" + +# This function will load all the variables defined here. They might be overridden +# by the 'global_config' file contents +global_variables() { + global_software_name="tildelog" + global_software_version="0.1" + + # Blog title + global_title="my tildelog" + # The typical subtitle for each blog + global_description="a blog about tildes" + # The server base domain + global_domain="tilde.team" + # The public base URL for this blog + global_url="https://${global_domain}/~$USER/blog" + # Your name + global_author="~$USER" + # You can use twitter or facebook or anything for global_author_url + global_author_url="https://${global_domain}/~$USER/" + # Your email + global_email="$USER@${global_domain}" + + # CC by-nc-nd is a good starting point, you can change this to "©" for Copyright + global_license="CC by-nc-nd" + + + # Leave this empty (i.e. "") if you don't want to use feedburner, + # or change it to your own URL + global_feedburner="" + + # Change this to your username if you want to use twitter for comments + global_twitter_username="" + # Set this to false for a Twitter button with share count. The cookieless version + # is just a link. + global_twitter_cookieless="true" + + # Blog generated files + # index page of blog (it is usually good to use "index.html" here) + index_file="index.html" + number_of_index_articles="10" + # global archive + archive_index="all_posts.html" + tags_index="all_tags.html" + + # ignore gophermap file + gophermap="gophermap" + + # ignore gemini generation script and gemini index + gemini_index="index.gmi" + + # Non blogpost files. Bashblog will ignore these. Useful for static pages and custom content + # Add them as a bash array, e.g. non_blogpost_files=("news.html" "test.html") + # TODO: Dash have no support for arrays. Get rid of it! + non_blogpost_files="" + + # feed file (rss in this case) + blog_feed="feed.rss" + number_of_feed_articles="50" + # "cut" blog entry when putting it to index page. Leave blank for full articles in front page + # i.e. include only up to first '
', or '----' in markdown + cut_do="cut" + # When cutting, cut also tags? If "no", tags will appear in index page for cut articles + cut_tags="yes" + # Regexp matching the HTML line where to do the cut + # note that slash is regexp separator so you need to prepend it with backslash + cut_line='
' + # save markdown file when posting with "bb post -m". Leave blank to discard it. + save_markdown="yes" + # prefix for tags/categories files + # please make sure that no other html file starts with this prefix + prefix_tags="tag_" + # personalized header and footer (only if you know what you're doing) + # DO NOT name them .header.html, .footer.html or they will be overwritten + # leave blank to generate them, recommended + header_file="" + footer_file="" + # extra content to add just after we open the tag + # and before the actual blog content + body_begin_file="" + # extra content to add just before we cloese ) + body_end_file="" + # CSS files to include on every page, f.ex. css_include=('main.css' 'blog.css') + # leave empty to use generated + # TODO: Dash have no support for arrays. Get rid of it! + css_include="" + # HTML files to exclude from index, f.ex. post_exclude=('imprint.html 'aboutme.html') + # TODO: Dash have no support for arrays. Get rid of it! + html_exclude="" + + # Localization and i18n + # "Comments?" (used in twitter link after every post) + template_comments="comments?" + # "Read more..." (link under cut article on index page) + template_read_more="read more..." + # "View more posts" (used on bottom of index page as link to archive) + template_archive="archive" + # "All posts" (title of archive page) + template_archive_title="all posts" + # "All tags" + template_tags_title="all tags" + # "posts" (on "All tags" page, text at the end of each tag line, like "2. Music - 15 posts") + template_tags_posts="posts" + template_tags_posts_2_4="posts" # Some slavic languages use a different plural form for 2-4 items + template_tags_posts_singular="post" + # "Posts tagged" (text on a title of a page with index of one tag, like "My Blog - Posts tagged "Music"") + template_tag_title="posts tagged" + # "Tags:" (beginning of line in HTML file with list of all tags for this article) + template_tags_line_header="tags:" + # "Back to the index page" (used on archive page, it is link to blog index) + template_archive_index_page="back home" + # "Subscribe" (used on bottom of index page, it is link to RSS feed) + template_subscribe="rss" + # "Subscribe to this page..." (used as text for browser feed button that is embedded to html) + template_subscribe_browser_button="subscribe to this page..." + # "Tweet" (used as twitter text button for posting to twitter) + template_twitter_button="tweet" + template_twitter_comment="<type your comment here but please leave the URL so that other people can follow the comments>" + + # The locale to use for the dates displayed on screen + date_format="%B %d, %Y" + date_locale="C" + date_inpost="bashblog_timestamp" + # Don't change these dates + date_format_full="%a, %d %b %Y %H:%M:%S %z" + date_format_timestamp="%Y%m%d%H%M.%S" + date_allposts_header="%B %Y" + + # Perform the post title -> filename conversion + # Experts only. You may need to tune the locales too + # Leave empty for no conversion, which is not recommended + # This default filter respects backwards compatibility + convert_filename="iconv -f utf-8 -t ascii//translit | sed 's/^-*//' | tr [:upper:] [:lower:] | tr ' ' '-' | tr -dc '[:alnum:]-'" + + # URL where you can view the post while it's being edited + # same as global_url by default + # You can change it to path on your computer, if you write posts locally + # before copying them to the server + preview_url="" + + # Markdown location. Trying to autodetect by default. + # The invocation must support the signature 'markdown_bin in.md > out.html' + + markdown_bin=$(which md2html.awk) +} + +# Check for the validity of some variables +# DO NOT EDIT THIS FUNCTION unless you know what you're doing +global_variables_check() { + [[ $header_file == .header.html ]] && \ + echo "Please check your configuration. '.header.html' is not a valid value for the setting 'header_file'" && \ + exit + [[ $footer_file == .footer.html ]] && \ + echo "Please check your configuration. '.footer.html' is not a valid value for the setting 'footer_file'" && \ + exit +} \ No newline at end of file diff --git a/lib/is_boilerplate_file.sh b/lib/is_boilerplate_file.sh new file mode 100644 index 0000000..a942cd8 --- /dev/null +++ b/lib/is_boilerplate_file.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# Check if the file is a 'boilerplate' (i.e. not a post) +# The return values are designed to be used like this inside a loop: +# is_boilerplate_file && continue +# +# $1 the file +# +# Return 0 (bash return value 'true') if the input file is an index, feed, etc +# or 1 (bash return value 'false') if it is a blogpost +is_boilerplate_file() { + declare non_blogpost_files + declare index_file + declare archive_index + declare gophermap + declare gemini_index + declare tags_index + declare footer_file + declare header_file + declare prefix_tags + declare html_exclude + name=${1#./} + # First check against user-defined non-blogpost pages + for item in "${non_blogpost_files[@]}"; do + [[ "$name" == "$item" ]] && return 0 + done + + case $name in + ( "$index_file" | "$archive_index" | "$gophermap" | "$gemini_index" | "$tags_index" | "$footer_file" | "$header_file" | "$prefix_tags"* ) + return 0 ;; + ( * ) # Check for excluded + for excl in "${html_exclude[@]}"; do + [[ $name == "$excl" ]] && return 0 + done + return 1 ;; + esac +} \ No newline at end of file diff --git a/lib/list_posts.sh b/lib/list_posts.sh new file mode 100644 index 0000000..eb56e0d --- /dev/null +++ b/lib/list_posts.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +# Displays a list of the posts +list_posts() { + declare date_format + declare date_locale + if ls ./*.html > /dev/null; then + echo "No posts yet. Use 'bb.sh post' to create one" && return + fi + + lines="" + n=1 + while IFS='' read -r i; do + is_boilerplate_file "$i" && continue + line="$n # $(get_post_title "$i") # $(LC_ALL=$date_locale date -r "$i" +"$date_format")" + lines+=$line\\n + n=$(( n + 1 )) + done < <(ls -t ./*.html) + + echo -e "$lines" | column -t -s "#" +} \ No newline at end of file diff --git a/lib/list_tags.sh b/lib/list_tags.sh new file mode 100644 index 0000000..87dbdd0 --- /dev/null +++ b/lib/list_tags.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# Displays a list of the tags +# +# $2 if "-n", tags will be sorted by number of posts +list_tags() { + declare prefix_tags + declare template_tags_posts_singular + declare template_tags_posts + if [[ $2 == -n ]]; then do_sort=1; else do_sort=0; fi + + if ls "./${prefix_tags}"*.html > /dev/null; then + echo "No posts yet. Use 'bb.sh post' to create one" && return + fi + + lines="" + for i in "./${prefix_tags}"*.html; do + [[ -f "$i" ]] || break + nposts=$(grep -c "<\!-- text begin -->" "$i") + tagname=${i#"$prefix_tags"} + tagname=${tagname#.html} + ((nposts > 1)) && word=$template_tags_posts || word=$template_tags_posts_singular + line="$tagname # $nposts # $word" + lines+=$line\\n + done + + if (( do_sort == 1 )); then + echo -e "$lines" | column -t -s "#" | sort -nrk 2 + else + echo -e "$lines" | column -t -s "#" + fi +} diff --git a/lib/make_gemini.sh b/lib/make_gemini.sh new file mode 100644 index 0000000..bdf3624 --- /dev/null +++ b/lib/make_gemini.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +make_gemini() { + declare gemini_index + if [ ! -d "${HOME}/public_gemini" ]; then + printf "Creating ~/public_gemini\\n" + mkdir "${HOME}/public_gemini" + fi + + if [ ! -L "${HOME}/public_gemini/blog" ]; then + ln -sf "${HOME}/public_html/blog/" "${HOME}/public_gemini/blog" + fi + + if [ ! -f "${HOME}/public_gemini/blog/$gemini_index" ]; then + cat <<- 'EOF' > "${HOME}/public_gemini/blog/${gemini_index}" + #!/usr/bin/env sh + printf "20 text/gemini\r\n" + printf "my bashblog posts\r\n" + user=$(stat -c '%U' $0) + for post in $(ls -t /home/$user/public_gemini/blog/*.md); do + post=$(basename $post) + printf "=> /~$user/blog/$post $post\r\n" + done +EOF + chmod +x "${HOME}/public_gemini/blog/${gemini_index}" + fi +} \ No newline at end of file diff --git a/lib/make_gophermap.sh b/lib/make_gophermap.sh new file mode 100644 index 0000000..6efd0ed --- /dev/null +++ b/lib/make_gophermap.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +make_gophermap() { + declare gophermap + if [ ! -d "${HOME}/public_gopher" ]; then + printf "Creating gopher hole\\n" + mkdir "${HOME}/public_gopher" + fi + + if [ ! -L "${HOME}/public_gopher/blog" ]; then + ln -sf "${HOME}/public_html/blog/" "${HOME}/public_gopher/blog" + fi + + if [ ! -f "${HOME}/public_gopher/blog/$gophermap" ]; then + cat <<- 'EOF' > "${HOME}/public_html/blog/${gophermap}" + #!/usr/bin/env sh + printf "my bashblog posts\n" + user=$(stat -c '%U' .) + for post in $(ls -t *.md); do + post=$(basename $post) + printf "0$post\t/~$user/blog/$post\ttilde.team\t70\n" + done +EOF + chmod +x "${HOME}/public_html/blog/${gophermap}" + fi + chmod 644 "./*.md" +} \ No newline at end of file diff --git a/lib/make_rss.sh b/lib/make_rss.sh new file mode 100644 index 0000000..5be3067 --- /dev/null +++ b/lib/make_rss.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# Generate the feed file +make_rss() { + declare blog_feed + declare date_format_full + declare global_title + declare global_url + declare global_description + declare index_file + declare number_of_feed_articles + declare cut_do + echo -n "Making RSS " + + rssfile=$blog_feed.$RANDOM + while [[ -f $rssfile ]]; do rssfile=$blog_feed.$RANDOM; done + + { + pubdate=$(LC_ALL=C date +"$date_format_full") + echo '' + echo '' + echo "$global_title$global_url/$index_file" + echo "$global_descriptionen" + echo "$pubdate" + echo "$pubdate" + echo "" + + n=0 + while IFS='' read -r i; do + is_boilerplate_file "$i" && continue + ((n >= number_of_feed_articles)) && break # max 10 items + echo -n "." 1>&3 + echo '' + get_post_title "$i" + echo '$global_url/${i#./}" + echo "$global_url/$i" + echo "$(get_post_author "$i")" + echo "$(LC_ALL=C date -r "$i" +"$date_format_full")" + + n=$(( n + 1 )) + done < <(ls -t ./*.html) + + echo '' + } 3>&1 >"$rssfile" + echo "" + + mv "$rssfile" "$blog_feed" + chmod 644 "$blog_feed" +} \ No newline at end of file diff --git a/lib/mrkdwn.sh b/lib/mrkdwn.sh new file mode 100644 index 0000000..76daa5a --- /dev/null +++ b/lib/mrkdwn.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# Parse a Markdown file into HTML and return the generated file +declare markdown_bin +mrkdwn() { + out=${1%.md}.html + while [[ -f $out ]] + do + out=${out%.md}.$RANDOM.html + done + $markdown_bin "$1" > "$out" + echo "$out" +} \ No newline at end of file diff --git a/lib/parse_file.sh b/lib/parse_file.sh new file mode 100644 index 0000000..89371c3 --- /dev/null +++ b/lib/parse_file.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +# Parse the plain text file into an html file +# +# $1 source file name +# $2 (optional) timestamp for the file +# $3 (optional) destination file name +# note that although timestamp is optional, something must be provided at its +# place if destination file name is provided, i.e: +# parse_file source.txt "" destination.html +parse_file() { + declare convert_filename + declare template_tags_line_header + declare prefix_tags + declare global_author + # Read for the title and check that the filename is ok + title="" + while IFS='' read -r line; do + if [[ -z $title ]]; then + # remove extra

and

added by markdown + title=$(echo "$line" | sed 's/<\/*p>//g') + if [[ -n $3 ]]; then + filename=$3 + else + filename=$title + [[ -n $convert_filename ]] && filename=$(echo "$title" | eval "$convert_filename") + [[ -n $filename ]] || filename=$RANDOM # don't allow empty filenames + filename=$filename.html + + # Check for duplicate file names + while [[ -f $filename ]]; do + filename=${filename%.html}$RANDOM.html + done + fi + content=$filename.tmp + # Parse possible tags + elif [[ $line == "

$template_tags_line_header"* ]]; then + tags=$(echo "$line" | cut -d ":" -f 2- | sed -e 's/<\/p>//g' -e 's/^ *//' -e 's/ *$//' -e 's/, /,/g') + IFS=, read -r -a array <<< "$tags" + + echo -n "

$template_tags_line_header " >> "$content" + for item in "${array[@]}"; do + echo -n "$item, " + done | sed 's/, $/<\/p>/g' >> "$content" + else + echo "$line" >> "$content" + fi + done < "$1" + + # Create the actual html page + create_html_page "$content" "$filename" no "$title" "$2" "$global_author" + rm "$content" +} \ No newline at end of file diff --git a/lib/posts_with_tags.sh b/lib/posts_with_tags.sh new file mode 100644 index 0000000..9dd90c2 --- /dev/null +++ b/lib/posts_with_tags.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +# Finds all posts referenced in a number of tags. +# Arguments are tags +# Prints one line with space-separated tags to stdout +posts_with_tags() { + (($# < 1)) && return + set -- "${@/#/$prefix_tags}" + set -- "${@/%/.html}" + sed -n '/^

/{s/.*href="\([^"]*\)">.*/\1/;p;}' "$@" 2> /dev/null +} \ No newline at end of file diff --git a/lib/rebuild_all_entries.sh b/lib/rebuild_all_entries.sh new file mode 100644 index 0000000..e1ecc37 --- /dev/null +++ b/lib/rebuild_all_entries.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# Regenerates all the single post entries, keeping the post content but modifying +# the title, html structure, etc +rebuild_all_entries() { + declare date_inpost + declare date_format_full + declare date_format_timestamp + echo -n "Rebuilding all entries " + + for i in ./*.html; do + is_boilerplate_file "$i" && continue; + contentfile=.tmp.$RANDOM + while [[ -f $contentfile ]]; do contentfile=.tmp.$RANDOM; done + + echo -n "." + # Get the title and entry, and rebuild the html structure from scratch (divs, title, description...) + title=$(get_post_title "$i") + + get_html_file_content 'text' 'text' <"$i" >> "$contentfile" + + # Read timestamp from post, if present, and sync file timestamp + timestamp=$(awk '// { print }' "$i" | cut -d '#' -f 2) + [[ -n $timestamp ]] && touch -t "$timestamp" "$i" + # Read timestamp from file in correct format for 'create_html_page' + timestamp=$(LC_ALL=C date -r "$i" +"$date_format_full") + + create_html_page "$contentfile" "$i.rebuilt" no "$title" "$timestamp" "$(get_post_author "$i")" + # keep the original timestamp! + timestamp=$(LC_ALL=C date -r "$i" +"$date_format_timestamp") + mv "$i.rebuilt" "$i" + chmod 644 "$i" + touch -t "$timestamp" "$i" + rm "$contentfile" + done + echo "" +} \ No newline at end of file diff --git a/lib/rebuild_index.sh b/lib/rebuild_index.sh new file mode 100644 index 0000000..2c8920a --- /dev/null +++ b/lib/rebuild_index.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +# Generate the index.html with the content of the latest posts +rebuild_index() { + declare index_file + declare number_of_index_articles + declare cut_do + declare cut_line + declare template_read_more + declare template_archive + declare template_subscribe + declare template_tags_title + declare blog_feed + declare global_author + declare global_feedburner + declare global_title + declare archive_index + declare tags_index + echo -n "Rebuilding the index " + newindexfile=$index_file.$RANDOM + contentfile=$newindexfile.content + while [[ -f $newindexfile ]]; do + newindexfile=$index_file.$RANDOM + contentfile=$newindexfile.content + done + + # Create the content file + { + n=0 + while IFS='' read -r i; do + is_boilerplate_file "$i" && continue; + if ((n >= number_of_index_articles)); then break; fi + if [[ -n $cut_do ]]; then + get_html_file_content 'entry' 'entry' 'cut' <"$i" | awk "/$cut_line/ { print \"

$template_read_more

\" ; next } 1" + else + get_html_file_content 'entry' 'entry' <"$i" + fi + echo -n "." 1>&3 + n=$(( n + 1 )) + done < <(ls -t ./*.html) # sort by date, newest first + + feed=$blog_feed + if [[ -n $global_feedburner ]]; then feed=$global_feedburner; fi + echo "" + } 3>&1 >"$contentfile" + + echo "" + + create_html_page "$contentfile" "$newindexfile" yes "$global_title" "$global_author" + rm "$contentfile" + mv "$newindexfile" "$index_file" + chmod 644 "$index_file" +} \ No newline at end of file diff --git a/lib/rebuild_tags.sh b/lib/rebuild_tags.sh new file mode 100644 index 0000000..e1da522 --- /dev/null +++ b/lib/rebuild_tags.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +# Rebuilds tag_*.html files +# if no arguments given, rebuilds all of them +# if arguments given, they should have this format: +# "FILE1 [FILE2 [...]]" "TAG1 [TAG2 [...]]" +# where FILEn are files with posts which should be used for rebuilding tags, +# and TAGn are names of tags which should be rebuilt. +# example: +# rebuild_tags "one_post.html another_article.html" "example-tag another-tag" +# mind the quotes! +rebuild_tags() { + declare prefix_tags + declare cut_do + declare cut_line + declare template_read_more + declare template_tag_title + declare global_title + declare global_author + if (($# < 2)); then + # will process all files and tags + files=$(ls -t ./*.html) + all_tags=yes + else + # will process only given files and tags + files=$(printf '%s\n' "$1" | sort -u) + # shellcheck disable=SC2086 # Intended splitting of $files + files=$(ls -t $files) + tags=$2 + fi + echo -n "Rebuilding tag pages " + #n=0 + if [[ -n $all_tags ]]; then + rm ./"$prefix_tags"*.html &> /dev/null + else + for i in $tags; do + rm "./$prefix_tags$i.html" &> /dev/null + done + fi + # First we will process all files and create temporal tag files + # with just the content of the posts + tmpfile=tmp.$RANDOM + while [[ -f $tmpfile ]]; do tmpfile=tmp.$RANDOM; done + while IFS='' read -r i; do + is_boilerplate_file "$i" && continue; + echo -n "." + if [[ -n $cut_do ]]; then + get_html_file_content 'entry' 'entry' 'cut' <"$i" | awk "/$cut_line/ { print \"

$template_read_more

\" ; next } 1" + else + get_html_file_content 'entry' 'entry' <"$i" + fi >"$tmpfile" + for tag in $(tags_in_post "$i"); do + if [[ -n $all_tags || " $tags " == *" $tag "* ]]; then + cat "$tmpfile" >> "$prefix_tags$tag".tmp.html + fi + done + done <<< "$files" + rm "$tmpfile" + # Now generate the tag files with headers, footers, etc + while IFS='' read -r i; do + tagname=${i#./"$prefix_tags"} + tagname=${tagname%.tmp.html} + create_html_page "$i" "$prefix_tags$tagname.html" yes "$global_title — $template_tag_title \"$tagname\"" "$global_author" + rm "$i" + done < <(ls -t ./"$prefix_tags"*.tmp.html 2>/dev/null) + echo +} \ No newline at end of file diff --git a/lib/reset.sh b/lib/reset.sh new file mode 100644 index 0000000..216620f --- /dev/null +++ b/lib/reset.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +# Delete all generated content, leaving only this script +reset() { + echo "Are you sure you want to delete all blog entries? Please write \"Yes, I am!\" " + read -r line + if [[ $line == "Yes, I am!" ]]; then + rm .*.html ./*.html ./*.css ./*.rss &> /dev/null + echo + echo "Deleted all posts, stylesheets and feeds." + echo "Kept your old '.backup.tar.gz' just in case, please delete it manually if needed." + else + echo "Phew! You dodged a bullet there. Nothing was modified." + fi +} \ No newline at end of file diff --git a/lib/tags_in_post.sh b/lib/tags_in_post.sh new file mode 100644 index 0000000..35c6eda --- /dev/null +++ b/lib/tags_in_post.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# Finds all tags referenced in one post. +# Accepts either filename as first argument, or post content at stdin +# Prints one line with space-separated tags to stdout +tags_in_post() { + declare template_tags_line_header + sed -n "/^

$template_tags_line_header/{s/^

$template_tags_line_header//;s/<[^>]*>//g;s/[ ,]\+/ /g;p;}" "$1" | tr ', ' ' ' +} \ No newline at end of file diff --git a/lib/test_markdown.sh b/lib/test_markdown.sh new file mode 100644 index 0000000..1ad49ef --- /dev/null +++ b/lib/test_markdown.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Test if the markdown script is working correctly +test_markdown() { + declare markdown_bin + if [[ -n $markdown_bin ]]; then + local m1; m1=$("$markdown_bin" <<< "$'line 1\n\nline 2'") + local m2; m2=$("$markdown_bin" <<< "$'line 1\n\nline 2'") + local c1=$'

line 1

\n\n

line 2

' + local c2=$'

line 1

\n

line 2

' + # shellcheck disable=SC22350 # must enforce order of operations + [[ "$m1" == "$c1" ]] || [[ "$m2" == "$c2" ]] + else + return 1 + fi +} \ No newline at end of file diff --git a/lib/twitter_card.sh b/lib/twitter_card.sh new file mode 100644 index 0000000..3366cb7 --- /dev/null +++ b/lib/twitter_card.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +# Create a Twitter summary (twitter "card") for the post +# +# $1 the post file +# $2 the title +twitter_card() { + declare global_twitter_username + declare template_tags_line_header + declare global_url + declare global_twitter_cookieless + declare template_twitter_button + declare template_twitter_comment + declare template_comments + [[ -z $global_twitter_username ]] && return + + echo "" + echo "" + echo "" # Twitter truncates at 70 char + description=$(grep -v "^

$template_tags_line_header" "$1" | sed -e 's/<[^>]*>//g' | head -c 250 | tr '\n' ' ' | sed "s/\"/'/g") + echo "" + image=$(sed -n 's/.*" +} + +# Adds the code needed by the twitter button +# +# $1 the post URL +twitter() { + [[ -z $global_twitter_username ]] && return + + if [[ $global_twitter_cookieless == true ]]; then + id=$RANDOM + + search_engine="https://twitter.com/search?q=" + + echo "

$template_comments $template_twitter_button " + echo " 

" + return; + else + echo "

$template_comments "; + fi + + echo "$template_twitter_button " + echo "

" +} \ No newline at end of file diff --git a/lib/usage.sh b/lib/usage.sh new file mode 100644 index 0000000..a5e00bd --- /dev/null +++ b/lib/usage.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +declare global_software_name +declare global_software_version +# Displays the help +usage() { + echo "$global_software_name $global_software_version" + echo "usage: $0 command [filename]" + echo "" + echo "Commands:" + echo " post [filename] insert a new blog post in markdown, or the filename of a draft to continue editing it" + echo " edit [-n] [filename] edit an already published .md file. **NEVER** edit manually a published .html file," + echo " always use this function as it keeps internal data and rebuilds the blog" + echo " use '-n' to give the file a new name, if title was changed" + echo " delete [filename] deletes the post and rebuilds the blog" + echo " rebuild regenerates all the pages and posts, preserving the content of the entries" + echo " reset deletes everything except this script. Use with a lot of caution and back up first!" + echo " list list all posts" + echo " tags [-n] list all tags in alphabetical order" + echo " use '-n' to sort list by number of posts" + echo "" + echo "for more information please see https://tilde.team/wiki/?page=tildeblogs" + echo "source here: https://tildegit.org/team/bashblog" +} diff --git a/lib/write_entry.sh b/lib/write_entry.sh new file mode 100644 index 0000000..ed49021 --- /dev/null +++ b/lib/write_entry.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash + +# Manages the creation of the text file and the parsing to html file +# also the drafts +write_entry() { + declare template_tags_line_header + declare global_url + declare convert_filename + declare save_markdown + test_markdown && fmt=md || fmt=html + f=$2 + [[ $2 == -html ]] && fmt=html && f=$3 + + if [[ -n $f ]]; then + TMPFILE=$f + if [[ ! -f $TMPFILE ]]; then + echo "The file doesn't exist" + delete_includes + exit + fi + # guess format from TMPFILE + extension=${TMPFILE##*.} + [[ $extension == md || $extension == html ]] && fmt=$extension + # but let user override it (`bb.sh post -html file.md`) + [[ $2 == -html ]] && fmt=html + # Test if Markdown is working before re-posting a .md file + if [[ $extension == md ]]; then + if test_markdown; then + echo "Markdown is not working, please edit HTML file directly." + exit + fi + fi + else + TMPFILE=.entry-$RANDOM.$fmt + echo -e "Title on this line\n" >> "$TMPFILE" + + [[ $fmt == html ]] && cat << EOF >> "$TMPFILE" +

The rest of the text file is an html blog post. The process will continue as soon +as you exit your editor.

+ +

$template_tags_line_header keep-this-tag-format, tags-are-optional, example

+EOF + + [[ $fmt == md ]] && cat << EOF >> "$TMPFILE" +The rest of the text file is a **Markdown** blog post. The process will continue +as soon as you exit your editor. + +$template_tags_line_header keep-this-tag-format, tags-are-optional, beware-with-underscores-in-markdown, example +EOF + fi + chmod 600 "$TMPFILE" + + post_status="E" + filename="" + while [[ $post_status != "p" && $post_status != "P" ]]; do + [[ -n $filename ]] && rm "$filename" # Delete the generated html file, if any + $EDITOR "$TMPFILE" + if [[ $fmt == md ]]; then + html_from_md=$(mrkdwn "$TMPFILE") + parse_file "$html_from_md" + rm "$html_from_md" + else + parse_file "$TMPFILE" # this command sets $filename as the html processed file + fi + + chmod 644 "$filename" + [[ -n $preview_url ]] || preview_url=$global_url + echo "To preview the entry, open $preview_url/$filename in your browser" + + echo -n "[P]ost this entry, [E]dit again, [D]raft for later? (p/E/d) " + read -r post_status + if [[ $post_status == d || $post_status == D ]]; then + mkdir -p "drafts/" + chmod 700 "drafts/" + + title=$(head -n 1 $TMPFILE) + [[ -n $convert_filename ]] && title=$(echo "$title" | eval "$convert_filename") + [[ -n $title ]] || title=$RANDOM + + draft=drafts/$title.$fmt + mv "$TMPFILE" "$draft" + chmod 600 "$draft" + rm "$filename" + delete_includes + echo "Saved your draft as '$draft'" + exit + fi + done + + if [[ $fmt == md && -n $save_markdown ]]; then + mv "$TMPFILE" "${filename%%.*}.md" + else + rm "$TMPFILE" + fi + chmod 644 "$filename" + echo "Posted $filename" + relevant_tags=$(tags_in_post "$filename") + if [[ -n $relevant_tags ]]; then + # shellcheck disable=SC2086 # Intended splitting of $relevant_tags + relevant_posts="$(posts_with_tags $relevant_tags) $filename" + rebuild_tags "$relevant_posts" "$relevant_tags" + fi +} \ No newline at end of file diff --git a/md2gemini.awk b/md2gemini.awk new file mode 100755 index 0000000..c77c168 --- /dev/null +++ b/md2gemini.awk @@ -0,0 +1,11 @@ +#!/bin/awk -f +# +# by: Josemar Lohn +# lo.hn on www/gemini/gopher +# +# Based on md2html by Jesus Galan (yiyus) 2009 +# +# +# Usage: md2gemini.awk file.md > file.html + +1 \ No newline at end of file diff --git a/md2gopher.awk b/md2gopher.awk new file mode 100755 index 0000000..7b58a4b --- /dev/null +++ b/md2gopher.awk @@ -0,0 +1,560 @@ +#!/bin/awk -f +# +# by: Josemar Lohn +# lo.hn on www/gemini/gopher +# +# Based on md2html by Jesus Galan (yiyus) 2009 +# +# +# Usage: md2gopher.awk file.md > file.html + +function eschtml(t) { + #gsub("&", "\\&", t); + #gsub("<", "\\<", t); + return t; +} + +function oprint(t){ + if(nr == 0) + print t; + else + otext = otext "\n" t; +} + +# https://unix.stackexchange.com/a/94751 +function centralize(t,c){ + L = col - c - length(t); + #print("====" length(t) "=====" t "=====\n") + for(i=1; i<=int(L/2); i++) + t = " "t; + for(i=1; i<=int(L/2+.5); i++) + t = t " " + return t +} + + +# function from https://unix.stackexchange.com/a/282338 +function justify(t,i,nbchar,nbspc,spaces,spcpf,r){ +$0=t +if (NF <= 1) { return t } +else { + nbchar = 0 + for (i = 1; i <= NF; i++) { + nbchar += length($i) + } + nbspc = col - nbchar + spcpf = int(nbspc / (NF - 1)) + for (i = 1; i < NF; i++) { + r = r $i + spaces = (NF == 2 || i == NF - 1) ? nbspc : spcpf + if (spaces < 1) spaces = 1 + for (j = 0; j < spaces; j++) { + r = r " " + } + nbspc -= spaces + } + r = r $NF +} +return r +} + +function subref(id){ + for(; nr > 0 && sub("<<" id, ref[id], otext); nr--); + if(nr == 0 && otext) { + print otext; + otext = ""; + } +} + +function nextil(t) { + if(!match(t, /[`<&\[*_\\-]|(!\[)|(\[\^)/)) + return t; + t1 = substr(t, 1, RSTART - 1); + tag = substr(t, RSTART, RLENGTH); + t2 = substr(t, RSTART + RLENGTH); + if(ilcode && tag != "`") + return eschtml(t1 tag) nextil(t2); + # Backslash escaping + if(tag == "\\"){ + if(match(t2, /^[\\`*_{}\[\]()#+\-\.!]/)){ + tag = substr(t2, 1, 1); + t2 = substr(t2, 2); + } + return t1 tag nextil(t2); + } + # Dashes + if(tag == "-"){ + if(sub(/^-/, "", t2)) + tag = "—"; + return t1 tag nextil(t2); + } + # Inline Code + if(tag == "`"){ + if(sub(/^`/, "", t2)){ + if(!match(t2, /``/)) + return t1 "”" nextil(t2); + ilcode2 = !ilcode2; + } + else if(ilcode2) + return t1 tag nextil(t2); + tag = ""; + if(ilcode){ + t1 = eschtml(t1); + tag = ""; + } + ilcode = !ilcode; + return t1 tag nextil(t2); + } + if(tag == "<"){ + # Autolinks + if(match(t2, /^[^ ]+[\.@][^ ]+>/)){ + url = eschtml(substr(t2, 1, RLENGTH - 1)); + t2 = substr(t2, RLENGTH + 1); + linktext = url; + if(match(url, /@/) && !match(url, /^mailto:/)) + url = "mailto:" url; + return t1 "" linktext "" nextil(t2); + } + # Html tags + if(match(t2, /^[A-Za-z\/!][^>]*>/)){ + tag = tag substr(t2, RSTART, RLENGTH); + t2 = substr(t2, RLENGTH + 1); + return t1 tag nextil(t2); + } + return t1 "<" nextil(t2); + } + # Html special entities + if(tag == "&"){ + if(match(t2, /^#?[A-Za-z0-9]+;/)){ + tag = tag substr(t2, RSTART, RLENGTH); + t2 = substr(t2, RLENGTH + 1); + return t1 tag nextil(t2); + } + return t1 "&" nextil(t2); + } + # Images + if(tag == "!["){ + if(!match(t2, /(\[.*\])|(\(.*\))/)) + return t1 tag nextil(t2); + match(t2, /^[^\]]*/); + alt = substr(t2, 1, RLENGTH); + t2 = substr(t2, RLENGTH + 2); + if(match(t2, /^\(/)){ + # Inline + sub(/^\(/, "", t2); + match(t2, /^[^\)]+/); + url = eschtml(substr(t2, 1, RLENGTH)); + t2 = substr(t2, RLENGTH + 2); + title = ""; + if(match(url, /[ ]+".*"[ ]*$/)) { + title = substr(url, RSTART, RLENGTH); + url = substr(url, 1, RSTART - 1); + match(title, /".*"/); + title = " title=\"" substr(title, RSTART + 1, RLENGTH - 2) "\""; + } + if(match(url, /^<.*>$/)) + url = substr(url, 2, RLENGTH - 2); + return t1 "\""" nextil(t2); + } + else{ + # Referenced + sub(/^ ?\[/, "", t2); + id = alt; + if(match(t2, /^[^\]]+/)) + id = substr(t2, 1, RLENGTH); + t2 = substr(t2, RLENGTH + 2); + if(ref[id]) + r = ref[id]; + else{ + r = "<<" id; + nr++; + } + return t1 "\""" nextil(t2); + } + } + # Footnotes + if(tag == "[^"){ + match(t2, /^[^\]]*(\[[^\]]*\][^\]]*)*/); + linktext = substr(t2, 1, RLENGTH); + t2 = substr(t2, RLENGTH + 2); + return t1 "" linktext "" nextil(t2); + } + # Links + if(tag == "["){ + if(!match(t2, /(\[.*\])|(\(.*\))/)) + return t1 tag nextil(t2); + match(t2, /^[^\]]*(\[[^\]]*\][^\]]*)*/); + linktext = substr(t2, 1, RLENGTH); + t2 = substr(t2, RLENGTH + 2); + if(match(t2, /^\(/)){ + # Inline + match(t2, /^[^\)]+(\([^\)]+\)[^\)]*)*/); + url = substr(t2, 2, RLENGTH - 1); + pt2 = substr(t2, RLENGTH + 2); + title = ""; + if(match(url, /[ ]+".*"[ ]*$/)) { + title = substr(url, RSTART, RLENGTH); + url = substr(url, 1, RSTART - 1); + match(title, /".*"/); + title = " title=\"" substr(title, RSTART + 1, RLENGTH - 2) "\""; + } + if(match(url, /^<.*>$/)) + url = substr(url, 2, RLENGTH - 2); + url = eschtml(url); + return t1 "" nextil(linktext) "" nextil(pt2); + } + else{ + # Referenced + sub(/^ ?\[/, "", t2); + id = linktext; + if(match(t2, /^[^\]]+/)) + id = substr(t2, 1, RLENGTH); + t2 = substr(t2, RLENGTH + 2); + if(ref[id]) + r = ref[id]; + else{ + r = "<<" id; + nr++; + } + pt2 = t2; + return t1 "" nextil(linktext) "" nextil(pt2); + } + } + # Emphasis + if(match(tag, /[*_]/)){ + ntag = tag; + if(sub("^" tag, "", t2)){ + if(stag[ns] == tag && match(t2, "^" tag)) + t2 = tag t2; + else + ntag = tag tag + } + n = length(ntag); + tag = (n == 2) ? "strong" : "em"; + if(match(t1, / $/) && match(t2, /^ /)) + return t1 tag nextil(t2); + if(stag[ns] == ntag){ + tag = "/" tag; + ns--; + } + else + stag[++ns] = ntag; + tag = "<" tag ">"; + return t1 tag nextil(t2); + } +} + +function inline(t) { + ilcode = 0; + ilcode2 = 0; + ns = 0; + + return nextil(t); +} + +#https://unix.stackexchange.com/a/337656 +function wrap(t,align,q,y,z,final) { + while (t) + { + q = match(t, / |$/); y += q + if (y > col) { + if (align != 0) + { + if (align=="c") + #print "zzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + final = final centralize(z) RS + if (align=="j") + final = final justify(z) RS + } + else + { + final = final z RS + } + y = q - 1 + z = "" + } + else if (z) z = z FS + z = z substr(t, 1, q - 1) + t = substr(t, q + 1) + } + if (align=="c") + { + final = final centralize(z) + } + else { + final = final z + } + return final +} + +function printp(tag) { + if(!match(text, /^[ ]*$/)){ + text = inline(text); + if(tag == "p") + { + oprint(wrap(text,"j")) + } + else + { + oprint(text); + } + } + text = ""; +} + +BEGIN { + blank = 0; + code = 0; + hr = 0; + html = 0; + nl = 0; + nr = 0; + otext = ""; + text = ""; + par = "p"; + col=70; + c=0; do { lineheader = "=" lineheader; c++ } while ( c < col ) + c=0; do { lineheadersmall = "-" lineheadersmall; c++ } while ( c < col ) +} + +# References +!code && /^ *\[\^![^\]]*\]:[ ]+/ { + sub(/^ *\[\^!/, ""); + match($0, /\]/); + id = substr($0, 1, RSTART - 1); + sub(id "\\]:[ ]+", ""); + title = ""; + if(match($0, /".*"$/)) + title = "\" title=\"" substr($0, RSTART + 1, RLENGTH - 2); + sub(/[ ]+".*"$/, ""); + url = eschtml($0); + ref[id] = url title; + + subref(id); + next; +} + +!code && /^ *\[\^[^\]]*\]:[ ]+/ { + sub(/^ *\[\^/, ""); + match($0, /\]/); + id = substr($0, 1, RSTART - 1); + sub(id "\\]:[ ]+", ""); + sub(/[ ]+".*"$/, ""); + url = eschtml($0); + fnref[id] = url; + + subref(id); + next; +} + +# html +!html && /^<(address|blockquote|center|dir|div|dl|fieldset|form|h[1-6r]|\ +isindex|menu|noframes|noscript|ol|p|pre|table|ul|!--)/ { + if(code) + oprint(""); + for(; !text && block[nl] == "blockquote"; nl--) + oprint(""); + match($0, /^<(address|blockquote|center|dir|div|dl|fieldset|form|h[1-6r]|\ + isindex|menu|noframes|noscript|ol|p|pre|table|ul|!--)/); + htag = substr($0, 2, RLENGTH - 1); + if(!match($0, "(<\\/" htag ">)|((^
$)")) + html = 1; + if(html && match($0, /^
$/ || +(hr && />$/)) { + html = 0; + hr = 0; + oprint($0); + next; +} + +html { + oprint($0); + next; +} + +# List and quote blocks + +# Remove indentation +{ + for(nnl = 0; nnl < nl; nnl++) + if((match(block[nnl + 1], /[ou]l/) && !sub(/^( | )/, "")) || \ + (block[nnl + 1] == "blockquote" && !sub(/^> ?/, ""))) + break; +} +nnl < nl && !blank && text && ! /^ ? ? ?([*+-]|([0-9]+\.)+)( +| )/ { nnl = nl; } +# Quote blocks +{ + while(sub(/^> /, "")) + nblock[++nnl] = "blockquote"; +} +# Horizontal rules +{ hr = 0; } +(blank || (!text && !code)) && /^ ? ? ?([-*_][ ]*)([-*_][ ]*)([-*_][ ]*)+$/ { + if(code){ + oprint(""); + code = 0; + } + blank = 0; + nnl = 0; + hr = 1; +} +# List items +block[nl] ~ /[ou]l/ && /^$/ { + blank = 1; + next; +} +{ newli = 0; } +!hr && (nnl != nl || !text || block[nl] ~ /[ou]l/) && /^ ? ? ?[*+-]( +| )/ { + sub(/^ ? ? ?[*+-]( +| )/, ""); + nnl++; + nblock[nnl] = "ul"; + newli = 1; +} +(nnl != nl || !text || block[nl] ~ /[ou]l/) && /^ ? ? ?([0-9]+\.)+( +| )/ { + sub(/^ ? ? ?([0-9]+\.)+( +| )/, ""); + nnl++; + nblock[nnl] = "ol"; + newli = 1; +} +newli { + if(blank && nnl == nl && !par) + par = "p"; + blank = 0; + printp(par); + if(nnl == nl && block[nl] == nblock[nl]) + oprint("
  • "); +} +blank && ! /^$/ { + if(match(block[nnl], /[ou]l/) && !par) + par = "p"; + printp(par); + par = "p"; + blank = 0; +} + +# Close old blocks and open new ones +nnl != nl || nblock[nl] != block[nl] { + if(code){ + oprint(""); + code = 0; + } + printp(par); + b = (nnl > nl) ? nblock[nnl] : block[nnl]; + par = (match(b, /[ou]l/)) ? "" : "p"; +} +nnl < nl || (nnl == nl && nblock[nl] != block[nl]) { + for(; nl > nnl || (nnl == nl && pblock[nl] != block[nl]); nl--){ + if(match(block[nl], /[ou]l/)) + oprint("
  • "); + oprint(""); + } +} +nnl > nl { + for(; nl < nnl; nl++){ + block[nl + 1] = nblock[nl + 1]; + oprint("<" block[nl + 1] ">"); + if(match(block[nl + 1], /[ou]l/)) + oprint("
  • "); + } +} +hr { + oprint(lineheader); + next; +} + +# Code blocks +code && /^$/ { + if(blanK) + oprint(""); + blank = 1; + next; +} +!text && sub(/^( | )/, "") { + if(blanK) + oprint(""); + blank = 0; + if(!code) + oprint("
    ");
    +	code = 1;
    +	$0 = eschtml($0);
    +	oprint($0);
    +	next;
    +}
    +code {
    +	oprint("
    "); + code = 0; +} + +# Setex-style Headers +text && /^=+$/ {printp("h1"); next;} +text && /^-+$/ {printp("h2"); next;} + +# Atx-Style headers +/^#+/ && (!newli || par=="p" || /^##/) { + for(n = 0; n < 6 && sub(/^# */, ""); n++) + { + sub(/#$/, ""); + } + par = "h" n; + if (n == 1) { + oprint( text lineheader "\n=" centralize($0,2) "=\n" lineheader "\n" ) + next; + } + if (n == 2) { + oprint("\n" text wrap($0,"c") "\n" lineheader "\n") + next; + } + if (n == 3) { + oprint(text centralize($0) "\n" lineheadersmall "\n") + next; + } + if (n > 3) { + text = text centralize($0) "\n" + next; + } +} + +# Paragraph +/^$/ { + printp(par); + par = "p"; + next; +} + +# Add text +{ text = (text ? text " " : "") $0; } + +function alen(a, ix, k) { + k = 0 + for(ix in a) k++ + return k +} + +END { + if(code){ + oprint(""); + code = 0; + } + printp(par); + for(; nl > 0; nl--){ + if(match(block[nl], /[ou]l/)) + oprint("
  • "); + oprint(""); + } + gsub(/<<[^"]*/, "", otext); + print(otext); + + # Print footnotes + if(alen(fnref)>0) { + print "
      "; + for (i in fnref) print "
    • " i ": " inline(fnref[i]) " ↩︎
    • "; + print "
    "; + } +} diff --git a/md2html.awk b/md2html.awk new file mode 100755 index 0000000..b981c04 --- /dev/null +++ b/md2html.awk @@ -0,0 +1,463 @@ +#!/bin/awk -f +# +# by: Josemar Lohn +# lo.hn on www/gemini/gopher +# +# Based on md2html by Jesus Galan (yiyus) 2009 +# +# +# Usage: md2html.awk file.md > file.html + +function eschtml(t) { + gsub("&", "\\&", t); + gsub("<", "\\<", t); + return t; +} + +function oprint(t){ + if(nr == 0) + print t; + else + otext = otext "\n" t; +} + +function subref(id){ + for(; nr > 0 && sub("<<" id, ref[id], otext); nr--); + if(nr == 0 && otext) { + print otext; + otext = ""; + } +} + +function nextil(t) { + if(!match(t, /[`<&\[*_\\-]|(!\[)|(\[\^)/)) + return t; + t1 = substr(t, 1, RSTART - 1); + tag = substr(t, RSTART, RLENGTH); + t2 = substr(t, RSTART + RLENGTH); + if(ilcode && tag != "`") + return eschtml(t1 tag) nextil(t2); + # Backslash escaping + if(tag == "\\"){ + if(match(t2, /^[\\`*_{}\[\]()#+\-\.!]/)){ + tag = substr(t2, 1, 1); + t2 = substr(t2, 2); + } + return t1 tag nextil(t2); + } + # Dashes + if(tag == "-"){ + if(sub(/^-/, "", t2)) + tag = "—"; + return t1 tag nextil(t2); + } + # Inline Code + if(tag == "`"){ + if(sub(/^`/, "", t2)){ + if(!match(t2, /``/)) + return t1 "”" nextil(t2); + ilcode2 = !ilcode2; + } + else if(ilcode2) + return t1 tag nextil(t2); + tag = ""; + if(ilcode){ + t1 = eschtml(t1); + tag = ""; + } + ilcode = !ilcode; + return t1 tag nextil(t2); + } + if(tag == "<"){ + # Autolinks + if(match(t2, /^[^ ]+[\.@][^ ]+>/)){ + url = eschtml(substr(t2, 1, RLENGTH - 1)); + t2 = substr(t2, RLENGTH + 1); + linktext = url; + if(match(url, /@/) && !match(url, /^mailto:/)) + url = "mailto:" url; + return t1 "" linktext "" nextil(t2); + } + # Html tags + if(match(t2, /^[A-Za-z\/!][^>]*>/)){ + tag = tag substr(t2, RSTART, RLENGTH); + t2 = substr(t2, RLENGTH + 1); + return t1 tag nextil(t2); + } + return t1 "<" nextil(t2); + } + # Html special entities + if(tag == "&"){ + if(match(t2, /^#?[A-Za-z0-9]+;/)){ + tag = tag substr(t2, RSTART, RLENGTH); + t2 = substr(t2, RLENGTH + 1); + return t1 tag nextil(t2); + } + return t1 "&" nextil(t2); + } + # Images + if(tag == "!["){ + if(!match(t2, /(\[.*\])|(\(.*\))/)) + return t1 tag nextil(t2); + match(t2, /^[^\]]*/); + alt = substr(t2, 1, RLENGTH); + t2 = substr(t2, RLENGTH + 2); + if(match(t2, /^\(/)){ + # Inline + sub(/^\(/, "", t2); + match(t2, /^[^\)]+/); + url = eschtml(substr(t2, 1, RLENGTH)); + t2 = substr(t2, RLENGTH + 2); + title = ""; + if(match(url, /[ ]+".*"[ ]*$/)) { + title = substr(url, RSTART, RLENGTH); + url = substr(url, 1, RSTART - 1); + match(title, /".*"/); + title = " title=\"" substr(title, RSTART + 1, RLENGTH - 2) "\""; + } + if(match(url, /^<.*>$/)) + url = substr(url, 2, RLENGTH - 2); + return t1 "\""" nextil(t2); + } + else{ + # Referenced + sub(/^ ?\[/, "", t2); + id = alt; + if(match(t2, /^[^\]]+/)) + id = substr(t2, 1, RLENGTH); + t2 = substr(t2, RLENGTH + 2); + if(ref[id]) + r = ref[id]; + else{ + r = "<<" id; + nr++; + } + return t1 "\""" nextil(t2); + } + } + # Footnotes + if(tag == "[^"){ + match(t2, /^[^\]]*(\[[^\]]*\][^\]]*)*/); + linktext = substr(t2, 1, RLENGTH); + t2 = substr(t2, RLENGTH + 2); + return t1 "" linktext "" nextil(t2); + } + # Links + if(tag == "["){ + if(!match(t2, /(\[.*\])|(\(.*\))/)) + return t1 tag nextil(t2); + match(t2, /^[^\]]*(\[[^\]]*\][^\]]*)*/); + linktext = substr(t2, 1, RLENGTH); + t2 = substr(t2, RLENGTH + 2); + if(match(t2, /^\(/)){ + # Inline + match(t2, /^[^\)]+(\([^\)]+\)[^\)]*)*/); + url = substr(t2, 2, RLENGTH - 1); + pt2 = substr(t2, RLENGTH + 2); + title = ""; + if(match(url, /[ ]+".*"[ ]*$/)) { + title = substr(url, RSTART, RLENGTH); + url = substr(url, 1, RSTART - 1); + match(title, /".*"/); + title = " title=\"" substr(title, RSTART + 1, RLENGTH - 2) "\""; + } + if(match(url, /^<.*>$/)) + url = substr(url, 2, RLENGTH - 2); + url = eschtml(url); + return t1 "" nextil(linktext) "" nextil(pt2); + } + else{ + # Referenced + sub(/^ ?\[/, "", t2); + id = linktext; + if(match(t2, /^[^\]]+/)) + id = substr(t2, 1, RLENGTH); + t2 = substr(t2, RLENGTH + 2); + if(ref[id]) + r = ref[id]; + else{ + r = "<<" id; + nr++; + } + pt2 = t2; + return t1 "" nextil(linktext) "" nextil(pt2); + } + } + # Emphasis + if(match(tag, /[*_]/)){ + ntag = tag; + if(sub("^" tag, "", t2)){ + if(stag[ns] == tag && match(t2, "^" tag)) + t2 = tag t2; + else + ntag = tag tag + } + n = length(ntag); + tag = (n == 2) ? "strong" : "em"; + if(match(t1, / $/) && match(t2, /^ /)) + return t1 tag nextil(t2); + if(stag[ns] == ntag){ + tag = "/" tag; + ns--; + } + else + stag[++ns] = ntag; + tag = "<" tag ">"; + return t1 tag nextil(t2); + } +} + +function inline(t) { + ilcode = 0; + ilcode2 = 0; + ns = 0; + + return nextil(t); +} + +function printp(tag) { + if(!match(text, /^[ ]*$/)){ + text = inline(text); + if(tag != "") + oprint("<" tag ">" text ""); + else + oprint(text); + } + text = ""; +} + +BEGIN { + blank = 0; + code = 0; + hr = 0; + html = 0; + nl = 0; + nr = 0; + otext = ""; + text = ""; + par = "p"; +} + +# References +!code && /^ *\[\^![^\]]*\]:[ ]+/ { + sub(/^ *\[\^!/, ""); + match($0, /\]/); + id = substr($0, 1, RSTART - 1); + sub(id "\\]:[ ]+", ""); + title = ""; + if(match($0, /".*"$/)) + title = "\" title=\"" substr($0, RSTART + 1, RLENGTH - 2); + sub(/[ ]+".*"$/, ""); + url = eschtml($0); + ref[id] = url title; + + subref(id); + next; +} + +!code && /^ *\[\^[^\]]*\]:[ ]+/ { + sub(/^ *\[\^/, ""); + match($0, /\]/); + id = substr($0, 1, RSTART - 1); + sub(id "\\]:[ ]+", ""); + sub(/[ ]+".*"$/, ""); + url = eschtml($0); + fnref[id] = url; + + subref(id); + next; +} + +# html +!html && /^<(address|blockquote|center|dir|div|dl|fieldset|form|h[1-6r]|\ +isindex|menu|noframes|noscript|ol|p|pre|table|ul|!--)/ { + if(code) + oprint(""); + for(; !text && block[nl] == "blockquote"; nl--) + oprint(""); + match($0, /^<(address|blockquote|center|dir|div|dl|fieldset|form|h[1-6r]|\ + isindex|menu|noframes|noscript|ol|p|pre|table|ul|!--)/); + htag = substr($0, 2, RLENGTH - 1); + if(!match($0, "(<\\/" htag ">)|((^
    $)")) + html = 1; + if(html && match($0, /^
    $/ || +(hr && />$/)) { + html = 0; + hr = 0; + oprint($0); + next; +} + +html { + oprint($0); + next; +} + +# List and quote blocks + +# Remove indentation +{ + for(nnl = 0; nnl < nl; nnl++) + if((match(block[nnl + 1], /[ou]l/) && !sub(/^( | )/, "")) || \ + (block[nnl + 1] == "blockquote" && !sub(/^> ?/, ""))) + break; +} +nnl < nl && !blank && text && ! /^ ? ? ?([*+-]|([0-9]+\.)+)( +| )/ { nnl = nl; } +# Quote blocks +{ + while(sub(/^> /, "")) + nblock[++nnl] = "blockquote"; +} +# Horizontal rules +{ hr = 0; } +(blank || (!text && !code)) && /^ ? ? ?([-*_][ ]*)([-*_][ ]*)([-*_][ ]*)+$/ { + if(code){ + oprint(""); + code = 0; + } + blank = 0; + nnl = 0; + hr = 1; +} +# List items +block[nl] ~ /[ou]l/ && /^$/ { + blank = 1; + next; +} +{ newli = 0; } +!hr && (nnl != nl || !text || block[nl] ~ /[ou]l/) && /^ ? ? ?[*+-]( +| )/ { + sub(/^ ? ? ?[*+-]( +| )/, ""); + nnl++; + nblock[nnl] = "ul"; + newli = 1; +} +(nnl != nl || !text || block[nl] ~ /[ou]l/) && /^ ? ? ?([0-9]+\.)+( +| )/ { + sub(/^ ? ? ?([0-9]+\.)+( +| )/, ""); + nnl++; + nblock[nnl] = "ol"; + newli = 1; +} +newli { + if(blank && nnl == nl && !par) + par = "p"; + blank = 0; + printp(par); + if(nnl == nl && block[nl] == nblock[nl]) + oprint("
  • "); +} +blank && ! /^$/ { + if(match(block[nnl], /[ou]l/) && !par) + par = "p"; + printp(par); + par = "p"; + blank = 0; +} + +# Close old blocks and open new ones +nnl != nl || nblock[nl] != block[nl] { + if(code){ + oprint(""); + code = 0; + } + printp(par); + b = (nnl > nl) ? nblock[nnl] : block[nnl]; + par = (match(b, /[ou]l/)) ? "" : "p"; +} +nnl < nl || (nnl == nl && nblock[nl] != block[nl]) { + for(; nl > nnl || (nnl == nl && pblock[nl] != block[nl]); nl--){ + if(match(block[nl], /[ou]l/)) + oprint("
  • "); + oprint(""); + } +} +nnl > nl { + for(; nl < nnl; nl++){ + block[nl + 1] = nblock[nl + 1]; + oprint("<" block[nl + 1] ">"); + if(match(block[nl + 1], /[ou]l/)) + oprint("
  • "); + } +} +hr { + oprint("
    "); + next; +} + +# Code blocks +code && /^$/ { + if(blanK) + oprint(""); + blank = 1; + next; +} +!text && sub(/^( | )/, "") { + if(blanK) + oprint(""); + blank = 0; + if(!code) + oprint("
    ");
    +	code = 1;
    +	$0 = eschtml($0);
    +	oprint($0);
    +	next;
    +}
    +code {
    +	oprint("
    "); + code = 0; +} + +# Setex-style Headers +text && /^=+$/ {printp("h1"); next;} +text && /^-+$/ {printp("h2"); next;} + +# Atx-Style headers +/^#+/ && (!newli || par=="p" || /^##/) { + for(n = 0; n < 6 && sub(/^# */, ""); n++) + sub(/#$/, ""); + par = "h" n; +} + +# Paragraph +/^$/ { + printp(par); + par = "p"; + next; +} + +# Add text +{ text = (text ? text " " : "") $0; } + +function alen(a, ix, k) { + k = 0 + for(ix in a) k++ + return k +} + +END { + if(code){ + oprint(""); + code = 0; + } + printp(par); + for(; nl > 0; nl--){ + if(match(block[nl], /[ou]l/)) + oprint("
  • "); + oprint(""); + } + gsub(/<<[^"]*/, "", otext); + print(otext); + + # Print footnotes + if(alen(fnref)>0) { + print "
      "; + for (i in fnref) print "
    • " i ": " inline(fnref[i]) " ↩︎
    • "; + print "
    "; + } +} diff --git a/spec/do_main_spec.sh b/spec/do_main_spec.sh new file mode 100644 index 0000000..c46c4bb --- /dev/null +++ b/spec/do_main_spec.sh @@ -0,0 +1,13 @@ +# shellcheck shell=bash +Describe 'do_main()' + Include ./lib/date_version_detect.sh + Include ./lib/do_main.sh + Include ./lib/global_variables.sh + Include ./lib/usage.sh + It 'Call function do_main without paramenters' + When call do_main + The line 1 of output should eq "You're not in your blog directory. Moving you there now" + #The line 2 of output should eq "tildelog 0.1" + #The line 3 of output should eq "usage: ./tildelog.sh command [filename]" + End +End diff --git a/spec/spec_helper.sh b/spec/spec_helper.sh new file mode 100644 index 0000000..197e06f --- /dev/null +++ b/spec/spec_helper.sh @@ -0,0 +1,7 @@ +#shellcheck shell=sh + +# set -eu + +# shellspec_spec_helper_configure() { +# shellspec_import 'support/custom_matcher' +# } diff --git a/spec/tildeblog_spec.sh b/spec/tildeblog_spec.sh new file mode 100644 index 0000000..d7664c3 --- /dev/null +++ b/spec/tildeblog_spec.sh @@ -0,0 +1,10 @@ +# shellcheck shell=bash + +Describe 'tildelog.sh' + It 'Call script without paramenters' + When run script ./tildelog.sh + The line 1 of output should eq "You're not in your blog directory. Moving you there now" + The line 2 of output should eq "tildelog 0.1" + The line 3 of output should eq "usage: ./tildelog.sh command [filename]" + End +End \ No newline at end of file diff --git a/spec/usage_spec.sh b/spec/usage_spec.sh new file mode 100644 index 0000000..853bc27 --- /dev/null +++ b/spec/usage_spec.sh @@ -0,0 +1,9 @@ +# shellcheck shell=bash +Describe 'usage()' + Include ./lib/global_variables.sh + Include ./lib/usage.sh + It 'Call function usage' + When call usage + The line 2 of output should include "usage:" + End +End \ No newline at end of file diff --git a/tildelog.sh b/tildelog.sh new file mode 100755 index 0000000..091e168 --- /dev/null +++ b/tildelog.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +# TildeLog, a not-so-simple blog/gemlog/phlog system made for tilde.team +# By Josemar Lohn +# +# Heavily based on BashBlog, by Carlos Fenollosa + +### BEGIN SOURCEFILES -> DO NOT REMOVE THIS LINE + +source ./lib/all_posts.sh +source ./lib/all_tags.sh +source ./lib/create_css.sh +source ./lib/create_html_page.sh +source ./lib/create_includes.sh +source ./lib/date_version_detect.sh +source ./lib/delete_includes.sh +source ./lib/do_main.sh +source ./lib/edit.sh +source ./lib/get_html_file_content.sh +source ./lib/get_post_author.sh +source ./lib/get_post_title.sh +source ./lib/global_variables.sh +source ./lib/is_boilerplate_file.sh +source ./lib/list_posts.sh +source ./lib/list_tags.sh +source ./lib/make_gemini.sh +source ./lib/make_gophermap.sh +source ./lib/make_rss.sh +source ./lib/mrkdwn.sh +source ./lib/parse_file.sh +source ./lib/posts_with_tags.sh +source ./lib/rebuild_all_entries.sh +source ./lib/rebuild_index.sh +source ./lib/rebuild_tags.sh +source ./lib/reset.sh +source ./lib/tags_in_post.sh +source ./lib/test_markdown.sh +source ./lib/twitter_card.sh +source ./lib/usage.sh +source ./lib/write_entry.sh + +### END SOURCEFILES -> DO NOT REMOVE THIS LINE + +# +# MAIN +# Do not change anything here. If you want to modify the code, edit do_main() +# +do_main "$@" + +# vim: set shiftwidth=4 tabstop=4 expandtab: \ No newline at end of file