Add initial files

This commit is contained in:
Andrew Stryker 2023-09-14 10:39:45 -07:00
parent 2d0ca4ac82
commit 03bbc3816d
5 changed files with 683 additions and 0 deletions

122
create-index-entry.awk Executable file
View File

@ -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 <axs@sdf.org>
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-#
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
}
}
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-#

115
curate.mk Normal file
View File

@ -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 <axs@sdf.org>
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-#
#-----------------------------------------------------------------------------#
#
# 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 $@"
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-#

125
environment.mk Normal file
View File

@ -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 <axs@sdf.org>
#
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-#
# 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
#>
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-#

96
fence.m4 Normal file
View File

@ -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 ---------------------------------------------------------------------------#

225
index.mk Normal file
View File

@ -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 <axs@sdf.org>
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-#
#-----------------------------------------------------------------------------#
#
# 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 $@
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-#