diff options
| author | Paul Bütow <snonux@users.noreply.github.com> | 2021-05-21 05:12:35 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-05-21 05:12:35 +0100 |
| commit | b2d00167fa2df42808e2c95cba47d28279ce81cd (patch) | |
| tree | 35b44d3a2795f8064e12562d69d226d128f79561 | |
| parent | 7868506c413b453c8c1935f953a0e35a0b4c4ef9 (diff) | |
| parent | 9dbd7f008192fd506bf642944232334fad0ed55c (diff) | |
Merge pull request #1 from snonux/master
Master
| -rw-r--r-- | .gitmodules | 0 | ||||
| -rw-r--r-- | README.md | 110 | ||||
| -rw-r--r-- | buetow.org.conf | 9 | ||||
| -rwxr-xr-x | buetow.org.sh | 107 | ||||
| -rw-r--r-- | footer.html.part | 2 | ||||
| -rw-r--r-- | header.html.part | 50 | ||||
| -rw-r--r-- | packages/assert.source.sh | 59 | ||||
| -rw-r--r-- | packages/atomfeed.source.sh | 128 | ||||
| -rw-r--r-- | packages/gemfeed.source.sh | 54 | ||||
| -rw-r--r-- | packages/generate.source.sh | 160 | ||||
| -rw-r--r-- | packages/git.source.sh | 50 | ||||
| -rw-r--r-- | packages/html.source.sh | 162 | ||||
| -rw-r--r-- | packages/log.source.sh | 30 | ||||
| -rw-r--r-- | packages/md.source.sh | 63 |
14 files changed, 984 insertions, 0 deletions
diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/.gitmodules diff --git a/README.md b/README.md new file mode 100644 index 0000000..e162309 --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +The buetow.org internet site +============================ + +This is the source code of my personal internet site and blog engine. All content is written in Gemini Gemtext format, but the script `buetow.org.sh` generates multiple other static output formats from it. You can reach the site(s)... + +* Via Gemini/Gemtext: [gemini://buetow.org](gemini://buetow.org) (You need a Gemini client for this) +* Via "normal" HTML: [https://buetow.org](https://buetow.org) (Actually it's XHTML Transitional 1.0) +* Via [Gemini Webproxy](https://portal.mozz.us/gemini/buetow.org) +* Via [GitHub Markdown](https://github.com/snonux/buetow.org/blob/content-md/index.md) +* Via [GitHub Page](https://snonux.github.io/buetow.org) (from Markdown) + +Have a look at the `content-*` branches of this buetow.org Git project for source samples. + +## Software I use to maintain this site + +* Text editor: [Vim](https://www.vim.org) +* Gemini server: [a-h/gemini](https://github.com/a-h/gemini) +* Bash +* ShellCheck (if you want to run the tests) +* Web server: [Apache HTTPD](https://httpd.apache.org) (for "normal" HTML site) +* Obviously, I am also using GitHub for source code and GitHub Page hosting. + +# Getting started + +## Requirements + +These are the requirements of the `buetow.org.sh` static site generator script: + +* GNU Bash 5.x or higher +* ShellCheck installed +* GNU Sed +* GNU Date +* Git + +The script was tested on a recent Fedora Linux. For *BSD or macOS you would need to install GNU Sed, GNU Date, and a newer version of Bash. + +## Usage + +So you want such a pretty internet site too? + +To get started, just clone this repo (master branch) and run `./buetow.org.sh`. You will will be prompted with further instructions. + +You will notice soon, that all site content is located in `../buetow.org-content/` (you can configure the `BASE_CONTENT_DIR` in `buetow.org.conf`). There is one sub-directory per output format, e.g.: + +``` +../buetow.org-content/gemtext +../buetow.org-content/html +../buetow.org-content/md +../buetow.org-content/meta +``` + +### Alternative config file path + +If you don't want to mess with `buetow.org.conf`, you can use an alternative config file path in `~/.config/buetow.org.conf`, which takes precedence if it exists. Another way is to set the `CONFIG_FILE_PATH` environment variable, e.g.: + +``` +export CONFIG_FILE_PATH=~/.config/my-site.geek.conf +./buetow.org.sh --generate +``` + +### What is what + +Whereas, you only want to directly edit/add/remove content in the `gemtext` folder. The `buetow.org.sh` then will take the Gemtext and update all other formats accordingly. Summary of what is what: + +* `gemtext`: The Gemini Gemtext markup files of the internet site. +* `html`: The XHTML version of it. +* `md`: The Markdown version of it. +* `meta`: Some meta data of all Gemtext blog posts. It's used by `buetow.org.sh` internally for Atom feed generation. + +### Special HTML configuration + +You will find the `./header.html.part` and `./footer.html.part` files, they are minimal template files for the HTML generation. + +### Special Markdown configuraiton + +`buetow.org.sh` will never touch the `../buetow.org-content/md/_config.yml` file (if it exists). That's a special configuration file for GitHub Pages. + +## Store all formats in Git + +I personally have for each directory in `../buetow.org-content/` a separate Git repository configured. So whenever something has changed it will be updated/added/removed to version control. The following will run the generator and commit everything to Git: + +``` +USE_GIT=yes ./buetow.org --generate +``` + +And the following will additionally perform a `git pull` and `git push` afterwards; + +``` +USE_GIT=yes GIT_PUSH=yes ./buetow.org --generate +``` + +You could add the `USE_GIT` and `GIT_PUSH` options to the `buetow.org.conf` config file too. + +## Publishing a blog post + +All what needs to be done is to create a new file in `./gemtext/gemfeed/YYYY-MM-DD-article-title-dash-separated.gmi`, whereas `YYYY-MM-DD` defines the publishing date of the blog post. + +A subsequent `./buetow.org.sh --generate` will then detect the new post and link it from `$BASE_CONTENT_DIR/gemtext/gemfeed/index.gmi`, link it from the main index `$BASE_CONTENT_DIR/gemtext/index.gmi`, and also add it to the Atom feed at `$BASE_CONTENT_DIR/gemtext/gemfeed/atom.xml`. The first level 1 Gemtext title (e.g. `# Title`) will be the displayed link name. `YYYY-MM-DD` will be the publishing date. There are various other settings, such as Author - they come from the `buetow.org.conf` configuration file. + +Once all of that is done, the `buetow.org.sh` script will convert the new post (plus all the indices and the Atom feed) to the other formats too (e.g. HTML, Markdown). + +You can also have a look at `$BASE_CONTENT_DIR/meta/gemfeed`. There is a meta file for each blog post stored. These meta files are required for the generation of the Atom feed. You can edit these meta files manually and run `./buetow.org.sh --generate` or `./buetow.org.sh --feed` again, in case you want to change some of the Atom feed content. + +## Finito + +After running `./buetow.org --genreate` you will have all static files ready to be published. But before you do that you could preview the content with `firefox ../buetow.org-content/html/index.html` or `glow ../buetow.org-content/md/index.md` (you get the idea). + +Have also a look at the generated `atom.xml` files. They make sense (at least) for Gemtext and HTML. + +Now it is up to you to setup a Gemini server for the Gemtext, a Webserver for the HTML and/or a GitHub page for the Markdowns. diff --git a/buetow.org.conf b/buetow.org.conf new file mode 100644 index 0000000..effeaef --- /dev/null +++ b/buetow.org.conf @@ -0,0 +1,9 @@ +declare -xr DOMAIN=buetow.org +declare -xr SUBTITLE='Having fun with computers!' +declare -xr AUTHOR='Paul Buetow' +declare -xr EMAIL='comments@mx.buetow.org' +declare -xr IMAGE_PATTERN='\.(jpg|png|gif)$' +declare -xr ATOM_MAX_ENTRIES=42 +declare -xr CONTENT_BASE_DIR=../buetow.org-content +declare -xr HTML_HEADER=./header.html.part +declare -xr HTML_FOOTER=./header.html.part diff --git a/buetow.org.sh b/buetow.org.sh new file mode 100755 index 0000000..29eadbd --- /dev/null +++ b/buetow.org.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# +# The buetow.org.sh static site generator +# by Paul Buetow 2021 + +declare -r ARG="$1"; shift +declare DATE=date +declare SED=sed +declare GREP=grep +which gdate &>/dev/null && DATE=gdate +which gsed &>/dev/null && SED=gsed +which ggrep &>/dev/null && GREP=ggrep +readonly DATE +readonly SED +readonly GREP + +set -e + +if [[ -n "$CONFIG_FILE_PATH" ]]; then + source "$CONFIG_FILE_PATH" +elif [[ -f ~/.config/buetow.org.conf ]]; then + source ~/.config/buetow.org.conf +else + source ./buetow.org.conf +fi + +source ./packages/assert.source.sh +source ./packages/git.source.sh +source ./packages/atomfeed.source.sh +source ./packages/gemfeed.source.sh +source ./packages/generate.source.sh +source ./packages/html.source.sh +source ./packages/log.source.sh +source ./packages/md.source.sh + +help () { + cat <<HELPHERE +$0's possible arguments: + --feed Generates Gemtext Atom feed and Gemfeed. + --generate Generates all known output formats (html, md, ...). + If USE_GIT=yes set, all files will be commited to git too. + If GIT_PUSH=yes is set, all content will be pushed to origin. + --test Only runs some shellcheck and unit tests. + --help Prints this retty text. +Example: + USE_GIT=yes GIT_PUSH=yes $0 --generate +HELPHERE +} + +setup () { + if [ ! -d "$CONTENT_BASE_DIR" ]; then + cat <<END +The content base directory, does not exist. Run the following to create it, it +also adds some sample Gemtext content: + + mkdir -p $CONTENT_BASE_DIR/{meta,md,html} + git clone --branch content-gemtext https://github.com/snonux/buetow.org $CONTENT_BASE_DIR/gemtext + rm -Rf $CONTENT_BASE_DIR/gemtext/.git + +Once done, you are ready to edit the files in $CONTENT_BASE_DIR/gemtext. Every +time you want to generate other formats from Gemtext (e.g. HTML, Markdown), run + $0 --generate +again. + +For a list of other available arguments run + $0 --help + +Pro tip: You could make all the directories in $CONTENT_BASE_DIR separate git +repositories or branches. You can then run + USE_GIT=yes $0 --generate +so that all static files are commited to the content repositories too. +END + exit 1 + fi +} + +main () { + local -r arg="$1"; shift + + setup + + case $arg in + --test) + LOG_VERBOSE=yes + assert::shellcheck + html::test + md::test + ;; + --feed) + gemfeed::generate + atomfeed::generate + ;; + --generate) + gemfeed::generate + atomfeed::generate + generate::fromgmi html md + ;; + --help|*) + help + ;; + esac + + return 0 +} + +main "$ARG" +exit $? diff --git a/footer.html.part b/footer.html.part new file mode 100644 index 0000000..308b1d0 --- /dev/null +++ b/footer.html.part @@ -0,0 +1,2 @@ +</body> +</html> diff --git a/header.html.part b/header.html.part new file mode 100644 index 0000000..d499e51 --- /dev/null +++ b/header.html.part @@ -0,0 +1,50 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> +<title>%%TITLE%%</title> +<link rel="shortcut icon" type="image/gif" href="/favicon.ico" /> +<style type="text/css"> +body { + margin: auto; + max-width: 900px; + background-color: #FFFFEF; + border: 1px dashed #880000; + border-radius: 8px; + padding: 5px; +} +img { + display:block; + max-width: 80%; +} +a.textlink:before { + content: " ⇒ "; + padding-left: 2px; +} +a.textlink { + text-decoration: none; + color: #FF0000; +} +a.textlink:hover { + text-decoration: underline; +} +i { + color: #48AAAD; +} +pre { + background-color: #F1F8E9; + border: 1px dashed #BB0000; + border-radius: 8px; + padding: 5px; + font-family: "Lucida Console", "Courier New", monospace; +} +h1 { + text-align: center; + color: #880000; +} +h2, h3 { + color: #BB0000; +} +</style> +</head> +<body> diff --git a/packages/assert.source.sh b/packages/assert.source.sh new file mode 100644 index 0000000..d53a728 --- /dev/null +++ b/packages/assert.source.sh @@ -0,0 +1,59 @@ +# Unit test for whether 2 given strings equal. +assert::equals () { + local -r result="$1"; shift + local -r expected="$1"; shift + local -r callee=${FUNCNAME[1]} + + if [[ "$result" != "$expected" ]]; then + cat <<ERROR | log::pipe ERROR +In $callee expected + '$expected' +But got + '$result' +ERROR + exit 2 + fi + + log VERBOSE "Result in $callee as expected: '$expected'" +} + +# Unit test for whether a given string is not empty. +assert::not_empty () { + local -r name="$1"; shift + local -r content="$1"; shift + local -r callee=${FUNCNAME[1]} + + if [ -z "$content" ]; then + log ERROR "In $callee expected '$name' not to be empty!" + exit 2 + fi + + log VERBOSE "Result in $callee as expected not empty" +} + +# Unit test for whether a given string matches a regex. +assert::matches () { + local -r name="$1"; shift + local -r content="$1"; shift + local -r regex="$1"; shift + local -r callee=${FUNCNAME[1]} + + if ! $GREP -q -E "$regex" <<< "$content"; then + log ERROR "In $callee expected '$name' to match '$regex'" + exit 2 + fi + + log VERBOSE "Matching in $callee as expected" +} + +# Checks if all the Bash scripts here are good. +assert::shellcheck () { + set -e + shellcheck \ + --norc \ + --external-sources \ + --check-sourced \ + --exclude=SC2155,SC2010,SC2154,SC1090,SC2012 \ + ./"$0" + set +e +} diff --git a/packages/atomfeed.source.sh b/packages/atomfeed.source.sh new file mode 100644 index 0000000..e1ee406 --- /dev/null +++ b/packages/atomfeed.source.sh @@ -0,0 +1,128 @@ +# Retrieve meta data of a given blog post. Generate new meta info if not yet exists. +atomfeed::meta () { + local -r gmi_file_path="$1"; shift + local -r meta_file=$($SED 's|gemtext|meta|; s|.gmi$|.meta|;' <<< "$gmi_file_path") + + log VERBOSE "Generating meta info for post $gmi_file_path" + + local is_draft=no + if $GREP -E -q '\.draft\.meta$' <<< "$meta_file"; then + is_draft=yes + fi + + local -r meta_dir=$(dirname "$meta_file") + if [[ ! -d "$meta_dir" ]]; then + mkdir -p "$meta_dir" + fi + + if [ ! -f "$meta_file" ]; then + # Extract first heading as post title. + local title=$($SED -n '/^# / { s/# //; p; q; }' "$gmi_file_path" | tr '"' "'") + # Extract first paragraph from Gemtext + local summary=$($SED -n '/^[A-Z]/ { p; q; }' "$gmi_file_path" | tr '"' "'") + # Extract the date from the file name. + local filename_date=$(basename "$gmi_file_path" | cut -d- -f1,2,3) + local date=$($DATE --iso-8601=seconds --date "$filename_date $($DATE +%H:%M:%S)") + + cat <<META | tee "$meta_file" +local meta_date="$date" +local meta_author="$AUTHOR" +local meta_email="$EMAIL" +local meta_title="$title" +local meta_summary="$summary. .....to read on please visit my site." +META + if [[ $is_draft == no ]]; then + git::add meta "$meta_file" + fi + return + fi + + cat "$meta_file" + if [[ $is_draft == yes ]]; then + rm "$meta_file" + fi +} + +# Retrieve the core content as XHTML of the blog post. +atomfeed::content () { + local -r gmi_file_path="$1"; shift + log VERBOSE "Retrieving feed content from $gmi_file_path" + + # sed: Remove all before the first header + # sed: Make HTML links absolute, Atom relative URLs feature seems a mess + # across different Atom clients. + html::fromgmi < <($SED '/Go back to the main site/d' "$gmi_file_path") | + $SED " + s|href=\"\./|href=\"https://$DOMAIN/gemfeed/|g; + s|src=\"\./|src=\"https://$DOMAIN/gemfeed/|g; + " +} + +# Generate an atom.xml feed file. +atomfeed::generate () { + local -r gemfeed_dir="$CONTENT_BASE_DIR/gemtext/gemfeed" + local -r atom_file="$gemfeed_dir/atom.xml" + local -r now=$($DATE --iso-8601=seconds) + log INFO "Generating Atom feed to $atom_file" + + assert::not_empty now "$now" + + cat <<ATOMHEADER > "$atom_file.tmp" +<?xml version="1.0" encoding="utf-8"?> +<feed xmlns="http://www.w3.org/2005/Atom"> + <updated>$now</updated> + <title>$DOMAIN feed</title> + <subtitle>$SUBTITLE</subtitle> + <link href="gemini://$DOMAIN/gemfeed/atom.xml" rel="self" /> + <link href="gemini://$DOMAIN/" /> + <id>gemini://$DOMAIN/</id> +ATOMHEADER + + while read -r gmi_file; do + # Load cached meta information about the post. + source <(atomfeed::meta "$gemfeed_dir/$gmi_file") + + # Get HTML content for the feed + local content="$(atomfeed::content "$gemfeed_dir/$gmi_file")" + + assert::not_empty meta_title "$meta_title" + assert::not_empty meta_date "$meta_date" + assert::not_empty meta_author "$meta_author" + assert::not_empty meta_email "$meta_email" + assert::not_empty meta_summary "$meta_summary" + assert::not_empty content "$content" + + cat <<ATOMENTRY >> "$atom_file.tmp" + <entry> + <title>$meta_title</title> + <link href="gemini://$DOMAIN/gemfeed/$gmi_file" /> + <id>gemini://$DOMAIN/gemfeed/$gmi_file</id> + <updated>$meta_date</updated> + <author> + <name>$meta_author</name> + <email>$meta_email</email> + </author> + <summary>$meta_summary</summary> + <content type="xhtml"> + <div xmlns="http://www.w3.org/1999/xhtml"> + $content + </div> + </content> + </entry> +ATOMENTRY + done < <(gemfeed::get_posts | head -n $ATOM_MAX_ENTRIES) + + cat <<ATOMFOOTER >> "$atom_file.tmp" +</feed> +ATOMFOOTER + + # Delete the 3rd line of the atom feeds (global feed update timestamp) + if ! diff -u <($SED 3d "$atom_file") <($SED 3d "$atom_file.tmp"); then + log INFO 'Feed got something new!' + mv "$atom_file.tmp" "$atom_file" + git::add gemtext "$atom_file" + else + log INFO 'Nothing really new in the feed' + rm "$atom_file.tmp" + fi +} diff --git a/packages/gemfeed.source.sh b/packages/gemfeed.source.sh new file mode 100644 index 0000000..c842bb1 --- /dev/null +++ b/packages/gemfeed.source.sh @@ -0,0 +1,54 @@ +# Filter out blog posts from other files in the gemfeed dir. +gemfeed::get_posts () { + local -r gemfeed_dir="$CONTENT_BASE_DIR/gemtext/gemfeed" + local -r gmi_pattern='^[0-9]{4}-[0-9]{2}-[0-9]{2}-.*\.gmi$' + local -r draft_pattern='\.draft\.gmi$' + + ls "$gemfeed_dir" | + $GREP -E "$gmi_pattern" | + $GREP -E -v "$draft_pattern" | + sort -r +} + +# Add the links from gemfeed/index.gmi to the main index site. +gemfeed::updatemainindex () { + local -r index_gmi="$CONTENT_BASE_DIR/gemtext/index.gmi" + local -r gemfeed_dir="$CONTENT_BASE_DIR/gemtext/gemfeed" + + log VERBOSE "Updating $index_gmi with posts from $gemfeed_dir" + + # Remove old gemfeeds from main index + $SED '/^=> .\/gemfeed\/[0-9].* - .*/d;' "$index_gmi" > "$index_gmi.tmp" + # Add current gemfeeds to main index + $SED -n '/^=> / { s| ./| ./gemfeed/|; p; }' "$gemfeed_dir/index.gmi" >> "$index_gmi.tmp" + + mv "$index_gmi.tmp" "$index_gmi" + git::add gemtext "$index_gmi" +} + +# Generate a index.gmi in the ./gemfeed subdir. +gemfeed::generate () { + local -r gemfeed_dir="$CONTENT_BASE_DIR/gemtext/gemfeed" + log INFO "Generating Gemfeed index for $gemfeed_dir" + +cat <<GEMFEED > "$gemfeed_dir/index.gmi.tmp" +# $DOMAIN's Gemfeed + +## $SUBTITLE + +GEMFEED + + gemfeed::get_posts | while read -r gmi_file; do + # Extract first heading as post title. + local title=$($SED -n '/^# / { s/# //; p; q; }' "$gemfeed_dir/$gmi_file" | tr '"' "'") + # Extract the date from the file name. + local filename_date=$(basename "$gemfeed_dir/$gmi_file" | cut -d- -f1,2,3) + + echo "=> ./$gmi_file $filename_date - $title" >> "$gemfeed_dir/index.gmi.tmp" + done + + mv "$gemfeed_dir/index.gmi.tmp" "$gemfeed_dir/index.gmi" + git::add gemtext "$gemfeed_dir/index.gmi" + + gemfeed::updatemainindex +} diff --git a/packages/generate.source.sh b/packages/generate.source.sh new file mode 100644 index 0000000..98cd215 --- /dev/null +++ b/packages/generate.source.sh @@ -0,0 +1,160 @@ +# Generate a HTML or Markdown link from given Gemtext link. +generate::make_link () { + local -r what="$1"; shift + local -r line="${1/=> }"; shift + local link + local descr + + while read -r token; do + if [ -z "$link" ]; then + link="$token" + elif [ -z "$descr" ]; then + descr="$token" + else + descr="$descr $token" + fi + done < <(echo "$line" | tr ' ' '\n') + + if $GREP -E -q "$IMAGE_PATTERN" <<< "$link"; then + if [[ "$what" == md ]]; then + md::make_img "$link" "$descr" + else + html::make_img "$link" "$(html::encode "$descr")" + fi + return + fi + + if [[ "$what" == md ]]; then + md::make_link "$link" "$descr" + else + html::make_link "$link" "$(html::encode "$descr")" + fi +} + +# Add other docs (e.g. images, videos) from Gemtext to output format. +generate::fromgmi_add_docs () { + local -r src="$1"; shift + local -r format="$1"; shift + local -r dest=${src/gemtext/$format} + local -r dest_dir=$(dirname "$dest") + + if [[ ! -d "$dest_dir" ]]; then + mkdir -p "$dest_dir" + fi + cp "$src" "$dest" + git::add "$format" "$dest" +} + +# Remove docs from output format which aren't present in Gemtext anymore. +generate::fromgmi_cleanup_docs () { + local -r src="$1"; shift + local -r format="$1"; shift + local dest=${src/.$format/.gmi} + dest=${dest/$format/gemtext} + + if [[ ! -f "$dest" ]]; then + git::rm "$format" "$src" + fi +} + +# Convert the Gemtext Atom feed to a HTML Atom feed. +generate::convert_gmi_atom_to_html_atom () { + local -r format="$1"; shift + if [[ "$format" != html ]]; then + return + fi + + log INFO 'Converting Gemtext Atom feed to HTML Atom feed' + + $SED 's|.gmi|.html|g; s|gemini://|https://|g' \ + < $CONTENT_BASE_DIR/gemtext/gemfeed/atom.xml \ + > $CONTENT_BASE_DIR/html/gemfeed/atom.xml + + git::add "$format" "$CONTENT_BASE_DIR/html/gemfeed/atom.xml" +} + +# Internal helper function for generate::fromgmi +generate::_fromgmi () { + local -r src="$1"; shift + local -r format="$1"; shift + local dest=${src/gemtext/$format} + dest=${dest/.gmi/.$format} + local dest_dir=$(dirname "$dest") + + if [[ ! -d "$dest_dir" ]]; then + mkdir -p "$dest_dir" + fi + + if [[ "$format" == html ]]; then + cat "$HTML_HEADER" > "$dest.tmp" + html::fromgmi < "$src" >> "$dest.tmp" + cat "$HTML_FOOTER" >> "$dest.tmp" + + elif [[ "$format" == md ]]; then + md::fromgmi < "$src" >> "$dest.tmp" + fi + + local title=$($SED -n '/^# / { s/# //; p; q; }' "$src" | tr '"' "'") + if [[ -z "$title" ]]; then + title=$SUBTITLE + fi + $SED -i "s|%%TITLE%%|$title|g" "$dest.tmp" + mv "$dest.tmp" "$dest" + + git::add "$format" "$dest" +} + +# Generate a given output format from a Gemtext file. +generate::fromgmi () { + local -i num_gmi_files=0 + local -i num_doc_files=0 + + log INFO "Generating $* from Gemtext" + + while read -r src; do + num_gmi_files=$(( num_gmi_files + 1 )) + for format in "$@"; do + generate::_fromgmi "$src" "$format" + done + done < <(find "$CONTENT_BASE_DIR/gemtext" -type f -name \*.gmi) + + log INFO "Converted $num_gmi_files Gemtext files" + + # Add non-.gmi files to html dir. + log VERBOSE "Adding other docs to $*" + + while read -r src; do + num_doc_files=$(( num_doc_files + 1 )) + for format in "$@"; do + generate::fromgmi_add_docs "$src" "$format" + done + done < <(find "$CONTENT_BASE_DIR/gemtext" -type f | $GREP -E -v '(\.git.*|\.gmi|atom.xml|\.tmp)$') + + log INFO "Added $num_doc_files other documents to each of $*" + + # Add atom feed for HTML + for format in "$@"; do + generate::convert_gmi_atom_to_html_atom "$format" + done + + # Remove obsolete files from ./html/. + # Note: The _config.yml is the config file for GitHub pages (md format). + for format in "$@"; do + find "$CONTENT_BASE_DIR/$format" -type f | + $GREP -E -v '(\.git.*|_config.yml)$'| + while read -r src; do + generate::fromgmi_cleanup_docs "$src" "$format" + done + done + + if [[ -z "$GIT_COMMIT_MESSAGE" ]]; then + GIT_COMMIT_MESSAGE='Publishing new version' + fi + git::commit gemtext "$GIT_COMMIT_MESSAGE" + git::commit meta "$GIT_COMMIT_MESSAGE" + + for format in "$@"; do + git::commit "$format" "$GIT_COMMIT_MESSAGE" + log INFO "$format can be found in $CONTENT_BASE_DIR/$format now" + done +} diff --git a/packages/git.source.sh b/packages/git.source.sh new file mode 100644 index 0000000..c502b00 --- /dev/null +++ b/packages/git.source.sh @@ -0,0 +1,50 @@ +# Add a static content file to git +git::add () { + if [[ "$USE_GIT" != yes ]]; then + return + fi + + local -r content_dir="$CONTENT_BASE_DIR/$1"; shift + local file="$1"; shift + file=${file/$content_dir/.\/} + + cd "$content_dir" &>/dev/null + git add "$file" + cd - &>/dev/null +} + +# Remove a static content file from git +git::rm () { + if [[ "$USE_GIT" != yes ]]; then + return + fi + + local -r content_dir="$CONTENT_BASE_DIR/$1"; shift + local file="$1"; shift + file=${file/$content_dir/.\/} + + cd "$content_dir" &>/dev/null + git rm "$file" + cd - &>/dev/null +} + +# Commit all changes +git::commit () { + if [[ "$USE_GIT" != yes ]]; then + return + fi + + local -r content_dir="$CONTENT_BASE_DIR/$1"; shift + local -r message="$1"; shift + + cd "$content_dir" &>/dev/null + set +e + git commit -a -m "$message" + if [[ "$GIT_PUSH" == yes ]]; then + log INFO "Invoking git pull/push in $content_dir" + git pull + git push + fi + set -e + cd - &>/dev/null +} diff --git a/packages/html.source.sh b/packages/html.source.sh new file mode 100644 index 0000000..6049c2f --- /dev/null +++ b/packages/html.source.sh @@ -0,0 +1,162 @@ +# Convert special characters to their HTML codes +html::encode () { + $SED ' + s|\&|\&|g; + s|<|\<|g; + s|>|\>|g; + ' <<< "$@" +} + +# Make a HTML paragraph. +html::make_paragraph () { + local -r text="$1"; shift + + if [[ -n "$text" ]]; then + echo "<p>$(html::encode "$text")</p>" + fi +} + +# Make a HTML header. +html::make_heading () { + local -r text=$($SED -E 's/^#+ //' <<< "$1"); shift + local -r level="$1"; shift + echo "<h${level}>$(html::encode "$text")</h${level}>" +} + +# Make a HTML quotation +html::make_quote () { + local -r quote="${1/> }" + echo "<p class=\"quote\"><i>$(html::encode "$quote")</i></p>" +} + +# Make a HTML image +html::make_img () { + local link="$1"; shift + local descr="$1"; shift + + if [ -z "$descr" ]; then + echo -n "<a href=\"$link\"><img src=\"$link\" /></a>" + else + echo -n "<i>$descr:</i>" + echo -n "<a href=\"$link\"><img alt=\"$descr\" title=\"$descr\" src=\"$link\" /></a>" + fi + + echo "<br />" +} + +# Make a HTML hyperlink +html::make_link () { + local link="$1"; shift + local descr="$1"; shift + + if ! $GREP -F -q '://' <<< "$link"; then + link=${link/.gmi/.html} + fi + + if [[ -z "$descr" ]]; then + descr="$link" + fi + + echo "<a class=\"textlink\" href=\"$link\">$descr</a><br />" +} + +# Convert Gemtext to HTML +html::fromgmi () { + local is_list=no + local is_plain=no + + while IFS='' read -r line; do + if [[ "$is_list" == yes ]]; then + if [[ "$line" == '* '* ]]; then + echo "<li>$(html::encode "${line/\* /}")</li>" + else + is_list=no + echo "</ul>" + fi + continue + + elif [[ "$is_plain" == yes ]]; then + if [[ "$line" == '```'* ]]; then + echo "</pre>" + is_plain=no + else + html::encode "$line" + fi + continue + fi + + case "$line" in + '* '*) + is_list=yes + echo "<ul>" + echo "<li>${line/\* /}</li>" + ;; + '```'*) + is_plain=yes + echo "<pre>" + ;; + '# '*) + html::make_heading "$line" 1 + ;; + '## '*) + html::make_heading "$line" 2 + ;; + '### '*) + html::make_heading "$line" 3 + ;; + '> '*) + html::make_quote "$line" + ;; + '=> '*) + generate::make_link html "$line" + ;; + *) + html::make_paragraph "$line" + ;; + esac + done +} + +# Test HTML package. +html::test () { + local line='Hello world! This is a paragraph.' + assert::equals "$(html::make_paragraph "$line")" '<p>Hello world! This is a paragraph.</p>' + + line='' + assert::equals "$(html::make_paragraph "$line")" '' + + line='Foo &<>& Bar!' + assert::equals "$(html::make_paragraph "$line")" '<p>Foo &<>& Bar!</p>' + + line='# Header 1' + assert::equals "$(html::make_heading "$line" 1)" '<h1>Header 1</h1>' + + line='## Header 2' + assert::equals "$(html::make_heading "$line" 2)" '<h2>Header 2</h2>' + + line='### Header 3' + assert::equals "$(html::make_heading "$line" 3)" '<h3>Header 3</h3>' + + line='> This is a quote' + assert::equals "$(html::make_quote "$line")" '<p class="quote"><i>This is a quote</i></p>' + + line='=> https://example.org' + assert::equals "$(generate::make_link html "$line")" \ + '<a class="textlink" href="https://example.org">https://example.org</a><br />' + + line='=> index.html' + assert::equals "$(generate::make_link html "$line")" \ + '<a class="textlink" href="index.html">index.html</a><br />' + + line='=> http://example.org Description of the link' + assert::equals "$(generate::make_link html "$line")" \ + '<a class="textlink" href="http://example.org">Description of the link</a><br />' + + line='=> http://example.org/image.png' + assert::equals "$(generate::make_link html "$line")" \ + '<a href="http://example.org/image.png"><img src="http://example.org/image.png" /></a><br />' + + line='=> http://example.org/image.png Image description' + assert::equals "$(generate::make_link html "$line")" \ + '<i>Image description:</i><a href="http://example.org/image.png"><img alt="Image description" title="Image description" src="http://example.org/image.png" /></a><br />' +} diff --git a/packages/log.source.sh b/packages/log.source.sh new file mode 100644 index 0000000..56c6587 --- /dev/null +++ b/packages/log.source.sh @@ -0,0 +1,30 @@ +# Log a message. +log () { + local -r level="$1"; shift + local message + + for message in "$@"; do + echo "$message" + done | log::_pipe "$level" +} + +# Log a stream through a pipe. +log::pipe () { + log::_pipe "$1" +} + +# Internal log implementation. +log::_pipe () { + local -r level="$1"; shift + + if [[ "$level" == VERBOSE && -z "$LOG_VERBOSE" ]]; then + return + fi + + local -r callee=${FUNCNAME[2]} + local -r stamp=$($DATE +%Y%m%d-%H%M%S) + + while read -r line; do + echo "$level|$stamp|$callee|$line" >&2 + done +} diff --git a/packages/md.source.sh b/packages/md.source.sh new file mode 100644 index 0000000..e7bfae1 --- /dev/null +++ b/packages/md.source.sh @@ -0,0 +1,63 @@ +# Make a Markdown image. +md::make_img () { + local link="$1"; shift + local descr="$1"; shift + + if [ -z "$descr" ]; then + echo "[]($link) " + else + echo "[]($link) " + fi +} + +# Make a Markdown hyperlink. +md::make_link () { + local link="$1"; shift + local descr="$1"; shift + + if ! $GREP -F -q '://' <<< "$link"; then + link=${link/.gmi/.md} + fi + if [[ -z "$descr" ]]; then + descr="$link" + fi + + echo "[$descr]($link) " +} + +# Convert Gemtext to Markdown. +md::fromgmi () { + while IFS='' read -r line; do + case "$line" in + '=> '*) + generate::make_link md "$line" + ;; + *) + echo "$line" + ;; + esac + done +} + +# Test the Markdown package. +md::test () { + local line='=> https://example.org' + assert::equals "$(generate::make_link md "$line")" \ + '[https://example.org](https://example.org) ' + + line='=> index.md' + assert::equals "$(generate::make_link md "$line")" \ + '[index.md](index.md) ' + + line='=> http://example.org Description of the link' + assert::equals "$(generate::make_link md "$line")" \ + '[Description of the link](http://example.org) ' + + line='=> http://example.org/image.png' + assert::equals "$(generate::make_link md "$line")" \ + '[](http://example.org/image.png) ' + + line='=> http://example.org/image.png Image description' + assert::equals "$(generate::make_link md "$line")" \ + '[](http://example.org/image.png) ' +} |
