#!/bin/sh
# conform-doc-style [-hnq] page ... -- the rules of a manual page that mandoc does not check.
# One stderr line per fault, "<page>: <what>"; stdout: the count. rc 0 clean, 1 faults, 2 usage
# or no mandoc. -n is accepted for the convention of the family and changes nothing.
#   mandoc -T lint     every message, minus the two a workstation always prints
#                      (a referenced page it does not hold, an outdated page database)
#   order              .It Fl letters of a list sorted: digits, then A a B b ...
#   synopsis           the Fl letters of SYNOPSIS are the .It Fl letters of the page
#   -h                 the description of -h is the standard one
#   section 7          DESCRIPTION and SEE ALSO present
#   MUST, SHOULD       the page cites RFC 2119 or BCP 14
#   DESIGN DECISIONS   each entry is .It Sy decided|proposed|open
#   exit status        EXIT STATUS of a section 1 or 8 page holds .Ex -std
#   ascii              no byte outside ASCII
#   words              no word of the banned list
PATH=/bin:/sbin:/usr/bin:/usr/sbin
set -u
export LC_ALL=C

usage() { echo "usage: conform-doc-style [-hnq] page ..." >&2; }
quiet=0
while getopts hnq o; do
	case $o in
	h) usage; exit 0 ;;
	n) ;;
	q) quiet=1 ;;
	*) usage; exit 2 ;;
	esac
done
shift $((OPTIND - 1))
[ $# -ge 1 ] || { usage; exit 2; }
command -v mandoc > /dev/null 2>&1 || { echo "conform-doc-style: mandoc not found" >&2; exit 2; }

BANNED='robust|seamless|seamlessly|comprehensive|powerful|simple|simply|easy|easily|flexible|furthermore|additionally|moreover|typically|leverage|utilize|basically|actually|just|serves as|acts as|in summary|in conclusion'
STD_H='Print the usage on standard output and exit.'

faults=0
for page in "$@"; do
	[ -f "$page" ] || { echo "conform-doc-style: $page: no such file" >&2; exit 2; }
	out=$( { mandoc -T lint "$page" 2>&1 || true; } | grep -v -E 'referenced manual not found|outdated mandoc\.db' | sed 's/^mandoc: //')
	if [ -n "$out" ]; then
		echo "$out" | sed 's/^/mandoc: /' >&2
		faults=$((faults + $(echo "$out" | wc -l)))
	fi
	out=$(awk -v std_h="$STD_H" '
		BEGIN { order = "0123456789AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz" }
		function fault(m) { printf "%s: %s\n", FILENAME, m; n++ }
		function flush_h() { if (inh) { gsub(/^ +| +$/, "", htext); if (htext != std_h) fault("the description of -h is not: " std_h); inh = 0; htext = "" } }
		/^\.Dt / { sect = $3 }
		/^\.Sh / { flush_h(); sec = substr($0, 5); secs[sec] = 1; if (sec == "EXIT STATUS") exitsec = 1; next }
		$0 !~ /^\.\\"/ && /(^|[^A-Za-z])(MUST|SHOULD)([^A-Za-z]|$)/ { must = 1 }
		$0 !~ /^\.\\"/ && /RFC 2119|BCP 14/ { rfc = 1 }
		sec == "DESIGN DECISIONS" && $1 == ".It" && !($2 == "Sy" && $3 ~ /^(decided|proposed|open)$/) { fault("DESIGN DECISIONS: an entry is not .It Sy decided, proposed or open") }
		/^\.Bl / { flush_h(); last = "" }
		/^\.El/ { flush_h() }
		sec == "SYNOPSIS" { for (i = 1; i < NF; i++) if ($i == "Fl" && $(i + 1) ~ /^[A-Za-z0-9]+$/) { w = $(i + 1); for (k = 1; k <= length(w); k++) syn[substr(w, k, 1)] = 1 } }
		sec == "DESCRIPTION" && $1 == ".It" { flush_h() }
		sec == "DESCRIPTION" && $1 == ".It" && $2 == "Fl" && $3 ~ /^[A-Za-z0-9]$/ {
			c = $3; desc[c] = 1; r = index(order, c)
			if (last != "" && r < index(order, last)) fault("option -" c " is after -" last ": order is digits, then A a B b ...")
			last = c
			if (c == "h") inh = 1
			next }
		inh && !/^\./ { htext = htext (htext == "" ? "" : " ") $0 }
		sec == "EXIT STATUS" && $1 == ".Ex" && $2 == "-std" { ex = 1 }
		END { flush_h()
			for (c in syn) if (!(c in desc)) fault("-" c " is in SYNOPSIS and not in DESCRIPTION")
			for (c in desc) if (!(c in syn)) fault("-" c " is in DESCRIPTION and not in SYNOPSIS")
			if (sect == "7") { if (!("DESCRIPTION" in secs)) fault("a section 7 page has no DESCRIPTION"); if (!("SEE ALSO" in secs)) fault("a section 7 page has no SEE ALSO") }
			if (must && !rfc) fault("MUST or SHOULD used, and neither RFC 2119 nor BCP 14 is cited")
			if (exitsec && (sect == "1" || sect == "8") && !ex) fault("EXIT STATUS has no .Ex -std")
			exit n > 0 ? 1 : 0 }' "$page" 2>&1)
	if [ -n "$out" ]; then
		echo "$out" >&2
		faults=$((faults + $(echo "$out" | wc -l)))
	fi
	out=$(grep -n '[^ -~	]' "$page" | cut -d: -f1 | while read -r l; do echo "$page: line $l: byte outside ASCII"; done)
	if [ -n "$out" ]; then
		echo "$out" >&2
		faults=$((faults + $(echo "$out" | wc -l)))
	fi
	out=$(grep -n -i -w -E "$BANNED" "$page" 2>/dev/null | grep -v -E '^[0-9]+:\.\\"' | while IFS= read -r l; do echo "$page: line ${l%%:*}: banned word ($(echo "${l#*:}" | grep -o -i -w -E "$BANNED" | head -1))"; done)
	if [ -n "$out" ]; then
		echo "$out" >&2
		faults=$((faults + $(echo "$out" | wc -l)))
	fi
done

[ "$quiet" -eq 1 ] || echo "conform-doc-style: $faults faults"
[ "$faults" -eq 0 ]
