~acdw/hell

ce40a9da27f130b2af9de21526aee22ceaa93ac9 — Case Duckworth 23 days ago 1e79136 master
Libraryize
3 files changed, 80 insertions(+), 257 deletions(-)

A hell
D hell.sh
D test.hell
A hell => hell +80 -0
@@ 0,0 1,80 @@
#!/bin/sh
# hell - HTML in shell

tx()
{
	: "${RAW:=false}"

	if "$RAW"; then
		printf '%b\n' "$*"
		return
	fi

	while
		[ "$#" -gt 0 ] && printf %s "$1"
	do
		[ "$#" -gt 1 ] && printf ' '
		shift
	done
	printf '\n'
}

el()
{ # el NAME [ATTR=VALUE...] [-] [TEXT...]
	el="$1"; shift
	READ_IN=true
	text=
	attr=
	class=

	for word; do
		case "$word" in
			\\*) # words starting with backslashes are text
				text="$text${text:+ }${word#\\}"
				;;
			class=*) # otherwise, 'class=' is a special case
				class="${class:-class=\"}${class:+ }${word#class=}"
				;;
			*=*) # otherwise, words containing an equals are attr=value pairs
				anam="${word%=*}"
				aval="${word#*=}"
				aval="${aval%\"}"
				aval="${aval#\"}"
				attr="$attr${attr:+ }${anam}=\"${aval}\""
				;;
			-) # otherwise, '-' denotes stdin
				"$READ_IN" &&
					while IFS= read -r line; do
						text="$text${text:+
}$line" # this is very ugly but it works
					done
				;;
			/) # close-tag flag: don't look for text from stdin
				READ_IN=false
				;;
			*) # otherwise, it's text
				text="$text${text:+ }$word"
				;;
		esac
	done

	# close out the class string
	if [ -n "$class" ]; then
		class="$class\""
		attr="$attr${attr:+ }$class"
	fi

	# if there's no text, try reading from stdin
	if [ -z "$text" ] && "$READ_IN"; then
		while IFS= read -r line; do
			text="$text${text:+
}$line" # this is very ugly but it works
		done
	fi

	# print the thing
	printf '<%s%s>%s' "$el" "${attr:+ }$attr" "$text"
	# if there's text, close the tag
	[ -n "$text" ] && printf '</%s>' "$el"
	printf '\n'
}

D hell.sh => hell.sh +0 -244
@@ 1,244 0,0 @@
#!/bin/sh
# hell - HTML in shell
# (c) 2020 Case Duckworth <acdw at acdw dot net>
# License: MIT
# v.0.0.2 (codename WHY AM I DOING THIS?)
#
# This file is part of _hawkish_, an ssg made of shell, awk, and Makefiles
# (maybe). As POSIX as possible. For fun and non-profit.
# Maybe I could rename this Saitama: I'm a build system for fun!
# ANYWAY:
# This file is meant to be sourced by the eventual generated sh script.
# It will define functions that the generated script will use to generate
# HTML.
# The generated sh script will be generated by an intermediary awk script that
# transforms input text files into a shab(github.com/zimbatm/shab)-like file,
# which will be run to output the HTML.

[ -n "$DEBUG" ] && set -x

# implement die (TODO: put this in a util file?)
die()
{
	case "$#" in
		1) echo "$1"; exit 1 ;;
		*) ec="$1"; shift; echo "$@"; exit "$ec" ;;
	esac
}


# First things first - define how to cleanly exit.
cleanup()
{
	rm "$BLOCKS" "$INLINES"
}
trap cleanup INT QUIT TERM EXIT


# BLOCKS holds all open block-level elements so that we can close them like good
# little children.
BLOCKS="$(mktemp hell-blocks.XXXXXX)"
# INLINES is the same, but for inlines.
INLINES="$(mktemp hell-inlines.XXXXXX)"

# You can push tags to the stack or pop them.
# Note: these only deal with tag NAMEs (e.g. 'p', 'h1', etc.). The actual TAGs
# ('<h1 class=whatever>', etc.) are left up to the caller.
# These only deal with one line at a time right now. I think that's best.
push()
{ # push FILE VALUE
	# returns: VALUE
	stack="$1"
	#shift

	printf '%s\n' "$2" >> "$stack"
}

# see unix.stackexchange.com/questions/474838
pop()
{ # pop FILE
	# returns: popped VALUE
	# shellcheck disable=2034
	LC_TYPE=C
	stack="$1"
	#shift

	l="$(tail -n1 "$stack"; echo t)"
	[ "$l" = t ] && return 1
	l="${l%t}"
	# TODO: make sure truncate(1) is available most places
	truncate -s "-${#l}" "$stack"
	printf '%s' "$l"
}

# common function for block() and inline().
# consider: parsing 'tag.class#id.class[attr=attr]' type lines
# or just what it is now, i.e. 'attr=value attr=value text text'
parseterms()
{ # parseterms [attr=VALUE] [TEXT]
	: "${attr:=}"
	: "${text:=}"
	class=""
	for term; do
		case "$term" in
			\\*) # if it starts with a backslash, include it in text
				text="$text ${term#\\}"
				;;
			class=*) # if it's a class, add it to the class string
				# TODO: any other attrs to do this with?
				if [ -z "$class" ]; then
					class="class=\"${term#\class=}"
				else
					class="$class ${term#\class=}"
				fi
				;;
			*=*) # if it has an equal, it's an attribute
				name="${term%=*}"
				value="${term#*=}"
				attr="$attr $name=\"$value\""
				;;
			*) # otherwise, it's text
				if [ -z "$text" ]; then
					text="$term"
				else
					text="$text $term"
				fi
				;;
		esac
	done
	if [ -n "$class" ]; then
		class="$class\""
		attr="$attr $class"
	fi
}

# create a new tag.
# it won't auto-close unless the name ends with '/'.
tag()
{ # tag <-i|-b> TAG[/] [ATTR=VALUE...] [TEXT...]
	autoclose=false
	attr=""
	text=""

	case "$1" in
		-i) # inline
			tagstack="$INLINES"
			shift
			;;
		-b) # block
			tagstack="$BLOCKS"
			shift
			;;
		*) # other -- default to block
			tagstack="$BLOCKS"
			;;
	esac

	case "$1" in
		*/) # set this tag to auto-close. don't push it onto the stack.
			autoclose=true
			tag="${1%/}"
			;;
		*) # begin a tag and push it onto the stack.
			tag="$1"
			push "$tagstack" "$tag"
			;;
	esac

	shift
	parseterms "$@" # parse the rest of the arguments

	if "$autoclose" && [ -z "$text" ]; then
		# Okay, here's an ugly bit of HTML.
		# APPARENTLY, with "void elements" or "foreign elements" we *can* use a slash to end an
		# empty tag, but only with those. Otherwise, it's an error (apparently
		# in HTML4 it's ALL an error, but W3C validates it fine. ufgghh...
		case "$tag" in
			area|base|br|col|embed|hr|img|input|link|meta|\
			param|source|track|wbr|command|keygen|menuitem)
				# void elements
				printf '<%s%s />' "$tag" "$attr"
				return
				;;
				# TODO: add mathml & svg ... maybe.
			*) : ;; # do nothing - I'll open and close the tag below.
		esac
	fi

	# open the tag and add whatever text we've provided
	printf '<%s%s>%s' "$tag" "$attr" "${text:- }"

	# close the tag if asked
	if "$autoclose"; then
		printf '</%s>' "$tag"
	fi
	printf '\n'
}


# end something
end()
{ # end [-b] [-t TAG] [-n NUMBER]
	while getopts t:n:ba OPT; do
		case "$OPT" in
			t) # end upto tag -- close tags up to and including TAG
				target="$OPTARG"
				while : ; do
					tag="$(pop "$INLINES")" || break # fall through to BLOCKS
					printf '</%s>' "$tag"
					[ "$tag" = "$target" ] && return 0 # found the thing!
				done
				while : ; do
					tag="$(pop "$BLOCKS")" || return 1 # didn't find target
					printf '</%s>\n' "$tag"
					[ "$tag" = "$target" ] && return 0 # found the thing!
				done
				;;
			n) # end NUMBER of tags
				n="$OPTARG"
				;;
			b) # end up to the nearest block -- a special case of -n
				i="$(wc -l < "$INLINES")"
				n=$((i+1))
				;;
			a) # end all tags -- another special case of -n
				i="$(wc -l < "$INLINES")"
				b="$(wc -l < "$BLOCKS")"
				n=$((i+b+1))
				;;
			\?) # bad OPTARG
				return 2
				;;
			*) # bad OPT
				return 2
				;;
		esac
	done

	# -n case
	while [ "$n" -gt 0 ]; do
		tag="$(pop "$INLINES")" || break # fall through to BLOCKS
		printf '</%s>' "$tag"
		n=$((n-1))
	done
	while [ "$n" -gt 0 ]; do
		tag="$(pop "$BLOCKS")" || return 1 # exhausted tags before NUMBER
		printf '</%s>\n' "$tag"
		n=$((n-1))
	done
}

# text is just a regular line of text.
# no error checking is done on where the text is going to be --
# it just prints it. (this is a less-error-prone `echo`.)
text()
{ # text [TEXT...]
	if [ "$#" -gt 0 ]; then
		printf '%s' "$1"
		shift
		if [ "$#" -gt 0 ]; then
			printf ' %s' "$@"
		fi
	fi
	printf '\n'
}

D test.hell => test.hell +0 -13
@@ 1,13 0,0 @@
#!/bin/sh

. ./hell.sh

tag -b article
tag -b header
tag -b h1/ class=title "Here is a sample title"

tag -b p "Here's a paragraph. It's going to be pretty basic"
text "but hopefully it'll work out well.  I need to quote"
text "the arguments so that quotes and shit don't have problems."
text "I wonder if exclamation points (!) will cause any issues."
end -a