X-Git-Url: https://git.ladys.computer/Shushe/blobdiff_plain/2024d501192c7399c0791dec5ba0d3004613aefc..61599492d0451c48f963a13a78a11a59415eb6f0:/GNUmakefile?ds=inline
diff --git a/GNUmakefile b/GNUmakefile
index 75045a9..2c77723 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -22,24 +22,26 @@ override define makefileinfo
 ║│ • cksum                                                    │║
 ║│ • cp                                                       │║
 ║│ • date                                                     │║
-║│ • echo                                                     │║
 ║│ • file                                                     │║
 ║│ • find                                                     │║
+║│ • grep                                                     │║
 ║│ • git (optional)                                           │║
 ║│ • ln                                                       │║
-║│ • mkdir (requires support for `-p´)                        │║
+║│ • ls                                                       │║
+║│ • mkdir                                                    │║
 ║│ • mv                                                       │║
-║│ • od (requires support for `-t x1´)                        │║
+║│ • od                                                       │║
+║│ • pax (only when generating archives)                      │║
 ║│ • printf                                                   │║
 ║│ • rm                                                       │║
 ║│ • sed                                                      │║
 ║│ • sleep                                                    │║
-║│ • stat                                                     │║
 ║│ • test                                                     │║
 ║│ • touch                                                    │║
-║│ • tr (requires support for `-d´)                           │║
-║│ • uuencode (requires support for `-m´ and `-r´)            │║
-║│ • xargs (requires support for `-0´)                        │║
+║│ • tr                                                       │║
+║│ • uuencode                                                 │║
+║│ • uudecode                                                 │║
+║│ • xargs                                                    │║
 ║│ • xmlcatalog (provided by libxml2)                         │║
 ║│ • xmllint (provided by libxml2)                            │║
 ║│ • xsltproc (provided by libxslt)                           │║
@@ -65,6 +67,10 @@ override define makefileinfo
 ║│ • `make list´: List all recognized source files and their  │║
 ║│   classification (including media type and dependencies).  │║
 ║│                                                            │║
+║│ • `make listout´: List out the destinations of all         │║
+║│   resulting files (relative to `DESTDIR´), e·g for         │║
+║│   processing by another script.                            │║
+║│                                                            │║
 ║│ • `make uninstall´: Remove installed files, but not        │║
 ║│   `BUILDDIR´.                                              │║
 ║│                                                            │║
@@ -96,28 +102,40 @@ CAT := cat
 CKSUM := cksum
 CP := cp
 DATE := date
-ECHO := echo
 FILE := file
 FIND := find
 GIT := git
+GREP := grep
 LN := ln
+LS := ls
 MKDIR := mkdir
 MV := mv
 OD := od
+PAX := pax
 PRINTF := printf
 RM := rm
 SED := sed
 SLEEP := sleep
-STAT := stat
 TEST := test
 TOUCH := touch
 TR := tr
+UUDECODE := uudecode
 UUENCODE := uuencode
 XARGS := xargs
 XMLCATALOG := xmlcatalog
 XMLLINT := xmllint
 XSLTPROC := xsltproc
 
+# This variable is not used in normal execution.
+#
+# In the archiving `MODE´, it provides the X·M·L file from which the archive should be constructed.
+SRC :=
+
+# This variable is not used in normal execution.
+#
+# In the archiving `MODE´, it provides a user‐friendly name for the archive.
+NAME :=
+
 # The directory which contains the source files.
 #
 # Multiple directories can be given so long as files with the same name do not exist in each.
@@ -132,14 +150,20 @@ SRCDIR := sources
 INCLUDEDIR := sources/includes
 
 # The directory in which to generate temporary buildfiles.
+#
+# This variable is also used by the archiving `MODE´.
 BUILDDIR := build
 
 # The directory into which to output files on `make install´.
+#
+# This variable is also used by the archiving `MODE´.
 DESTDIR := public
 
 # The location of this Makefile (and related ⛩️📰 书社 files), relative to the current working directory.
 #
 # By default, this is inferred from the variable `MAKEFILE_LIST´.
+#
+# This variable is also used by the archiving `MODE´.
 THISDIR := $(patsubst %/,%,$(dir $(lastword $(MAKEFILE_LIST))))
 
 # Configuration of `find´.
@@ -169,7 +193,10 @@ EXTRATRANSFORMS :=
 TRANSFORMS := $(sort $(patsubst ./%,%,$(wildcard $(THISDIR)/transforms/*.xslt)) $(EXTRATRANSFORMS))
 
 # List of types which should be treated as X·M·L.
-XMLTYPES := application/xml text/xml
+#
+# Types which start with a `+´ are suffixes and match any type which
+# ends accordingly.
+XMLTYPES := application/xml text/xml +xml
 
 ifdef GIT
 ifneq ($(wildcard $(THISDIR)/.git),)
@@ -183,13 +210,28 @@ SRCREV := $(shell $(GIT) describe 2> /dev/null || $(GIT) rev-parse HEAD 2> /dev/
 endif
 endif
 
+# The mode in which to run this make·file.
+# Modes essentially allow for the combination of multiple conceptual make·files into a single source.
+#
+# The following modes are available :—
+#
+# • ‹ urn:fdc:ladys.computer:20231231:Shu1She4:mode:default ›:
+#   The default mode; typical Shushe behaviours.
+#
+# • ‹ urn:fdc:ladys.computer:20231231:Shu1She4:mode:archive ›:
+#   Generates archive files from parse results.
+MODE := urn:fdc:ladys.computer:20231231:Shu1She4:mode:default
+
+# Set to a non·empty value to silence informative messages.
+QUIET :=
+
 # Set to a non·empty value to print all commands as they run.
 VERBOSE :=
 
 # The default target for this makefile.
 .DEFAULT_GOAL := all
 
-# ━ § BEGIN MAKE·FILE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+# ━ § BEGIN SHARED MAKE·FILE CONSTRUCTS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
 # ─ ¶ Non‐Recipe Variable Definitions ─────────────────────────────────
 
@@ -211,9 +253,54 @@ override comma := ,
 # (callable) Quote the given string for use within shell calls.
 override quote = '$(subst ','"'"',$1)'
 
-# Outputs an `@´ to silence rules, unless `VERBOSE´ is nonempty.
+# (callable) Get the modified time of the provided file.
+#
+# Only the date·time information available via `ls -l´ is supported; that is, only minute precision for files newer than six months, and only day precision for files which are older.
+# This is, unfortunately, the best that can be done in pure Posix as of now.
+#
+# There is an easier implementation which provides the full date·time by archiving an empty file and then reading it back out of the archive :—
+#
+#     $(shell LC_TIME=POSIX TZ=UTC0 $(TOUCH) -m -r $(callquote,$1) $(BUILDDIR)/.mtime && $(PAX) -w -x ustar $(BUILDDIR)/.mtime | $(PAX) -v -o 'listopt=%(mtime=%Y-%m-%dT%H:%M:%SZ)T')
+#
+# However, it depends on the `listopt´ option in `pax´, which (despite being specified in Posix) no implementations actually support.
+#
+# ☡ This variable creates a subshell every time it is computed.
+override modtime = $(shell { $(DATE) -u '+%Y-%m'; LC_TIME=POSIX TZ=UTC0 ls -q -A -l $(call quote,$1) | $(SED) 's/\([^ ]* \{1,\}\)\{5\}\(\([^ ]* \{1,\}\)\{3\}\).*/\2/;s/ \([0123456789]\) / 0\1 /;s/Jan/01/;s/Feb/02/;s/Mar/03/;s/Apr/04/;s/May/05/;s/Jun/06/;s/Jul/07/;s/Aug/08/;s/Sep/09/;s/Oct/10/;s/Nov/11/;s/Dec/12/;s/\([0123456789]\{2\}\) *\([0123456789]\{2\}\) *\([0123456789]\{4\}\)/\3-\1-\2T00:00:00Z/;s/\([0123456789]\{2\}\) *\([0123456789]\{2\}\) *\([0123456789:]\{5\}\)/XXXX-\1-\2T\3:00Z/'; } | $(TR) -d ' ' | $(AWK) -F '-' 'NR==1{year=$$1;month=$$2}$$1=="XXXX"&&$$2<=month{print year"-"$$2"-"$$3}$$1=="XXXX"&&$$2>month{print year-1"-"$$2"-"$$3}NR>1&&$$1!="XXXX"')
+
+# ─ ¶ Recipe Variable Definitions ─────────────────────────────────────
+
+# Outputs an `@´ to silence rules, unless `VERBOSE´ is non·empty.
 override silent := $(if $(VERBOSE),,@)
 
+# (callable) Only perform the rule if `QUIET` is not set.
+override inform = $(if $(QUIET),,@$1)
+
+# (callable) Check to see if the given directory exists and create it if not.
+override ensuredirectory = if $(TEST) ! -d $(call quote,$1); then $(MKDIR) -p $(call quote,$1); fi
+
+# Quote standard input in such a way that piping it to xargs will result in it being processed as a single argument.
+#
+# If standard input ends in a newline, it is stripped; all other newlines are preserved.
+override xargsquote = $(SED) $(call quote,s/'/'"'"'/g;s/^/'/;s/$$/'/;$$!s/$$/\\/)
+
+# Quote standard input in such a way that piping it to xargs will result in each line being processed as a single argument.
+override xargsmultiquote = $(SED) $(call quote,s/'/'"'"'/g;s/^/'/;s/$$/'/)
+
+# (callable) Test if the provided xpath expression matches the provided document.
+override xpath = $(XMLLINT) --xpath $(call quote,$1) $(call quote,$2) > /dev/null 2> /dev/null
+
+# (callable) Extract the value of the text nodes in the provided X·M·L document and print them to `stdout´.
+override extracttext = $(PRINTF) '%s' '' | $(XSLTPROC) --nonet --novalid - $(call quote,$1)
+
+# (callable) Process the provided transformation result and output the result to the provided location, given the provided relative path.
+override processresultto = if $(call xpath,/*[local-name()="raw-text" and namespace-uri()="urn:fdc:ladys.computer:20231231:Shu1She4"],$1); then $(call extracttext,$1) > $(call quote,$2); elif $(call xpath,/*[local-name()="base64-binary" and namespace-uri()="urn:fdc:ladys.computer:20231231:Shu1She4"],$1); then { $(PRINTF) '%s\n' 'begin-base64 644 -'; $(call extracttext,$1) | $(TR) -d '\t\n\f\r '; $(PRINTF) '\n%s\n' '===='; } | $(UUDECODE) -o /dev/stdout > $(call quote,$2); elif $(call xpath,/*[local-name()="archive" and namespace-uri()="urn:fdc:ladys.computer:20231231:Shu1She4"],$1); then $(MAKE) -f $(call quote,$(abspath $(THISDIR)/GNUmakefile)) $(call quote,$2) NAME=$(call quote,$3) SRC=$(call quote,$1) BUILDDIR=$(call quote,$(BUILDDIR)/archive/$3) DESTDIR=$(call quote,$(patsubst %/,%,$(dir $2))) MODE='urn:fdc:ladys.computer:20231231:Shu1She4:mode:archive'; else $(XMLLINT) --nsclean $(call quote,$1) > $(call quote,$2); fi
+
+# ━ § BEGIN DEFAULT MAKE·FILE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ifeq ($(MODE),urn:fdc:ladys.computer:20231231:Shu1She4:mode:default)
+
+# ─ ¶ Non‐Recipe Variable Definitions ─────────────────────────────────
+
 # (callable) Test to see if the prerequisites provided by the second argument matches the value in the file corresponding to the first argument in `$(BUILDDIR)/lastprereqs´.
 # If not, save the new value and then add FORCE.
 # Return them regardless.
@@ -227,7 +314,7 @@ override diffprereqs = $(if $(subst $(shell $(CAT) $(call quote,$(BUILDDIR)/last
 override sedesc = $(subst /,[/],$(subst $$,\$$,$(subst *,\*,$(subst .,\.,$(subst [,\[,$(subst ^,\^,$(subst \,\\,$1)))))))
 
 # The command to use for percent‐decoding.
-override perdeccmd := $(SED) 's/|/%7C/g;s/[\]/%5C/g;s/%[0-9A-Fa-f]\{2\}/|&|/g' | $(TR) '|' '\n' | $(SED) '/^%[0-9A-Fa-f]\{2\}$$/!s/%/|%25|/' | $(TR) '|' '\n' | $(AWK) '$$0!~/%/{printf "%s",$$0}/%/{d="0123456789ABCDEF";v=substr(toupper($$0),2,2);printf "\\%04o",(index(d,substr(v,1,1))-1)*16+index(d,substr(v,2,1))-1}' | $(XARGS) -0 $(PRINTF) '%b'
+override perdeccmd := $(SED) 's/|/%7C/g;s/[\]/%5C/g;s/%[0123456789ABCDEFabcdef]\{2\}/|&|/g' | $(TR) '|' '\n' | $(SED) '/^%[0123456789ABCDEFabcdef]\{2\}$$/!s/%/|%25|/' | $(TR) '|' '\n' | $(AWK) '$$0!~/%/{printf "%s",$$0}/%/{d="0123456789ABCDEF";v=substr(toupper($$0),2,2);printf "\\%04o",(index(d,substr(v,1,1))-1)*16+index(d,substr(v,2,1))-1}' | $(xargsquote) | $(XARGS) -E '' $(PRINTF) '%b'
 
 # (callable) Percent‐decode the given strings.
 #
@@ -242,7 +329,7 @@ override perdec = $(shell $(PRINTF) '%s\0450A' $(foreach encoded,$1,$(call quote
 # It is assumed that the given strings do not contain newlines.
 #
 # ☡ This variable creates a subshell every time it is computed.
-override perenc = $(shell $(PRINTF) '%s\n' $(foreach unencoded,$1,$(call quote,$(unencoded))) | $(OD) -t x1 | $(SED) 's/^[0-9]*//;s/0[Aa]/%&/g;s/2[146-9A-Ea-e]/%&/g;s/3[0-9AaBbDd]/%&/g;s/40/%&/g;s/[46][1-9A-Fa-f]/%&/g;s/[57][0-9Aa]/%&/g;s/5[Ff]/%&/g;s/7[Ee]/%&/g;s/[ ][0-9A-Fa-f]\{2\}/%25&/g' | $(TR) -d ' \n' | $(TR) 'abcdef' 'ABCDEF' | $(perdeccmd))
+override perenc = $(shell $(PRINTF) '%s\n' $(foreach unencoded,$1,$(call quote,$(unencoded))) | $(OD) -t x1 | $(SED) 's/^[0123456789]*//;s/0[Aa]/%&/g;s/2[146789ABCDEabcde]/%&/g;s/3[0123456789AaBbDd]/%&/g;s/40/%&/g;s/[46][123456789ABCDEFabcdef]/%&/g;s/[57][0123456789Aa]/%&/g;s/5[Ff]/%&/g;s/7[Ee]/%&/g;s/[ ][0123456789ABCDEFabcdef]\{2\}/%25&/g' | $(TR) -d ' \n' | $(TR) 'abcdef' 'ABCDEF' | $(perdeccmd))
 
 # (callable) Percent‐encode each component in the given paths.
 #
@@ -257,7 +344,7 @@ sourcefiles := $(filter-out $(sourceincludes),$(shell $(FIND) $(foreach dir,$(SR
 
 # Figure out the file type of each source file and source include.
 ifneq ($(wildcard $(BUILDDIR)/magic.mgc),)
-override types := $(shell $(SED) 's/^ *//;s/ *$$//;s/ {2,}/ /g' <<< $(call quote,$(sourcefiles) $(sourceincludes)) | $(TR) ' ' '\n' | $(FILE) -m $(call quote,$(BUILDDIR)/magic.mgc) --mime-type --separator '|' --files-from - | $(SED) 's/| */|/g')
+override types := $(shell $(PRINTF) '%s\n' $(call quote,$(sourcefiles) $(sourceincludes)) | $(SED) 's/^ *//;s/ *$$//;s/ {2,}/ /g' | $(TR) ' ' '\n' | $(FILE) -m $(call quote,$(BUILDDIR)/magic.mgc) --mime-type --separator '|' --files-from - | $(SED) 's/| */|/g')
 endif
 
 # Get the list of supported plaintext file types from the parser.
@@ -265,8 +352,10 @@ ifneq ($(wildcard $(BUILDDIR)/parser.xslt),)
 override plaintexttypes := $(filter-out $(XMLTYPES),$(shell $(XMLLINT) --xpath '/*/*[@name="书社:parsers"]//*[namespace-uri()="http://www.w3.org/1999/xhtml" and local-name()="dd"]/text()' $(call quote,$(BUILDDIR)/parser.xslt)))
 endif
 
-# (callable) Get all of the files (source and includes) which have the given types.
-override filesoftype = $(foreach type,$1,$(patsubst %|$(type),%,$(filter %|$(type),$(types))))
+# (callable) Get all of the files (source and includes) which have the provided types.
+#
+# If a provided type begins with a `+´, it is analysed as a suffix, and all matching types will be used.
+override filesoftype = $(foreach typeorsuffix,$1,$(foreach type,$(if $(filter +%,$(typeorsuffix)),$(sort $(foreach suffixedtype,$(filter %$(typeorsuffix),$(types)),$(lastword $(subst |, ,$(suffixedtype))))),$(typeorsuffix)),$(patsubst %|$(type),%,$(filter %|$(type),$(types)))))
 
 # Build up collections of various file types.
 override xmlfiles := $(call filesoftype,$(XMLTYPES))
@@ -294,7 +383,7 @@ override includepath = $(firstword $(foreach directory,$(INCLUDEDIR),$(if $(filt
 # (callable) Get base64 data u·r·i’s for the given files.
 #
 # ☡ This variable creates a subshell every time it is computed.
-override datauri = $(foreach file,$1,data:$(call typeoffile,$(file));base64,$(shell $(UUENCODE) -m -r $(call quote,$(file)) _ | tr -d ' \n'))
+override datauri = $(foreach file,$1,data:$(call typeoffile,$(file));base64,$(shell $(UUENCODE) -m $(call quote,$(file)) _ | $(SED) '2,$$!d;$$d' | $(TR) -d ' \n'))
 
 # Pair each source file and include with its local u·r·i.
 override sourcelocalpair := $(foreach file,$(sourcefiles) $(sourceincludes),$(file)|about:shushe?$(if $(filter $(file),$(sourceincludes)),include=$(call pathenc,$(call includepath,$(file))),source=$(call pathenc,$(call sourcepath,$(file)))))
@@ -366,44 +455,47 @@ override sourcedestinationpair := $(foreach destination,$(destinations),$(call s
 override destination = $(foreach file,$1,$(patsubst $(file)|%,%,$(filter $(file)|%,$(sourcedestinationpair))))
 
 # Pair each source file with its compiled location.
-override sourcecompiledpair := $(foreach file,$(sourcefiles),$(file)|$(BUILDDIR)/public/$(call destination,$(file)))
+override sourcecompiledpair := $(foreach file,$(sourcefiles),$(file)|$(BUILDDIR)/results/$(call destination,$(file)))
 
 # (callable) Get the location of the transformed X·M·L files for the given source files.
 override compiled = $(foreach file,$1,$(patsubst $(file)|%,%,$(filter $(file)|%,$(sourcecompiledpair))))
 
-# (callable) Get the source files for the given compiled file.
+# (callable) Get the location of the source files for the given compiled file.
 override uncompiled = $(foreach file,$1,$(patsubst %|$(file),%,$(filter %|$(file),$(sourcecompiledpair))))
 
+# (callable) Get the location of the final built files for the given source files.
+override built = $(foreach file,$1,$(patsubst $(BUILDDIR)/results/%,$(BUILDDIR)/public/%,$(call compiled,$(file))))
+
+# (callable) Get the location of the source files for the given built files.
+override unbuilt = $(foreach file,$1,$(call uncompiled,$(patsubst $(BUILDDIR)/public/%,$(BUILDDIR)/results/%,$(file))))
+
 # (callable) Get the installed locations for the given source files.
 override installed = $(foreach file,$1,$(DESTDIR)/$(call destination,$(file)))
 endif
 
 # ─ ¶ Recipe Variable Definitions ─────────────────────────────────────
 
-# (callable) Check to see if the given directory exists and create it if not.
-override ensuredirectory = if $(TEST) ! -d $(call quote,$1); then $(MKDIR) -p $(call quote,$1); fi
-
 # (callable) Get the identifier for the given parser or transform.
 override id = $(XMLLINT) --xpath '/*/*[local-name()="id" and namespace-uri()="urn:fdc:ladys.computer:20231231:Shu1She4"]/text()[1]' $(call quote,$1) 2> /dev/null || $(PRINTF) '%s\n' $(call quote,about:shushe?$(or $2,unknown)=$(call pathenc,$(basename $(notdir $1))))
 
 # (callable) Sanitize and wrap the provided plaintext file in X·M·L, printing to `stdout´.
-override wrapplaintext = $(TR) '\000\013\014' '\032\011\012' < $(call quote,$1) | $(SED) "$$($(PRINTF) '%b' 's/]]>/]]]]>/g\ns/\0357\0277\0276/�/g\ns/\0357\0277\0277/�/g\n$$!s/\\r$$//g\ns/\\r/\\n/g\n$$!s/\0302\0205$$//g\ns/\0302\0205/\\n/g;s/\0342\0200\0250/\\n/g;s/[\0001-\0010]/�/g;s/[\0016-\0037]/�/g')" | $(XARGS) -0 -J %% $(PRINTF) '%b%s%s\n' '\n'
+override wrapplaintext = { $(PRINTF) '%s\n%s' '' ''; }
 
 # (callable) Check if the provided X·M·L file is X·M·L 1.1, and if so, coerce to X·M·L 1.0 as best as possible, printing the result (or the original file contents) to `stdout´.
 #
 # The X·M·L declaration will be dropped and character escapes for C0 control codes will be replaced with a literal `U+0091 PRIVATE USE ONE´, which is invalid in X·M·L 1.1, but valid X·M·L 1.0 (making the replacement obvious).
 #
 # This isn’t a perfect substitution (it makes some assumptions about the format of the underlying X·M·L), but it should be workable for most sensible, welformed files.
-override serializexml = $(SED) "$$($(PRINTF) '%b' '/]*?>//\n s/&\0043x0*[1-8BCEFbcef];/\0302\0221/g\n s/&\0043x0*1[0-9A-Fa-f];/\0302\0221/g\n s/&\00430*[1-8];/\0302\0221/g\n s/&\00430*1[124-9];/\0302\0221/g\n s/&\00430*2[0-9];/\0302\0221/g\n s/&\00430*3[01];/\0302\0221/g\n}')" < $(call quote,$1) | $(SED) "$$(PRINTF '%b' ':a\n/^\\n*$$/{ $$d\n N\n ba\n}')"
+override serializexml = $(SED) "$$($(PRINTF) '%b' '/]*?>//\n s/&\0043x0*[12345678BCEFbcef];/\0302\0221/g\n s/&\0043x0*1[0123456789ABCDEFabcdef];/\0302\0221/g\n s/&\00430*[12345678];/\0302\0221/g\n s/&\00430*1[12456789];/\0302\0221/g\n s/&\00430*2[0123456789];/\0302\0221/g\n s/&\00430*3[01];/\0302\0221/g\n}')" < $(call quote,$1) | $(SED) "$$($(PRINTF) '%b' ':a\n/^\\n*$$/{ $$d\n N\n ba\n}')"
 
 # ─ ¶ Phony Targets ───────────────────────────────────────────────────
 
 # Compile all files, or error if any are recursive.
-all : $(call compiled,$(recursivefiles) $(compilablefiles)) ;
+all : $(call built,$(recursivefiles) $(installablefiles)) ;
 
 # Destroy buildfiles.
 clean :
-	$(if $(BUILDDIR),$(silent)$(RM) -rf $(call quote,$(BUILDDIR)/),)
+	$(if $(BUILDDIR),$(silent)$(RM) -f -R $(call quote,$(BUILDDIR)/),)
 
 # Destroy build directory and installed files.
 gone : clean uninstall ;
@@ -417,15 +509,19 @@ install : $(call installed,$(recursivefiles) $(installablefiles)) ;
 
 # List all source files and includes and their computed types.
 list :
-	@$(PRINTF) '%b' $(call quote,$(foreach file,$(sort $(sourcefiles)) $(sort $(sourceincludes)),\0033[1m$(file)\0033[22m|$(call typeoffile,$(file))|[\0033[3m$(if $(filter $(file),$(xmlfiles)),xml,$(if $(filter $(file),$(plaintextfiles)),text,asset))$(if $(filter $(file),$(sourceincludes)),|include,)\0033[23m]$(if $(call dependencies,$(file))$(call recursives,$(file)), $(strip $(foreach recursive,$(call recursives,$(file)),\0033[93;41m•|Recursive|Dependency|\0033[39;49m|$(recursive)) $(foreach dependency,$(call dependencies,$(file)),\0033[2m•|Dependency|\0033[22m|$(dependency))))$(if $(filter $(file),$(sourcefiles)), →|<\0033[4m/$(call destination,$(file))\0033[24m>,) )) | $(TR) ' |' '\n '
+	@$(PRINTF) '%b' $(call quote,$(foreach file,$(sort $(sourcefiles)) $(sort $(sourceincludes)),\0033[1m$(file)\0033[22m|$(call typeoffile,$(file))|[\0033[3m$(if $(filter $(file),$(xmlfiles)),xml,$(if $(filter $(file),$(plaintextfiles)),text,asset))$(if $(filter $(file),$(sourceincludes)),|include,)\0033[23m] Modified:|$(call modtime,$(file))$(if $(call dependencies,$(file))$(call recursives,$(file)), $(strip $(foreach recursive,$(call recursives,$(file)),\0033[93;41m•|Recursive|Dependency|\0033[39;49m|$(recursive)) $(foreach dependency,$(call dependencies,$(file)),\0033[2m•|Dependency|\0033[22m|$(dependency))))$(if $(filter $(file),$(sourcefiles)), →|<\0033[4m/$(call destination,$(file))\0033[24m>,) )) | $(TR) ' |' '\n '
+
+# Lists out the destinations of all resulting files (relative to `DESTDIR´).
+listout :
+	@$(PRINTF) '%s\n' $(call quote,$(foreach file,$(sort $(sourcefiles)),$(call destination,$(file))))
 
 # Destroy installed files.
 uninstall :
 	$(foreach file,$(installablefiles),$(if $(wildcard $(call installed,$(file))),$(silent)$(PRINTF) '%s\n' $(call quote,Removing $(patsubst $(DESTDIR)/%,%,$(call installed,$(file)))>…)$(newline)$(silent)$(RM) -f $(call quote,$(call installed,$(file)))$(newline),))
 
 # Raise an error when attempting to build any files with recursive dependencies.
-$(call compiled,$(recursivefiles)) :
-	@$(PRINTF) '%b\n' $(call quote,\0033[93;41mError:\0033[39;49m `$(call uncompiled,$@)´ has recursive dependencies:\n$(subst |, ,$(subst $(space),$(newline),$(foreach recursive,$(call recursives,$(call uncompiled,$@)),•|$(recursive))))) && false
+$(call built,$(recursivefiles)) :
+	@$(PRINTF) '%b\n' $(call quote,\0033[93;41mError:\0033[39;49m `$(call unbuilt,$@)´ has recursive dependencies:\n$(subst |, ,$(subst $(space),$(newline),$(foreach recursive,$(call recursives,$(call unbuilt,$@)),•|$(recursive))))) >&2 && false
 
 # Add as a prerequisite to treat the target as tho it were phony.
 FORCE : ;
@@ -439,7 +535,7 @@ FORCE : ;
 .SUFFIXES : ;
 
 # Phony rules; always consider these out·of·date.
-.PHONY : FORCE all default clean gone info install list uninstall $(call compiled,$(recursivefiles)) ;
+.PHONY : FORCE all default clean gone info install list listout uninstall $(call built,$(recursivefiles)) ;
 
 ifneq ($(typeupdates)$(wildcard $(BUILDDIR)/dependencies)$(wildcard $(BUILDDIR)/destinations),)
 # Reload this make·file if the dependency graph or output destinations have changed.
@@ -452,10 +548,10 @@ ifneq ($(typeupdates)$(wildcard $(BUILDDIR)/dependencies)$(wildcard $(BUILDDIR)/
 $(THISDIR)/GNUmakefile :: $(BUILDDIR)/dependencies $(BUILDDIR)/destinations
 	$(silent)$(TOUCH) $(THISDIR)/GNUmakefile
 ifeq ($(typeupdates),)
-	@if $(TEST) ! -f $(call quote,$(BUILDDIR)/.update-types); then $(PRINTF) '%b\n' '\0033[1mDependency graph and output destinations updated. Restarting…\0033[22m'; fi
+	$(call inform,if $(TEST) ! -f $(call quote,$(BUILDDIR)/.update-types); then $(PRINTF) '%b\n' '\0033[1mDependency graph and output destinations updated. Restarting…\0033[22m' >&2; fi)
 else
-	$(silent)$(RM) $(BUILDDIR)/.update-types
-	@$(PRINTF) '%b\n' '\0033[1mDependency graph and output destinations updated. Restarting…\0033[22m'
+	$(silent)$(RM) $(call quote,$(BUILDDIR)/.update-types)
+	$(call inform,$(PRINTF) '%b\n' '\0033[1mDependency graph and output destinations updated. Restarting…\0033[22m' >&2)
 endif
 endif
 
@@ -468,7 +564,7 @@ ifeq ($(typeupdates),)
 $(THISDIR)/GNUmakefile :: $(BUILDDIR)/magic.mgc $(BUILDDIR)/parser.xslt
 	$(silent)$(TOUCH) $(THISDIR)/GNUmakefile
 	$(silent)$(RM) -f $(call quote,$(BUILDDIR)/dependencies) $(call quote,$(BUILDDIR)/destinations)
-	@$(PRINTF) '%b\n' '\0033[1mMagic file or parsers have updated. Restarting…\0033[22m'
+	$(call inform,$(PRINTF) '%b\n' '\0033[1mMagic file or parsers have updated. Restarting…\0033[22m' >&2)
 	$(silent)$(SLEEP) 1
 	$(silent)$(TOUCH) $(call quote,$(BUILDDIR)/.update-types)
 endif
@@ -485,19 +581,19 @@ $(call magicfile,$(MAGIC)) : $(BUILDDIR)/magic/% : $$(call magicsource,$$@)
 # It must be updated if any of the files in the magic directory change.
 $(BUILDDIR)/magic.mgc : $(call diffprereqs,magic,$(sort $(call magicfile,$(MAGIC))))
 	$(foreach outdated,$(filter-out $^,$(wildcard $(BUILDDIR)/magic/*)),$(silent)$(RM) $(call quote,$(outdated))$(newline))
-	@$(ECHO) "Compiling new magic…"
+	$(call inform,$(PRINTF) '%s\n' 'Compiling new magic…' >&2)
 	$(silent)$(call ensuredirectory,$(dir $@))
 	$(silent)cd $(call quote,$(BUILDDIR)) && $(FILE) -C -m $(call quote,$(realpath $(BUILDDIR)/magic))
 	$(silent)$(TOUCH) $(call quote,$(BUILDDIR)/.update-types)
 
 # Generate the main parser.
 $(BUILDDIR)/parser.catalog : $(call diffprereqs,parsers,$(sort $(PARSERS)))
-	@$(ECHO) "Generating catalog of parsers…"
+	$(call inform,$(PRINTF) '%s\n' 'Generating catalog of parsers…' >&2)
 	$(silent)$(XMLCATALOG) --create --noout $(call quote,$@)
-	$(foreach parser,$(PARSERS),$(silent)( $(call id,$(parser)) ) | $(XARGS) -I %% $(XMLCATALOG) --add uri %% $(call quote,$(call fileuri,$(parser))) --noout $(call quote,$@)$(newline))
+	$(foreach parser,$(PARSERS),$(silent){ $(call id,$(parser)); $(PRINTF) '%s\n' $(call quote,$(call fileuri,$(parser))) '--noout' $(call quote,$@); } | $(xargsmultiquote) | $(XARGS) -E '' $(XMLCATALOG) --add uri$(newline))
 $(BUILDDIR)/parser.xslt : $(BUILDDIR)/parser.catalog $(THISDIR)/lib/catalog2parser.xslt
-	@$(ECHO) "Generating main parser…"
-	$(silent)$(XSLTPROC) -o $(call quote,$@) $(call quote,$(THISDIR)/lib/catalog2parser.xslt) $(call quote,$<)
+	$(call inform,$(PRINTF) '%s\n' 'Generating main parser…' >&2)
+	$(silent)$(XSLTPROC) --nonet --novalid -o $(call quote,$@) $(call quote,$(THISDIR)/lib/catalog2parser.xslt) $(call quote,$<)
 	$(silent)$(TOUCH) $(call quote,$(BUILDDIR)/.update-types)
 
 # Parse the files.
@@ -505,52 +601,125 @@ $(BUILDDIR)/parser.xslt : $(BUILDDIR)/parser.catalog $(THISDIR)/lib/catalog2pars
 # Even plain X·M·L files are parsed, because they may contain X·H·T·M·L `