diff --git a/.gitignore b/.gitignore index f4697c4..4e8f83d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ /.circleci/.config.yml .download bin +man diff --git a/README.md b/README.md index ee4b3b6..872c72b 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,14 @@ If you only want to install a single script, you can run the build process and c +**Manuals:** (EXPERIMENTAL) + +You can specify `--manuals` to have the build script generate a `man` page for each of the markdown documentation files. +This is an experimental feature that uses a non-compliant Markdown "parser" written in Bash, and there is no guarantee +as for the quality of the generated manual pages. + + + **Alternate Executable:** Depending on the distribution, bat may have been renamed to avoid package conflicts. diff --git a/build.sh b/build.sh index 187499c..e00d2a1 100755 --- a/build.sh +++ b/build.sh @@ -8,6 +8,8 @@ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BIN="$HERE/bin" SRC="$HERE/src" +MAN="$HERE/man" +MAN_SRC="$HERE/doc" LIB="$HERE/lib" source "${LIB}/print.sh" source "${LIB}/opt.sh" @@ -363,6 +365,7 @@ OPT_INSTALL=false OPT_COMPRESS=false OPT_VERIFY=true OPT_BANNER=true +OPT_MANUALS=false OPT_MINIFY="lib" OPT_PREFIX="/usr/local" OPT_BAT="$(basename "$EXECUTABLE_BAT")" @@ -375,6 +378,7 @@ while shiftopt; do case "$OPT" in --install) OPT_INSTALL=true ;; --compress) OPT_COMPRESS=true ;; + --manuals) OPT_MANUALS=true ;; --no-verify) OPT_VERIFY=false ;; --no-banner) OPT_BANNER=false ;; --prefix) shiftval; OPT_PREFIX="$OPT_VAL" ;; @@ -427,6 +431,30 @@ for file in "$SRC"/*.sh; do SOURCES+=("$file") done +# ----------------------------------------------------------------------------- +# Build manuals. + +if "$OPT_MANUALS"; then + source "${HERE}/mdroff.sh" + if ! [[ -d "$MAN" ]]; then + mkdir -p "$MAN" + fi + + printc_msg "%{YELLOW}Building manuals...%{CLEAR}\n" + for source in "${SOURCES[@]}"; do + name="$(basename "$source" .sh)" + doc="${MAN_SRC}/${name}.md" + docout="${MAN}/${name}.1" + if ! [[ -f "$doc" ]]; then + continue + fi + + printc_msg " %{YELLOW} %{MAGENTA}%s%{CLEAR}\n" "$(basename "$docout")" + (mdroff < "$doc" > "${MAN}/${name}.1") + done + printc_msg "\n" +fi + # ----------------------------------------------------------------------------- # Build files. diff --git a/mdroff.sh b/mdroff.sh new file mode 100755 index 0000000..d3a7eef --- /dev/null +++ b/mdroff.sh @@ -0,0 +1,278 @@ +#!/usr/bin/env bash +# ----------------------------------------------------------------------------- +# bat-extras | Copyright (C) 2020 eth-p | MIT License +# +# Repository: https://github.com/eth-p/bat-extras +# Issues: https://github.com/eth-p/bat-extras/issues +# ----------------------------------------------------------------------------- + +printf_msg() { + printf "$@" 1>&2 +} + +printf_err() { + printf "$@" 1>&2 +} + +# ----------------------------------------------------------------------------- +# MDroff: +# ----------------------------------------------------------------------------- + +mdroff:emit:h1() { + printf '.TH "%s" 1\n' "$1" +} + +mdroff:emit:h2() { + printf '.SH "%s"\n' "$1" +} + +mdroff:emit:h3() { + echo "$1" +} + +mdroff:emit:h4() { + echo "$1" +} + +mdroff:emit:h5() { + echo "$1" +} + +mdroff:emit:line() { + if "$MDROFF_PARAGRAPH"; then + MDROFF_PARAGRAPH=false + printf ".P\n%s\n" "$1" + else + printf ".br\n%s\n" "$1" + fi +} + +mdroff:emit:attr() { + printf '\\fR' + + if "$MDROFF_ATTR_STRONG"; then + printf '\\fB' + fi + + if "$MDROFF_ATTR_EMPHASIS"; then + printf '\\fI' + fi +} + +mdroff:emit:link() { + printf "%s" "$1" +} + +mdroff:emit:table_heading() { + printf '.P\n\\fB' + printf '%s ' "$@" + printf '\\fR\n' + + # Emit separator. + printf '.br\n' + local cell + for cell in "$@"; do + printf "%$(wc -c <<< "$cell")s" '' | tr ' ' '-' + printf " " + done + printf "\n" +} + +mdroff:emit:table_row() { + printf '.br\n' + printf '%s ' "$@" + printf '\n' +} + +mdroff:emit() { + local type="$1" + local data="$2" + + if type "mdroff:rewrite:${type}" &>/dev/null; then + data="$("mdroff:rewrite:${type}" "${@:2}")" + fi + + if type "mdroff:emit_hook:${type}" &>/dev/null; then + "mdroff:emit_hook:${type}" "$data" "${@:3}" + return + fi + + "mdroff:emit:${type}" "$data" "${@:3}" +} + +mdroff:parseln() { + MDROFF_ATTR_STRONG=false + MDROFF_ATTR_EMPHASIS=false + + local buffer="$1" + local before + local found + local pos + + while [[ "${#buffer}" -gt 0 ]]; do + [[ "$buffer" =~ \*{1,3}|\[([^\]]+)\]\(([^\)]+)\) ]] || { + printf "%s\n" "$buffer" + return + } + + found="${BASH_REMATCH[0]}" + pos="$(awk -v search="$found" '{print index($0,search) - 1}' <<< "$buffer")" + + before="${buffer:0:$pos}" + buffer="${buffer:$(($pos + ${#found}))}" + + printf "%s" "$before" + case "$found" in + '***') + if "$MDROFF_ATTR_STRONG" && "$MDROFF_ATTR_EMPHASIS"; then + MDROFF_ATTR_STRONG=false + MDROFF_ATTR_EMPHASIS=false + else + MDROFF_ATTR_STRONG=true + MDROFF_ATTR_EMPHASIS=true + fi + mdroff:emit attr + ;; + + '**') + if "$MDROFF_ATTR_STRONG"; then + MDROFF_ATTR_STRONG=false + else + MDROFF_ATTR_STRONG=true + fi + mdroff:emit attr + ;; + + '*') + if "$MDROFF_ATTR_EMPHASIS"; then + MDROFF_ATTR_EMPHASIS=false + else + MDROFF_ATTR_EMPHASIS=true + fi + mdroff:emit attr + ;; + + '['*) + mdroff:emit link "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" + esac + done +} + +mdroff() { + MDROFF_HEADING_LEVEL=0 + MDROFF_HEADING='' + MDROFF_ATTR_STRONG=false + MDROFF_ATTR_EMPHASIS=false + MDROFF_IN_TABLE=false + MDROFF_PARAGRAPH=false + + local line + local empty=0 + local empty_continue=0 + while IFS='' read -r line; do + line="$(mdroff:parseln "$line")" + + # Empty + if [[ "$line" =~ ^[[:space:]]*$ ]]; then + ((empty_continue++)) || true + MDROFF_PARAGRAPH=true + MDROFF_IN_TABLE=false + continue + fi + + empty="$empty_continue" + empty_continue=0 + + # Headings + if [[ "$line" =~ ^(#{1,})[[:space:]]{1,}(.*)$ ]]; then + local level="${#BASH_REMATCH[1]}" + local text="${BASH_REMATCH[2]}" + + MDROFF_HEADING_LEVEL="$level" + MDROFF_HEADING="$text" + + mdroff:emit "h${level}" "$text" + MDROFF_PARAGRAPH=true + continue + fi + + # Tables (Partially Supported) + if [[ "$line" =~ ^[[:space:]]*\| ]]; then + local cells=() + line="$(sed 's/^[[:space:]]*|//; s/|[[:space:]]*$//' <<< "$line")" + + # shellcheck disable=SC2206 + IFS='|' cells=($line) + + if [[ "${cells[0]}" =~ ^[[:space:]]*-+[[:space:]]*$ ]]; then + continue + fi + + if ! "$MDROFF_IN_TABLE"; then + MDROFF_IN_TABLE=true + mdroff:emit table_heading "${cells[@]}" + else + mdroff:emit table_row "${cells[@]}" + fi + + MDROFF_PARAGRAPH=true + continue + fi + + mdroff:emit line "$line" + done < <(sed 's/
//') +} + +# ----------------------------------------------------------------------------- +# bat-extras: +# ----------------------------------------------------------------------------- + +mdroff:rewrite:h1() { + emitted_name=false + emitted_description=false + sed 's/^bat-extras: //' <<< "$1" | tr '[[:lower:]]' '[[:upper:]]' +} + + +mdroff:emit_hook:h2() { + case "$MDROFF_HEADING" in + "Installation") return ;; + "Issues?") return ;; + esac + + mdroff:emit:h2 "$(tr '[[:lower:]]' '[[:upper:]]' <<< "$1")" +} + +mdroff:emit_hook:line() { + if [[ "$MDROFF_HEADING_LEVEL" = "1" ]] && [[ "$emitted_name" != true ]]; then + emitted_name=true + printf ".SH NAME\n%s - %s\n" "$(sed 's/^bat-extras: //' <<< "$MDROFF_HEADING" | tr '[[:upper:]]' '[[:lower:]]')" "$1" + printf ".SH DESCRIPTION\n" + return + fi + + case "$MDROFF_HEADING" in + "Installation") return ;; + "Issues?") return ;; + esac + + mdroff:emit:line "$@" +} + +# ----------------------------------------------------------------------------- +# Main: +# ----------------------------------------------------------------------------- + +if [[ "${#BASH_SOURCE[@]}" -eq 1 ]]; then + case "$1" in + "") mdroff ;; + *) { + if ! [ -f "$1" ]; then + printf_err "%s: cannot find or read file %s\n" "$0" "$1" + exit 1; + fi + + mdroff < "$1" + } ;; + esac +fi