From 03bbc3816d245ad8768597e0ec9a16c265bf541c Mon Sep 17 00:00:00 2001 From: Andrew Stryker Date: Thu, 14 Sep 2023 10:39:45 -0700 Subject: [PATCH] Add initial files --- create-index-entry.awk | 122 ++++++++++++++++++++++ curate.mk | 115 +++++++++++++++++++++ environment.mk | 125 +++++++++++++++++++++++ fence.m4 | 96 ++++++++++++++++++ index.mk | 225 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 683 insertions(+) create mode 100755 create-index-entry.awk create mode 100644 curate.mk create mode 100644 environment.mk create mode 100644 fence.m4 create mode 100644 index.mk diff --git a/create-index-entry.awk b/create-index-entry.awk new file mode 100755 index 0000000..4705f55 --- /dev/null +++ b/create-index-entry.awk @@ -0,0 +1,122 @@ +#! /usr/bin/awk -f +#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-# +# create-post-entry.awk +# +# Create an index entry from a Gemtext file +# +# This program transforms information from a gemtext post into an index entry. +# The index entry needs to be a valid Gemini hyperlink. The hyperlink notation +# contains the following elements: +# +# * Hyperlink line marker. That is, the line must start with "=> ". +# * URL as the second element. +# * Desription as a optional third element. +# +# The program: +# +# * Forms the URL from the filename of the file that it is processing. Thus, +# it will not work correctly with text from standard input. +# +# * Constructs the desciption from the +# * Published date +# * Title +# * Revised date (if present) +# +# The AWK program expects the Gemtext file to: +# +# 1. Contain the title on the first line, without the heading level prefix. +# +# 2. Record the published date on a line that begins with "Published: " and +# then has an ISO formatted date, e.g., 2023-05-26. +# +# 3. Record revision history on lines that begin with "Revised: " and then +# have ISO formatted date. These are optional. The program takes the last +# revision date. +# +# The program will write a warning to standard error when there are empty +# values for the title or published date. If the program cannot determine the +# file name, then it exits with error code 1. +# +# +# USAGE +# +# awk -f create-post-entry.awk gemtext-file +# +# AUTHOR +# +# © Andrew Stryker +#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-# + +BEGIN { + # Include colon and comma for record splitting + FS = "[:,[:space:]]+" + + # Declare the variables that we will use and set them to empty + # strings. Note, AWK evaluates the empty string as false. + title = "" + published = "" + revised = "" + tags["---"] = 1 +} + +FNR == 1 { + # if we cannot get a file name, then we cannot create a valid URL + # and need to fail + assert(FILENAME != "-", "Could not determine the filename") + + title = gensub(/^#+ */, "", 1, $0) # remove heading marker, if present + if (!title) { + print "Missing title on the first line" > "/dev/stderr" + } + + next +} + +/^Published: +[[:digit:]]{4}(-[[:digit:]]{2}){2}/ { + published = $2 + next +} + +/^Revised: +[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2}/ { + revised = sprintf(", revised on %s", $2) + next +} + +# Create a set of tags +/^Tags:/ { + for (i = 2; i <= NF; ++i) { + tags[$i] = 1 + } + next +} + +END { + if (_assert_exit) { + exit 1 + } + + if (!published) { + printf("Missing published date in %s\n", FILENAME) \ + > "/dev/stderr" + } + + for (t in tags) { + printf("%s\t=> %s %s -- %s%s\n", t, FILENAME, published, + title, revised) + } +} + +# assert --- assert that a condition is true. Otherwise, exit. +# adapted from: +# https://www.gnu.org/software/gawk/manual/gawk.html#Assert-Function + +function assert(condition, string) { + if (! condition) { + printf("%s:%d: assertion failed: %s\n", + FILENAME, FNR, string) > "/dev/stderr" + _assert_exit = 1 + exit 1 + } +} + +#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-# diff --git a/curate.mk b/curate.mk new file mode 100644 index 0000000..7c1fe75 --- /dev/null +++ b/curate.mk @@ -0,0 +1,115 @@ +#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-# +#> content.makefile +# +#> Build +#> +#> This Makefile handles creating, building, and publishing posts for a Gemini +#> site. +#> +# The key challenge here is creating the index files. +# 1. Create a file with index entries +# 2. Extract a list of tags to file +# 3. +# +# © 2023 Andrew Stryker +#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-# + +#-----------------------------------------------------------------------------# +# +# Configuration +# +#-----------------------------------------------------------------------------# + +# Record the name of this Makefile +self ::= $(lastword ${MAKEFILE_LIST}) + +# Load environment variables if needed--not the case when this is called from +# the main Makefile +ifndef ENV_LOADED + + # Assume the base directory is two levels up if not defined + MAKO_DIR ?= ../.. + + include ${MAKO_DIR}/environment.mk + +endif + +# Use the current directory as the content section name +content_section ::= $(shell basename ${CURDIR}) +staging_dir ::= ${STAGING}/${content_section} + +# Gather file lists +templates ::= $(wildcard *.gmi.m4) +templates_expanded ::= $(addprefix ${staging_dir}/,${templates:.gmi.m4=.gmi}) + +gemtext ::= $(wildcard *.gmi) +gemtext_copied ::= $(addprefix ${staging_dir}/,${gemtext}) + +all ::= $(notdir $(filter-out _%,%.m4,%~,.%,$(wildcard *))) + +#-----------------------------------------------------------------------------# +# +# User interface +# +#> This makefile supports the following targets: +#> +#-----------------------------------------------------------------------------# + +.PHONY: default build clean show help create + +# Define the default target explicitly +default: create + +create: #> Create a new post (default) + @if [ -z $${EDITOR} ]; then \ + python3 create-post.py; \ + else \ + python3 create-post.py --edit; \ + fi + +build: ${gemtext_copied} ${templates_expanded} + @echo "✓ Completed processing ${content_section}" + @echo + + +show: #> Show enironment variables with values + @echo "Key variables defined in ${self}:" + @echo + @echo "Makefile list: ${MAKEFILE_LIST}" + @echo + @echo "Content section............................... ${content_section}" + @echo "Staging space................................. ${staging_dir}" + @echo + @echo "Templates found:" + @for x in ${templates}; do echo "\t$$x"; done + @echo + @echo "Gemtext files found:" + @for x in ${gemtext}; do echo "\t\t$$x"; done + @echo + +clean: #> Delete generated files + @rm -rf ${staging_dir} + @echo "✓ Deleted ${staging_dir} and everything in it" + +help: #> Display this help message + @awk -f ${AWKHELP} ${self} + +#-----------------------------------------------------------------------------# +# +# File system interface +# +#-----------------------------------------------------------------------------# + +${staging_dir}: + @mkdir -p $@ + @echo "\t✓ Created staging space: $@" + +${templates_expanded}: ${staging_dir}/%: %.m4 ${staging_dir} ${FENCE} ${all} + @m4 --include=${MAKO_DIR} $< > $@ + @echo "\t✓ Generated $@" + +${gemtext_copied}: ${staging_dir}/%: % ${staging_dir} + @cat $< > $@ + @echo "\t✓ Copied $@" + +#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-# diff --git a/environment.mk b/environment.mk new file mode 100644 index 0000000..0af083c --- /dev/null +++ b/environment.mk @@ -0,0 +1,125 @@ +#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-# +#> environment.mk +#> +#> Define global environment variables +#> +#> This file is typically included from the main Makefile rather called +#> directly. Its purpose is to keep the responsbility of the main Mainfile +#> compact. The responsbility of this Makefile is to define global +#> environment variables, some of which it read from a user created file. +#> +# +# © 2023 Andrew Stryker +# +#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-# + +# Track file name of caller +ifdef self + caller ::= self +endif + +self ::= $(notdir $(lastword ${MAKEFILE_LIST})) + +#-----------------------------------------------------------------------------# +# +# Configuration +# +#-----------------------------------------------------------------------------# + +ifndef ENV_LOADED + +# Track loading the environment +ENV_LOADED ::= 1 +export ENV_LOADED + +# Avoid an unexpected shell environment +SHELL = /bin/sh +export SHELL + +# Base/root directory of the build system. Allows us to use absolute paths. +MAKO_DIR ?= ${CURDIR} +export MAKO_DIR + +# Place for user content +CONTENT ?= ${MAKO_DIR}/content +CONTENT ::= $(strip ${CONTENT}) +export CONTENT + +# User-defined configuration file +site_env ?= ${CONTENT}/site-env +site_env ::= $(strip ${site_env}) + +ifeq ($(strip $(shell [ -r ${site_env} ] && echo ${site_env})),) + $(info Generate a site configuration file first via `make configure`) + $(error Configuration file ${site_env} not readable.) +else + include ${site_env} +endif + +# Place for intermediate files +WORKING ?= ${MAKO_DIR}/workspace +WORKING ::= $(strip ${WORKING}) +export WORKING + +# Place for site files ready to be transferred to the site +STAGING ?= ${MAKO_DIR}/staging +STAGING ::= $(strip ${STAGING}) +export STAGING + +# Makefiles +CURATE_MAKE ::= ${MAKO_DIR}/curate.mk +INDEX_MAKE ::= ${MAKO_DIR}/index.mk + +# M4 fencing for raw text +FENCE ::= ${MAKO_DIR}/fence.m4 +export FENCE + +# Help generation +AWKHELP ::= ${MAKO_DIR}/generate-help.awk +export AWKHELP + +endif + +#-----------------------------------------------------------------------------# +# +# User interface targets +# +#> User facing targets: +#> +#-----------------------------------------------------------------------------# + +# Only define targets if called directly +ifeq ($(firstword ${MAKEFILE_LIST}), ${self}) + +.PHONY: default show help + +default: show + +show: #> Show key variables + @echo "Key variables defined in ${self}:" + @echo + @echo "\tBase/root directory of the build system..... ${MAKO_DIR}" + @echo "\tUsef-defined configuration.................. ${site_env}" + @echo + # future location for templates + @echo "\tLocation of user content.................... ${CONTENT}" + @echo "\tWorking area for intermediate files......... ${WORKING}" + @echo "\tStaging area for site....................... ${STAGING}" + @echo + @echo "\tM4 macro for raw text....................... ${FENCE}" + @echo "\tHelp generation AWK file.................... ${AWKHELP}" + +help: #> Show this help message + @awk -f ${AWKHELP} ${self} + +endif + +#-----------------------------------------------------------------------------# + +# Restore value of self to the caller's file name if possible +ifdef caller + self ::= ${caller} +endif + +#> +#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-# diff --git a/fence.m4 b/fence.m4 new file mode 100644 index 0000000..838874f --- /dev/null +++ b/fence.m4 @@ -0,0 +1,96 @@ +dnl ---------------------------------------------------------------------------# +dnl fence.m4 +dnl +dnl Fence pre-formated text or code into Markdown and Gemtext documents +dnl +dnl Both Markdown extensions and Gemini use backticks to fence 'raw' text +dnl blocks. m4 uses backticks to quote text. This file simplifies managing +dnl text that would otherwise be subject to macro quoting rules. +dnl +dnl This file provides four macros to resolve this conflict: +dnl +dnl * DELIMITER -- insert three backticks delimiter +dnl * FENCE -- wraps its argument inside a backtick fence +dnl * CODE -- wraps its arguement with backticks for inline code +dnl * CODEBLOCK -- forms a code block where the first argument is placed +dnl after the leading backticks and the second argument is +dnl the code +dnl +dnl In some circumstances, changing m4's quote characters will make for +dnl a better solution. +dnl +dnl Note, the implementation uses "-<-<" and ">->-" as temporary m4 quote +dnl characters. +dnl +dnl Examples: +dnl +dnl FENCE(foo) => +dnl ``` +dnl foo +dnl ``` +dnl +dnl CODEBLOCK(sh, `echo foo') => +dnl ```sh +dnl echo foo +dnl ``` +dnl +dnl CODE(`x = 3') => +dnl `x = 3` +dnl '' +dnl +dnl Use in combination `undivert' to include code files: +dnl +dnl CODEBLOCK(c, `undivert(`hellow-world.c')') +dnl +dnl Use in combination with `syscmd`: +dnl +dnl FENCE(`syscmd(`ls')') +dnl +dnl +dnl Usage: +dnl +dnl Use the 'include()' macro to import this macros into your m4 file. +dnl +dnl Author: axs@sdf.org +dnl +dnl ---------------------------------------------------------------------------# +dnl +dnl Write backticks, handling the conflict with m4 quoting +dnl +dnl Following advice from Michael Breen, we: +dnl * Redefine the quote characters +dnl * Write the backticks +dnl * Surpress the apostrophies +dnl * Revert the quote characters to the orginal state +dnl +dnl see: https://mbreen.com/m4.html +dnl +define(`DELIMITER', `changequote(`-<-<',`>->-')```dnl''' +changequote`'')dnl +dnl +dnl ---------------------------------------------------------------------------# +dnl +dnl Create a pre-formatted text block +dnl +define(`FENCE', `dnl +DELIMITER() +$1`'dnl +DELIMITER() +')dnl +dnl ---------------------------------------------------------------------------# +dnl +dnl Create a code block +dnl +define(`CODEBLOCK', `dnl +DELIMITER()$1 +$2 +DELIMITER() +')dnl +dnl ---------------------------------------------------------------------------# +dnl +dnl Inline code +dnl +define(`CODE', `changequote(`-<-<',`>->-')`$1`dnl'' +changequote`'')dnl +dnl +dnl ---------------------------------------------------------------------------# diff --git a/index.mk b/index.mk new file mode 100644 index 0000000..7051778 --- /dev/null +++ b/index.mk @@ -0,0 +1,225 @@ +#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-# +# index.mk +# +#> Index Gemini site content +#> +#> This Makefile builds content indicies for a Gemini site. +#> +# +# Allowing posts to be m4 files introduces complexity without clear benefit. +# Thus we are +# +# Strategy: +# 1. Mark all *.gmi files as posts. +# 2. Limit macros to: +# a. Section index +# b. Tagged index +# c. Header +# d. Footer +# 4. Create index entries for each *.gmi file. +# 5. Build the full list of tags. +# 6. Generate the post as `cat footer.gmi post.gmi footer.gmi`. +# 7. Build the tagged index files from the list of tags. +# 8. Build index.gmi from tag list. +# +# The key challenge is step 7. We do not know how to build this until we +# completed step 5. Two possible approaches: +# 1. Create a macro +# 2. Recurse into another Makefile. +# +# © 2023 Andrew Stryker +#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-# + +#-----------------------------------------------------------------------------# +# +# Configuration +# +#-----------------------------------------------------------------------------# + +# Record the name of this Makefile +self ::= $(lastword ${MAKEFILE_LIST}) + +# Load environment variables if needed--not the case when this is called from +# the main Makefile +ifndef ENV_LOADED + + # Assume the base directory is two levels up if not defined + MAKO_DIR ?= ../.. + + include ${MAKO_DIR}/environment.mk + +endif + +# Use the current directory as the content section name +content_section ::= $(shell basename ${CURDIR}) +working_dir ::= ${WORKING}/${content_section} +staging_dir ::= ${STAGING}/${content_section} + +# Define using the same definition as in the main Makefile +build_date_msg ?= This page was built on $$(date). + +# Define special files +index_template ::= index.gmi.m4 +index ::= ${staging_dir}/index.gmi + +header_template ::= header.gmi.m4 +header ::= ${working_dir}/header.gmi + +footer_template ::= footer.gmi.m4 +footer ::= ${working_dir}/footer.gmi + +# Capture potential dependencies +all ::= $(notdir $(filter-out _%,%~,.%,$(wildcard *))) + +# all good above this line +#-----------------------------------------------------------------------------# +# +# Configuration +# +#-----------------------------------------------------------------------------# + + + +tagged_index_template ?= tagged-index.gmi.m4 + +posts ::= $(wildcard *.gmi) +targets ::= $(addprefix ${STAGING}/, ${posts}) + +# support indexing +entries ::= $(addprefix ${WORKSPACE}/, $(patsubst %.gmi, %.lnk, ${posts})) +tag_list ::= ${WORKSPACE}/tag-list + + +#tags = $(shell cut --delimiter ' ' --fields 3 ${tag_list} | sort) +#tagged_indicies = $(addprefix ${STAGING}/, $(addsuffix .gmi, ${tags})) + + + +# TODO: is there a name for elements header and footer? How can this work with +# templates? +# header and footer + + +#-----------------------------------------------------------------------------# +# +# User interface +# +#> This makefile supports the following targets: +#> +#-----------------------------------------------------------------------------# + +.PHONY: default build clean show help create + +# define the default target explicitly +default: create + +create: #> Create a new post (default) + @if [ -z $${EDITOR} ]; then \ + python3 create-post.py; \ + else \ + python3 create-post.py --edit; \ + fi + +build: ${index} + @echo "✓ Completed processing ${content_section}" + @echo + + +show: #> Show enironment variables with values + @echo staging area: ${STAGING} + @echo workspace: ${WORKSPACE} + @echo post_index: ${post_index} + @echo ${build_date_msg} + @echo targets: ${post_targets} + @echo header: ${header} + @echo footer: ${footer} + @echo tag list: ${tag_list} + +clean: #> Delete generated files + @rm -rf ${STAGING} ${WORKSPACE} + @echo "\t✓ Deleted intermediate files" + @echo "\t✓ Deleted all posts in ${STAGING}" + +help: #> Display this help message + @awk -f ../generate-help.awk ${MAKEFILE_LIST} + +#-----------------------------------------------------------------------------# +# +# File system interface +# +#-----------------------------------------------------------------------------# + +${working_dir} ${staging_dir}: %: + @mkdir -p $@ + @echo "\t✓ Created space: $@" + +${header} ${footer}: ${working_dir}/%: % ${working_dir} ${all} + @mkdir -p $@ + @echo "\t✓ Created: $@" + +${index}: ${index_template} ${header} ${footer} ${posts} + +# Build posts +# +# 1. Expand header and footer macros +# 2. Concatenate header, post, footer into the staging area +${header} ${footer}: ${WORKSPACE}/%: %.m4 + @echo building $@ + @m4 --include=${MAKO_DIR}/fence.m4 $< > $@ + @echo "\t✓ Created $@" + +${targets}: ${STAGING}/%: % ${header} ${footer} + @cat ${header} $< ${footer} > $@ + @echo ${build_date_msg} >> $@ + @echo "\t✓ Generated $@" + +# +# Build the tag list (indexing data) +# +# 1. Extract indexing data from each post +# 2. Combine into one list of tags +${entries}: ${WORKSPACE}/%.lnk: ${MAKO_DIR}/create-index-entry.awk %.gmi + @awk -f $^ > $@ + @echo "\t✓ Created $@" + +# build the tag list +${tag_list}: ${entries} + @cat $^ | sort --unique | \ + sed -e '/^---/ d; s/^\([a-zA-Z0-09]\+\).*/=> \1.gmi \1/' > $@ + @echo "\t✓ Created the tags list" + + +# +# Build the index files +# +# +${index}: ${index_template} ${tag_list} + # build the index files + # recurse into another Makefile? + @m4 --include=${MAKO_DIR} $< > $@ + + +#${tagged_indicies}: ${STAGING}/%.gmi: tag-index-template.gmi.m4 ${tag_list} +# @m4 --include=.. \ +# --define=TAG=% \ +# --define=ENTRIES=${entries} \ +# --define=HEADER=${header} \ +# --define=FOOTER=${footer} \ +# tag-index-template.gmi.m4 $< > $@ +# @echo ✓ Created index $@ + +# generate the index entries across all posts +${index_entries}: ${tag_list} + @grep "^---" $^ | cut --delimiter=' ' --fields=2 | sort --reverse --key=3 > $@ + @echo ✓ Created $@ + +${index}: content-index.gmi.m4 ${STAGING} ${tag_list} ${header} ${footer} ${post_entry} + @m4 --include=.. \ + --define=TAGS=${tags_list} \ + --define=POSTS=${post_entry} \ + --define=HEADER=${header} \ + --define=FOOTER=${footer} \ + $< > $@ + @echo ✓ Created $@ + +#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-#