#!/bin/bash
# dev/docs/ref - package source reference support

set -e

lib_load 'dev/docs/rst'


######
# Reference Manual Configuration

dev_docs_ref_config_init() {
	# $docs_note_types[] - List of known documentation note types
	lib_setting_arrays docs_note_types
	docs_note_types=( returns default example fixme todo )
	docs_note_types+=( "${rst_admonitions[@]}" )

	# $docs_last_line - Stores the last generated line of documentation.
	# Used internally to minimize spurious whitespace.
	lib_setting_vars --null docs_last_line
}


######
# Note Sections
#
# A `note section` begins with a line that starts with a known note
# keyword.  The following pedantic example demonstrates a `returns` note
# section as it would appear the source code.  As the name suggestions,
# this note section specifies a return value for a function:
#
#   ``# Returns: Success``
#
# This note section will be rendered as:
#
#   Returns: Success
#
# Each type of note section may impose additional documentation syntax
# requirements or override the format for outputting the section contents.
#
# See ``$docs_note_types[]`` for the list of built-in note types.

# docs_note_keyword() - Prints the canonical keyword of a note section
# on the current line ($1).  If no known keyword is found, nothing is
# output.
docs_note_keyword() {
	local line=$(str_trim "$1")
	[ "${line/: /}" != "$line" ] || return 0

	local -l tag=${line%%: *}
	[ "${tag// /}" = "$tag" ] || return 0

	echo "$tag"
}

# is_docs_note() - Returns success if line ($1) begins with a known
# note section keyword.
is_docs_note() {
	local keyword="$(docs_note_keyword "$1")"
	[ "$keyword" ] && in_list "$keyword" "${docs_note_types[@]}"
}

# docs_gen_file_note() - Begins a note section that starts on the given
# line ($1), formatting the output according the rules defined by the
# type of note section.
docs_gen_file_note() {
	local line=$1
	local keyword=${line%%: *}
	local desc="${line#*: }"

	docs_lines_cr

	local -l ntype=$keyword
	case "$ntype" in
	(*) docs_gen_file_note_default "$keyword" "$desc" ;;
	esac
}

# docs_gen_file_note_default() - Prints a note section using a
# default format.
docs_gen_file_note_default() {
	local keyword=$1
	keyword=$(str_trim "$keyword")
	local indent=${1%$keyword*}
	docs_lines_add "$indent$(rst_bold "$keyword"): $2"
}


######
# Feature Detection

# is_docs_section() - Returns success if the given line ($1) is the
# start of a new section.
is_docs_section() { [ "$1" = '#####' ]; }

# is_docs_symbol() - Returns success if the given line ($1) is a
# documentated symbol.
is_docs_symbol() { [ "${1/ - /}" != "$1" -a -n "$(str_trim "${1%% - *}")" ]; }

# is_docs_arg() - Returns success if the given line ($1) is a
# documentated function argument.
is_docs_arg() { [ "${1/\$[0-9\@] - /}" != "$1" -o "${1#... - }" != "$1" ]; }


######
# Source Line Processing

# docs_readline() - Reads a line into the named var ($1)
# $1 - Variable name to receive line of documentation
# Returns: If a line could be read, returns success.
docs_readline() {
	local var=$1
	local -n value=$var
	local ignoring=false
	while true; do
		read $var || return
		[ "$value" ] || continue

		if $ignoring; then
			[ "$value" != '#+++' ] || ignoring=false
			continue
		elif [ "$value" = '#---' ]; then
			ignoring=true
			continue
		fi

		if [ "${value#\#}" != "$value" ]; then
			value=${value#\#}
			#; ignore comments like this one
			[ "${value:0:1}" != ';' ] || continue
			value=${value##[ \t\n]}
		else
			value=''
		fi
		break
	done
}

######
# Output  Generation

# docs_lines_add() - Caches lines ($@) of documentation for later generation.
docs_lines_add() { docs_lines+=( "$@" ); }

# docs_lines_cr() - Caches a blank line of documentation.
docs_lines_cr() { docs_lines_add ''; }

# docs_lines_here() - Caches lines of documentation from standard input.
docs_lines_here() {
	local -a lines
	readarray -t lines
	docs_lines_add "${lines[@]}"
}

# docs_lines_func() - Caches lines of documentation generated from
# the given command ($@).
docs_lines_func() {
	min_args 1 "$@"
	local tmp
	file_capture_func tmp "$@"
	docs_lines_here <"$tmp"
}

# docs_gen_lines() - Prints lines of documentation that have been cached
# via ``docs_lines_add()`` and friends.  Sequences of multiple blank
# lines are reduced to a single line.  This allows generation functions
# to be liberal when emitting whitespace between successive blocks of lines.
docs_gen_lines() {
	[ ${#docs_lines[*]} -gt 0 ] || return 0
	local line
	for line in "${docs_lines[@]}"; do
		[ "$line" -o "$docs_last_line" ] || continue
		echo "$line"
		docs_last_line=$line
	done
	docs_lines=()
}


######
# Package Reference Documentation

package_docs_ref_clean() { run rm -rf "$package_docs_refdir"; }

docs_gen_files() {
	local -a files=( "$@" )
	[ "$*" ] || docs_file_list files src conf libs

	run_mkdir "$package_docs_refdir"
	for_each docs_gen_file "${files[@]}"
}

docs_gen_file() {
	local file=$1
	local base=${name%.in}

	local out=$(docs_filename "$package_docs_refdir" "$file" rst)
	file_mkdir "$out"

	local ext=''
	[ "${file/conf\//}" != "$file" ] || ext='.sh'

	local in="$package_gendir/${file/src\//bin\/}$ext"

	local name=${file#*/}
	info "$name: generating documentation..."
	if ! docs_gen_file_body "$file" <"$in" >"$out"; then
		error "$name: error generating documentation"
	fi
}


######
# Source File Processing

docs_gen_file_body() {
	local file=$1
	local name=${file#*/}

	local docs_cur_state=sec
	local -a docs_lines

	local line
	docs_readline line

	if [ "!/bin/bash" = "$line" ]; then
		# discard shebang line, read next
		docs_readline line
	else
		warn "$name: $line: first line should be a shebang"
	fi

	docs_gen_file_title "$line"
	docs_gen_file_header "$name"
	docs_gen_file_deps "$file"
	docs_gen_file_sections "$name"
}

docs_gen_file_title() {
	local line=$1
	local title=${line% - *}
	local desc=${line#* - }
	docs_lines_func rst_title "$name" "$desc"
	docs_gen_lines
}

docs_gen_file_header() {
	local name=$1

	local line
	while docs_readline line; do
		if ! is_docs_section "$line"; then
			docs_lines_add "$line"
			continue
		fi

		local -a desc
		file_array_func desc docs_gen_lines
		if [ "${desc[*]}" ]; then
			rst_h1 "Script Overview"
			for_each echo "${desc[@]}"
		fi

		docs_cur_state='sec'
		break
	done
}

docs_gen_file_deps() {
	local file=$1

	local name=${file#*/}
	local base=${file##*/}
	local svg="$base.deps.svg"

	docs_lines_cr

	local tmp
	file_capture_func tmp docs_gen_file_deps_body "$file"
	file_read_func "$tmp" docs_lines_func rst_figure "$svg" \
		':align:' right ':alt:' "Dependency Graph" ':width:' "100%"
}

docs_gen_file_deps_body() {
	local file=$1
	local dotfile=$(docs_filename "$package_docs_refdir" "$file" deps.dot)

	local name=${file#*/}
	local base=${file##*/}
	local dot="$base.deps.dot"
	local svg="$base.deps.svg"
	local pdf="$base.deps.pdf"
	local rst="$base.rst"
	local raw="$base.raw.html"

	echo "$(rst_literal "$name"): Library Dependencies"

	if [ -s "$dotfile" ]; then
		cat <<GRAPH

View the full-size SVG (svg_) or PDF (pdf_) image or
graphviz source (dot_)

.. _svg: $svg
.. _pdf: $pdf
.. _dot: $dot
GRAPH
		echo
	fi

	cat <<RST

View color source code (raw_) for this file

.. _raw: $raw

View reStructuredText (rst_) source code for this page

.. _rst: $rst

RST
}


######
# Sections/Symbols/Arguments

docs_gen_file_sections() {
	local name=$1

	local docs_cur_section
	local docs_cur_symbol

	local line
	while docs_readline line; do
		case "$docs_cur_state" in
		(top|arg|note|sym)
			docs_gen_lines
			if is_docs_section "$line"; then
				docs_cur_section=''
				docs_cur_symbol=''
				docs_cur_state='sec'
			elif is_docs_arg "$line"; then
				docs_gen_file_sym_arg "$line"
				docs_cur_state='arg'
			elif is_docs_note "$line"; then
				docs_gen_file_note "$line"
				docs_cur_state='note'
			elif is_docs_symbol "$line"; then
				docs_gen_file_sym "$line"
				docs_cur_symbol="$line"
				docs_cur_state='sym'
			elif [ "$docs_cur_section" ]; then
				docs_lines_add "$line"
			elif [ "$line" ]; then
				warn "ignoring: $line"
			fi
			;;
		(sec)
			line=$(rst_trim "$line")
			docs_gen_file_section 1 "$line"
			docs_cur_section=$line
			docs_cur_state='top'
			;;
		(*)
			error "$state: unknown generator state"
			;;
		esac
	done
	docs_gen_lines
}

docs_gen_file_section() {
	min_args 2 "$@"
	local level=$1
	local title=$2
	local desc=$3

	docs_lines_cr

	title=$(rst_trim "$title")
	docs_lines_func rst_header_n $level "$title"
	desc=$(rst_trim "$desc")
	if [ "$desc" ]; then
		docs_lines_func rst_reflow "$desc"
	fi
}

docs_gen_file_sym() {
	local title="${1%% - *}"
	title=$(str_trim "$title")
	title=$(rst_code "$title")
	local desc="${1#* - }"

	docs_lines_cr
	docs_gen_file_section 2 "$title" "$desc"
}

docs_gen_file_sym_arg() {
	local name="${1%% - *}"
	name=$(str_trim "$name")
	name=$(rst_bold "$name")
	local desc="${1#* - }"

	docs_lines_cr
	docs_lines_add "$name - $desc"
}


######
# HTML Documentation

docs_gen_html_files() {
	local -a files=( "$@" )
	[ "$*" ] || docs_file_list files libs src conf
	for_each docs_gen_html_file "${files[@]}"
}

docs_gen_html_file() {
	local file=$1
	local base=${file%.sh}
	local rst=$(docs_filename "$package_docs_refdir" "$base" rst)

	local tmp
	tmp=$(cmd_tempfile)
	{
		docs_rst_to_html "$rst"
		echo

		docs_gen_html_page_footer
	} >"$tmp"

	local html=$(docs_filename "$package_docs_refdir" "$base" html)
	run mv "$tmp" "$html"
}

View the Developer Guide Index

View the Reference Manual Index


Generated on Fri Jul 28 14:35:13 PDT 2017 by mcsh d14 v0.23.0.