#!/usr/bin/env bash
#
# a2x - convert Asciidoc text file to PDF, XHTML, HTML Help, manpage
#                  or plain text
#
# Copyright (C) 2006 Stuart Rackham. Free use of this software is granted
# under the terms of the GNU General Public License (GPL).
#

VERSION=1.0.0
BASENAME=$(basename "$0")
REALNAME="$0"
if [ ! -e "$REALNAME" ]; then
    REALNAME=$(which "$REALNAME")
fi
REALNAME="$(readlink -f "$REALNAME")"
GLOBAL_CONF_DIR=/etc/asciidoc

#--------------------------------------------------------------------
# Constants.
#--------------------------------------------------------------------

# These are mostly related to command options and are set by parse_options().
ASCIIDOC_OPTS=
COPY=no
DESTINATION_DIR=
DOCTYPE=
DRY_RUN=no
FORMAT=xhtml
ICONS=no
ICONS_DIR=./images/icons
SKIP_ASCIIDOC=no
SRC_DIR=
SRC_FILE=
SRC_NAME=           # Source file name sans path and file name extension.
STYLESHEET=./docbook-xsl.css
VERBOSE_2=no
VERBOSE=no
XSLTPROC_OPTS=

#--------------------------------------------------------------------
# General purpose functions
#--------------------------------------------------------------------

# Write $1 to stderr with backslash-escaped characters and no trailing newline.
function write_console()
{
	echo -ne "$1" >&2
}

# Write newline to stderr.
function newline()
{
	echo >&2
}

# Write $1 message to stderr.
function console_msg()
{
	echo "$BASENAME: $1" >&2
}

# Write $1 to stderr if verbose or dry-run options set.
function verbose_msg()
{
    if isyes "$VERBOSE" || isyes "$DRY_RUN"; then
	    console_msg "$1"
    fi
}

# Return 0 if $1 is interpreted as an affirmative string.
function isyes()
{
    case "$1" in
        y|Y|yes|YES|Yes|true|TRUE|True) return 0;;
    esac
    return 1
}

# Log message $1 and exit with status $2 (default 1).
function quit()
{
    local err tmp
    err=${2:-1}
    if [ $err -ne 0 ]; then
        tmp="$VERBOSE"
        VERBOSE=yes     # Force console error exit message.
        console_msg "failed: $1"
        VERBOSE="$tmp"
    else
        console_msg "$1"
    fi
    cleanup
	exit $err
}

# Execute the $1 command in the current shell subject to VERBOSE, DRY_RUN shell
# variables.
function execute_command()
{
    if isyes "$VERBOSE" || isyes "$DRY_RUN"; then
    console_msg "eval: $1"
    fi
    if isyes "$DRY_RUN"; then
        return 0
    else
        eval $1
        return $?
    fi
}

# Same as execute_command() but if error occurs prints optional $2 message and
# exits program.
function execute_command_2()
{
    local msg
    execute_command "$1"
    if [ $? -ne 0 ]; then
        if [ -n "$2" ]; then
            msg="$2"
        else
            msg="$1"
        fi
        quit "$msg"
    fi
}

# Return 127 if $1 is not in search path else return 0.
function require()
{
    if ! which "$1" >/dev/null 2>&1; then
        quit "cannot find required program: $1" 127
    fi
}

# Join path $1 to path $2.
function join()
{
    if [ -n "$1" ]; then
        echo "$1/$2"
    else
        echo "$2"
    fi
}

# Echo the total size in bytes of file name arguments.
function file_size()
{
    echo $(du -cb "$@" | tail -1 | awk '{print $1}')
}

#--------------------------------------------------------------------
# Application specific functions
#--------------------------------------------------------------------

# Trap interrupts.
function set_trap()
{
    # By convention exit code is 128 + signal number.
    trap "newline; quit 'exiting: SIGINT' 130" SIGINT
    trap "newline; quit 'exiting: SIGQUIT' 131" SIGQUIT
    trap "quit 'exiting: SIGHUP' 129" SIGHUP
    trap "quit 'exiting: SIGTERM' 143" SIGTERM
}

# Called at program exit.
function cleanup()
{
    if [ "$(pwd)" != "$PWD" ]; then
        execute_command "cd \"$PWD\""
    fi
}

# Print help summary.
function help()
{
    cat <<EOF
synopsis:

  $BASENAME [OPTIONS] FILE

options:
  --asciidoc-opts=ASCIIDOC_OPTS  asciidoc(1) options
  --copy                         copy icons or HTML stylesheet
  -D, --destination-dir=PATH     output directory (defaults to FILE directory)
  -d, --doctype=DOCTYPE          article, manpage, book
  -f, --format=FORMAT            chunked,htmlhelp,manpage,odt,pdf,text,xhtml
  -h, --help                     print command syntax summary
  --icons                        use admonition and navigation icons
  --icons-dir=PATH               admonition and navigation icon directory
  -n, --dry-run                  don't do anything just print the commands
  -s, --skip-asciidoc            skip asciidoc(1) execution
  --stylesheet=PATH              target HTML CSS stylesheet file name.
  --version                      print program version to stdout
  -v, --verbose                  print operational details to stderr
  --xsltproc-opts=XSLTPROC_OPTS  xsltproc(1) options
EOF
}

# Print full path name of file $1 searching first in the directory containing
# the asciidoc executable and then in the global configuration directory.
function conf_file()
{
    local result dir
    # First look in same directory as asciidoc executable.
    dir="$(dirname "$REALNAME")"
    if [ ! -f "$dir/$1" -a -d $GLOBAL_CONF_DIR ]; then
        dir=$GLOBAL_CONF_DIR
    fi
    result="$dir/$1"
    echo $result
}

#--------------------------------------------------------------------
# Process command-line arguments $@
#--------------------------------------------------------------------
function parse_options()
{
    if [ -z "$*" ]; then
        help; exit 0
    fi
    require "getopt"
    getopt -T >/dev/null
    if [ $? -ne 4 ]; then
        quit "enhanced getopt(1) required"
    fi
    short_opts="d:D:f:hnsv"
    long_opts="asciidoc-opts:,destination-dir:,doctype:,help,icons-dir:,dry-run,format:,copy,icons,skip-asciidoc,stylesheet:,version,verbose,xsltproc-opts:"
    args=$(getopt -o $short_opts -l $long_opts -n $BASENAME -- "$@" 2>/dev/null)
    if [ $? -ne 0 ]; then
        quit "invalid command options, run: a2x --help"
    fi
    eval set -- "$args"     # Set positional variables.
    while true ; do
        case "$1" in
            --asciidoc-opts) 
                ASCIIDOC_OPTS=$2
                shift 2 ;;
            --copy)
                COPY=yes;
                shift ;;
            -d|--doctype)
                DOCTYPE=$2
                shift 2 ;;
            -D|--destination-dir)
                DESTINATION_DIR=$2
                shift 2 ;;
            -f|--format)
                FORMAT=$2
                shift 2 ;;
            -h|--help)
                help; exit 0 ;;
            --icons)
                ICONS=yes;
                shift ;;
            --icons-dir)
                ICONS_DIR=$2
                shift 2 ;;
            -n|--dry-run)
                DRY_RUN=yes;
                shift ;;
            -s|--skip-asciidoc)
                SKIP_ASCIIDOC=yes;
                shift ;;
            --stylesheet)
                STYLESHEET=$2
                shift 2 ;;
            --version)
                echo "$BASENAME $VERSION" ; exit 0 ;;
            -v|--verbose) 
                if isyes "$VERBOSE"; then
                    VERBOSE_2=yes
                else
                    VERBOSE=yes
                fi
                shift ;;
            --xsltproc-opts) 
                XSLTPROC_OPTS=$2
                shift 2 ;;
            --)
                shift; break ;;
            *)
                quit "unrecognized option: $1" ;;
        esac
    done

    if isyes "$DRY_RUN"; then
        VERBOSE=yes
    fi

    if [ $# -eq 0 ]; then
        quit "source file not specified"
    fi
    if [ $# -ne 1 ]; then
        quit "only one source file allowed"
    fi
    if [ ! -r "$1" ]; then
        quit "source file not found: $1"
    fi
    SRC_FILE=$1
    SRC_DIR=$(dirname "$1")
    SRC_NAME=$1
    SRC_NAME=${SRC_NAME##*/}    # Strip path.
    SRC_NAME=${SRC_NAME%.*}     # Strip extension.
}

#--------------------------------------------------------------------
# Validate program options.
#--------------------------------------------------------------------
function validate_options()
{
    case "$FORMAT" in
        chunked|htmlhelp|manpage|odt|pdf|text|xhtml) ;;
        *) quit "illegal format: $FORMAT" ;;
    esac

    if [ -z "$DOCTYPE" ]; then
        if [ "$FORMAT" = "manpage" ]; then
            DOCTYPE=manpage
        else
            DOCTYPE=article
        fi
    fi
    case "$DOCTYPE" in
        article|book|manpage) ;;
        *) quit "illegal doctype: $DOCTYPE" ;;
    esac

    if [ -z "$ICONS_DIR" ]; then
        quit "icons directory not specified"
    fi
    if [[ "$ICONS_DIR" == /* ]]; then
        quit "icons directory must be relative: $ICONS_DIR"
    fi
    ICONS_DIR=${ICONS_DIR%*/}     # Strip trailing backslash.

    if [ ! -z "$DESTINATION_DIR" ]; then
        if [ ! -d "$DESTINATION_DIR" ]; then
            quit "destination directory not found: $DESTINATION_DIR"
        fi
    else
        DESTINATION_DIR="$SRC_DIR"
    fi

    if [ -z "$STYLESHEET" ]; then
        quit "stylesheet cannot be blank"
    fi
    if [[ "$STYLESHEET" == /* ]]; then
        quit "stylesheet path must be relative: $STYLESHEET"
    fi
}

# Conditionally copy distribution stylesheet and admonition and navigation
# icons to destination directory $1.
function copy_stylesheet_and_icons()
{
    if isyes $COPY; then
        copy_stylesheet "$1"
        if isyes $ICONS; then
            copy_icons "$1/$ICONS_DIR"
        fi
    fi
}

# Copy distribution stylesheet to destination directory $1.
function copy_stylesheet()
{
    local src dst
    src=$(conf_file stylesheets/docbook-xsl.css)
    if [ ! -r "$src" ]; then
        quit "file not found: $src"
    fi
    dst="$1/$STYLESHEET"
    # Check we're not trying to copy the file onto itself.
    if [[ "$src" -ef "$dst" ]]; then
        return
    fi
    execute_command_2 "cp -u \"$src\" \"$dst\""
}

# Copy distribution admonition and navigation icons to destination directory
# $1.
function copy_icons()
{
    local src dst
    dst="$1"
    # Set source icons directory.
    src=$(conf_file images/icons/home.png)
    if [ ! -r "$src" ]; then
        quit "file not found: $src"
    fi
    src=$(dirname "$src")
    # Check we're not trying to copy the file onto itself.
    if [[ "$src" -ef "$dst" ]]; then
        return
    fi
    if [ -e "$dst" ]; then
        if [ ! -d "$dst" ]; then
            quit "icon destination must be a directory: $dst"
        fi
    else
        execute_command_2 "mkdir -p \"$dst\""
    fi
    execute_command_2 "cp -rfu  \"$src/\"* \"$dst\""
}

#--------------------------------------------------------------------
# Format conversion functions.
#--------------------------------------------------------------------

# Convert AsciiDoc $SRC_FILE to DocBook XML if it is newer than the
# XML output file.  $1 has additional asciidoc(1) options.
function to_docbook()
{
    local xml
    xml="$SRC_DIR/$SRC_NAME.xml"
    if isyes $SKIP_ASCIIDOC; then
        if [ ! -r "$xml" ]; then
            quit "file not found: $xml"
        fi
        return
    fi
    require "asciidoc"
    execute_command_2 "asciidoc $ASCIIDOC_OPTS $1 -b docbook \"$SRC_FILE\""
}

function to_xhtml()
{
    require "xsltproc"
    local xsl xml html
    xsl=$(conf_file docbook-xsl/xhtml.xsl)
    if [ ! -r "$xsl" ]; then
        quit "file not found: $xsl"
    fi
    to_docbook
    xml=$(readlink -f "$SRC_DIR/$SRC_NAME.xml")
    html="$SRC_NAME.html"
    copy_stylesheet_and_icons "$DESTINATION_DIR"
    execute_command_2 "cd \"$DESTINATION_DIR\""
    execute_command_2 "xsltproc $XSLTPROC_OPTS --nonet \
        \"$xsl\" \"$xml\" >\"$html\""
    execute_command_2 "cd - >/dev/null"
}

function to_chunked()
{
    require "xsltproc"
    local chunkdir xsl xml hhp chm
    case "$FORMAT" in
        chunked)
            chunkdir="$DESTINATION_DIR/$SRC_NAME.chunked"
            xsl=chunked.xsl
        ;;
        htmlhelp)
            chunkdir="$DESTINATION_DIR/$SRC_NAME.htmlhelp"
            hhp="$SRC_NAME.hhp"
            chm="$SRC_NAME.chm"
            XSLTPROC_OPTS="$XSLTPROC_OPTS \
                --stringparam htmlhelp.hhp \"$hhp\"
                --stringparam htmlhelp.chm \"$chm\""
            xsl=htmlhelp.xsl
        ;;
    esac
    xsl=$(conf_file docbook-xsl/$xsl)
    if [ ! -r "$xsl" ]; then
        quit "file not found: $xsl"
    fi
    to_docbook
    xml=$(readlink -f "$SRC_DIR/$SRC_NAME.xml")
    if [ ! -d "$chunkdir" ]; then
        execute_command_2 "mkdir \"$chunkdir\""
    fi
    execute_command_2 "rm -f \"$chunkdir/*.html\""
    copy_stylesheet_and_icons "$chunkdir"
    execute_command_2 "cd \"$DESTINATION_DIR\""
    execute_command_2 "xsltproc $XSLTPROC_OPTS --nonet \
        --stringparam base.dir \"$(basename "$chunkdir")/\" \
        \"$xsl\" \"$xml\""
    execute_command_2 "cd - >/dev/null"
}

function to_manpage()
{
    require "xsltproc"
    local xsl xml
    xsl=$(conf_file docbook-xsl/manpage.xsl)
    if [ ! -r "$xsl" ]; then
        quit "file not found: $xsl"
    fi
    to_docbook "-d manpage"
    xml=$(readlink -f "$SRC_DIR/$SRC_NAME.xml")
    execute_command_2 "cd \"$DESTINATION_DIR\""
    execute_command_2 "xsltproc $XSLTPROC_OPTS --nonet \
        \"$xsl\" \"$xml\""
    execute_command_2 "cd - >/dev/null"
}

function to_pdf()
{
    require "xsltproc"
    require "fop.sh"
    local xsl xml fo pdf
    xsl=$(conf_file docbook-xsl/fo.xsl)
    if [ ! -r "$xsl" ]; then
        quit "file not found: $xsl"
    fi
    xml="$SRC_DIR/$SRC_NAME.xml"
    fo="$SRC_DIR/$SRC_NAME.fo"
    pdf="$DESTINATION_DIR/$SRC_NAME.pdf"
    to_docbook
    execute_command_2 "xsltproc $XSLTPROC_OPTS --nonet \
        \"$xsl\" \"$xml\" >\"$fo\""
    execute_command_2 "fop.sh \"$fo\" \"$pdf\""
}

function to_odt()
{
    require "docbook2odf"
    local xml odt opts
    xml="$SRC_DIR/$SRC_NAME.xml"
    odt="$DESTINATION_DIR/$SRC_NAME.odt"
    opts="--force"
    if ! isyes $VERBOSE; then
        opts="$opts --quiet"
    fi
    to_docbook
    execute_command_2 "docbook2odf $opts --input-file \"$xml\" --output-file \"$odt\""
}

function to_text()
{
    require "asciidoc"
    require "lynx"
    local html text conf
    html="$SRC_DIR/$SRC_NAME.html"
    text="$DESTINATION_DIR/$SRC_NAME.text"
    conf=$(conf_file text.conf)
    execute_command_2 "asciidoc $ASCIIDOC_OPTS -f "$conf" -b html4 \
        -o - \"$SRC_FILE\" | lynx -dump -stdin >\"$text\""
}

#--------------------------------------------------------------------
# Main
#--------------------------------------------------------------------

PWD=`pwd`
set_trap
parse_options "$@"
validate_options
ASCIIDOC_OPTS="--doctype=$DOCTYPE $ASCIIDOC_OPTS"
if isyes $VERBOSE_2; then
    ASCIIDOC_OPTS="$ASCIIDOC_OPTS --verbose"
    XSLTPROC_OPTS="$XSLTPROC_OPTS --verbose"
fi
case "$FORMAT" in
    xhtml|chunked|htmlhelp)
        XSLTPROC_OPTS="$XSLTPROC_OPTS \
            --stringparam html.stylesheet \"$STYLESHEET\""
    ;;
esac
if isyes $ICONS; then
    XSLTPROC_OPTS="$XSLTPROC_OPTS --stringparam callout.graphics 1 \
        --stringparam navig.graphics 0 \
        --stringparam admon.textlabel 0 \
        --stringparam admon.graphics 1 \
        --stringparam admon.graphics.path \"$ICONS_DIR/\" \
        --stringparam callout.graphics.path \"$ICONS_DIR/callouts/\" \
        --stringparam navig.graphics.path \"$ICONS_DIR/\""
else
    XSLTPROC_OPTS="$XSLTPROC_OPTS --stringparam callout.graphics 0 \
        --stringparam navig.graphics 0 \
        --stringparam admon.textlabel 1 \
        --stringparam admon.graphics 0"
fi
case "$FORMAT" in
    chunked|htmlhelp)   to_chunked;;
    manpage)            to_manpage;;
    odt)                to_odt;;
    pdf)                to_pdf;;
    text)               to_text;;
    xhtml)              to_xhtml;;
esac
cleanup

#--------------------------------------------------------------------
# vim: set et ts=4 sw=4 sts=4:
#--------------------------------------------------------------------