#
# buildhelp.tcl --
#
# Program to extract help files from TCL manual pages or TCL script files.
# The help directories are built as a hierarchical tree of subjects and help
# files.  
# 
#------------------------------------------------------------------------------
# Copyright 1992-1999 Karl Lehenbauer and Mark Diekhans.
#
# Permission to use, copy, modify, and distribute this software and its
# documentation for any purpose and without fee is hereby granted, provided
# that the above copyright notice appear in all copies.  Karl Lehenbauer and
# Mark Diekhans make no representations about the suitability of this
# software for any purpose.  It is provided "as is" without express or
# implied warranty.
#------------------------------------------------------------------------------
# $Id: buildhelp.tcl,v 1.1 2001/10/24 23:31:48 hobbs Exp $
#------------------------------------------------------------------------------
#
# For nroff man pages, the areas of text to extract are delimited with:
#
#     '\"@help: subjectdir/helpfile
#     '\"@endhelp
#
# start in column one. The text between these markers is extracted and stored
# in help/subjectdir/help.  The file must not exists, this is done to enforced 
# cleaning out the directories before help file generation is started, thus
# removing any stale files.  The extracted text is run through:
#
#     nroff -man|col -xb   {col -b on BSD derived systems}
#
# If there is other text to include in the helpfile, but not in the manual 
# page, the text, along with nroff formatting commands, may be included using:
#
#     '\"@:Other text to include in the help page.
#
# A entry in the brief file, used by apropos my be included by:
#
#     '\"@brief: Short, one line description
#
# These brief request must occur with in the bounds of a help section.
#
# If some header text, such as nroff macros, need to be preappended to the
# text streem before it is run through nroff, then that text can be bracketed
# with:
#
#     '\"@header
#     '\"@endheader
#
# If multiple header blocks are encountered, they will all be preappended.
#
# For TCL script files, which are indentified because they end in ".tcl",
# the text to be extracted is delimited by:
#
#    #@help: subjectdir/helpfile
#    #@endhelp
#
# And brief lines are in the form:
#
#     #@brief: Short, one line description
#
# The only processing done on text extracted from .tcl files it to replace
# the # in column one with a space.
#
#
#-----------------------------------------------------------------------------
# 
# To generate help:
#
#   buildhelp helpDir brief.brf filelist
#
# o helpDir is the help tree root directory.  helpDir should  exists, but any
#   subdirectories that don't exists will be created.  helpDir should be
#   cleaned up before the start of manual page generation, as this program
#   will not overwrite existing files.
# o brief.brf  is the name of the brief file to create form the @brief entries.
#   It must have an extension of ".brf".  It will be created in helpDir.
# o filelist are the nroff manual pages, or .tcl, .tlib files to extract
#   the help files from. If the suffix is not .tcl or .tlib, a nroff manual
#   page is assumed.
#
#-----------------------------------------------------------------------------

#@package: TclX-buildhelp buildhelp

#-----------------------------------------------------------------------------
# Truncate a file name of a help file if the system does not support long
# file names.  If the name starts with `Tcl_', then this prefix is removed.
# If the name is then over 14 characters, it is truncated to 14 charactes
#  
proc TruncFileName {pathName} {
    global truncFileNames

    if {!$truncFileNames} {
        return $pathName}
    set fileName [file tail $pathName]
    if {"[crange $fileName 0 3]" == "Tcl_"} {
        set fileName [crange $fileName 4 end]}
    set fileName [crange $fileName 0 13]
    return "[file dirname $pathName]/$fileName"
}

#-----------------------------------------------------------------------------
# Proc to ensure that all directories for the specified file path exists,
# and if they don't create them.  Don't use -path so we can set the
# permissions.

proc EnsureDirs {filePath} {
    set dirPath [file dirname $filePath]
    if [file exists $dirPath] return
    foreach dir [split $dirPath /] {
        lappend dirList $dir
        set partPath [join $dirList /]
        if [file exists $partPath] continue

        mkdir $partPath
        chmod u=rwx,go=rx $partPath
    }
}

#-----------------------------------------------------------------------------
# Proc to set up scan context for use by FilterNroffManPage.
# This keeps the a two line cache of the previous two lines encountered
# and the blank lines that followed them.
#

proc CreateFilterNroffManPageContext {} {
    global filterNroffManPageContext

    set filterNroffManPageContext [scancontext create]

    # On finding a page header, drop the previous line (which is
    # the page footer). Also deleting the blank lines followin
    # the last line on the previous page.

    scanmatch $filterNroffManPageContext {@@@BUILDHELP@@@} {
        catch {unset prev2Blanks}
        catch {unset prev1Line}
        catch {unset prev1Blanks}
        set nukeBlanks {}
    }

    # Save blank lines

    scanmatch $filterNroffManPageContext {$^} {
        if ![info exists nukeBlanks] {
            append prev1Blanks \n
        }
    }

    # Non-blank line, save it.  Output the 2nd previous line if necessary.

    scanmatch $filterNroffManPageContext {
        catch {unset nukeBlanks}
        if [info exists prev2Line] {
            puts $outFH $prev2Line
            unset prev2Line
        }
        if [info exists prev2Blanks] {
            puts $outFH $prev2Blanks nonewline
            unset prev2Blanks
        }
        if [info exists prev1Line] {
            set prev2Line $prev1Line
        }
        set prev1Line $matchInfo(line)
        if [info exists prev1Blanks] {
            set prev2Blanks $prev1Blanks
            unset prev1Blanks
        }
    }
}

#-----------------------------------------------------------------------------
# Proc to filter a formatted manual page, removing the page headers and
# footers.  This relies on each manual page having a .TH macro in the form:
#   .TH @@@BUILDHELP@@@ n

proc FilterNroffManPage {inFH outFH} {
    global filterNroffManPageContext

    if ![info exists filterNroffManPageContext] {
        CreateFilterNroffManPageContext
    }

    scanfile $filterNroffManPageContext $inFH

    if [info exists prev2Line] {
        puts $outFH $prev2Line
    }
}

#-----------------------------------------------------------------------------
# Proc to set up scan context for use by ExtractNroffHeader
#

proc CreateExtractNroffHeaderContext {} {
    global extractNroffHeaderContext

    set extractNroffHeaderContext [scancontext create]

    scanmatch $extractNroffHeaderContext {'\\"@endheader[ 	]*$} {
        break
    }
    scanmatch $extractNroffHeaderContext {'\\"@:} {
        append nroffHeader "[crange $matchInfo(line) 5 end]\n"
    }
    scanmatch $extractNroffHeaderContext {
        append nroffHeader "$matchInfo(line)\n"
    }
}

#-----------------------------------------------------------------------------
# Proc to extract nroff text to use as a header to all pass to nroff when
# processing a help file.
#    manPageFH - The file handle of the manual page.
#

proc ExtractNroffHeader {manPageFH} {
    global extractNroffHeaderContext nroffHeader

    if ![info exists extractNroffHeaderContext] {
        CreateExtractNroffHeaderContext
    }
    scanfile $extractNroffHeaderContext $manPageFH
}


#-----------------------------------------------------------------------------
# Proc to set up scan context for use by ExtractNroffHelp
#

proc CreateExtractNroffHelpContext {} {
    global extractNroffHelpContext

    set extractNroffHelpContext [scancontext create]

    scanmatch $extractNroffHelpContext {^'\\"@endhelp[ 	]*$} {
        break
    }

    scanmatch $extractNroffHelpContext {^'\\"@brief:} {
        if $foundBrief {
            error {Duplicate "@brief:" entry}
        }
        set foundBrief 1
        puts $briefHelpFH "$helpName\t[csubstr $matchInfo(line) 11 end]"
        continue
    }

    scanmatch $extractNroffHelpContext {^'\\"@:} {
        puts $nroffFH  [csubstr $matchInfo(line) 5 end]
        continue
    }
    scanmatch $extractNroffHelpContext {^'\\"@help:} {
        error {"@help" found within another help section"}
    }
    scanmatch $extractNroffHelpContext {
        puts $nroffFH $matchInfo(line)
    }
}

#-----------------------------------------------------------------------------
# Proc to extract a nroff help file when it is located in the text.
#    manPageFH - The file handle of the manual page.
#    manLine - The '\"@help: line starting the data to extract.
#

proc ExtractNroffHelp {manPageFH manLine} {
    global helpDir nroffHeader briefHelpFH colArgs
    global extractNroffHelpContext

    if ![info exists extractNroffHelpContext] {
        CreateExtractNroffHelpContext
    }

    set helpName [string trim [csubstr $manLine 9 end]]
    set helpFile [TruncFileName "$helpDir/$helpName"]
    if [file exists $helpFile] {
        error "Help file already exists: $helpFile"
    }
    EnsureDirs $helpFile

    set tmpFile "[file dirname $helpFile]/tmp.[id process]"

    echo "    creating help file $helpName"

    set nroffFH [open "| nroff -man | col $colArgs > $tmpFile" w]

    puts $nroffFH {.TH @@@BUILDHELP@@@ 1}

    set foundBrief 0
    scanfile $extractNroffHelpContext $manPageFH

    # Close returns an error on if anything comes back on stderr, even if
    # its a warning.  Output errors and continue.

    set stat [catch {
        close $nroffFH
    } msg]
    if $stat {
        puts stderr "nroff: $msg"
    }

    set tmpFH [open $tmpFile r]
    set helpFH [open $helpFile w]

    FilterNroffManPage $tmpFH $helpFH

    close $tmpFH
    close $helpFH

    unlink $tmpFile
    chmod a-w,a+r $helpFile
}

#-----------------------------------------------------------------------------
# Proc to set up scan context for use by ExtractScriptHelp
#

proc CreateExtractScriptHelpContext {} {
    global extractScriptHelpContext

    set extractScriptHelpContext [scancontext create]

    scanmatch $extractScriptHelpContext {^#@endhelp[ 	]*$} {
        break
    }

    scanmatch $extractScriptHelpContext {^#@brief:} {
        if $foundBrief {
            error {Duplicate "@brief" entry}
        }
        set foundBrief 1
        puts $briefHelpFH "$helpName\t[csubstr $matchInfo(line) 9 end]"
        continue
    }

    scanmatch $extractScriptHelpContext {^#@help:} {
        error {"@help" found within another help section"}
    }
 
    scanmatch $extractScriptHelpContext {^#$} {
        puts $helpFH ""
    }

    scanmatch $extractScriptHelpContext {
        if {[clength $matchInfo(line)] > 1} {
            puts $helpFH " [csubstr $matchInfo(line) 1 end]"
        } else {
            puts $helpFH $matchInfo(line)
        }
    }
}

#-----------------------------------------------------------------------------
# Proc to extract a tcl script help file when it is located in the text.
#    ScriptPageFH - The file handle of the .tcl file.
#    ScriptLine - The #@help: line starting the data to extract.
#

proc ExtractScriptHelp {scriptPageFH scriptLine} {
    global helpDir briefHelpFH
    global extractScriptHelpContext

    if ![info exists extractScriptHelpContext] {
        CreateExtractScriptHelpContext
    }

    set helpName [string trim [csubstr $scriptLine 7 end]]
    set helpFile "$helpDir/$helpName"
    if {[file exists $helpFile]} {
        error "Help file already exists: $helpFile"
    }
    EnsureDirs $helpFile

    echo "    creating help file $helpName"

    set helpFH [open $helpFile w]

    set foundBrief 0
    scanfile $extractScriptHelpContext $scriptPageFH

    close $helpFH
    chmod a-w,a+r $helpFile
}

#-----------------------------------------------------------------------------
# Proc to scan a nroff manual file looking for the start of a help text
# sections and extracting those sections.
#    pathName - Full path name of file to extract documentation from.
#

proc ProcessNroffFile {pathName} {
   global nroffScanCT scriptScanCT nroffHeader

   set fileName [file tail $pathName]

   set nroffHeader {}
   set manPageFH [open $pathName r]
   set matchInfo(fileName) [file tail $pathName]

   echo "    scanning $pathName"

   scanfile $nroffScanCT $manPageFH

   close $manPageFH
}

#-----------------------------------------------------------------------------
# Proc to scan a Tcl script file looking for the start of a
# help text sections and extracting those sections.
#    pathName - Full path name of file to extract documentation from.
#

proc ProcessTclScript {pathName} {
   global scriptScanCT nroffHeader

   set scriptFH [open "$pathName" r]
   set matchInfo(fileName) [file tail $pathName]

   echo "    scanning $pathName"
   scanfile $scriptScanCT $scriptFH

   close $scriptFH
}

#-----------------------------------------------------------------------------
# build: main procedure.  Generates help from specified files.
#    helpDirPath - Directory were the help files go.
#    briefFile - The name of the brief file to create.
#    sourceFiles - List of files to extract help files from.

proc buildhelp {helpDirPath briefFile sourceFiles} {
    global helpDir truncFileNames nroffScanCT
    global scriptScanCT briefHelpFH colArgs

    echo ""
    echo "Begin building help tree"

    # Determine version of col command to use (no -x on BSD)
    if {[system {col -bx </dev/null >/dev/null 2>&1}] != 0} {
        set colArgs {-b}
    } else {
        set colArgs {-bx}
    }
    set helpDir $helpDirPath
    if {![file exists $helpDir]} {
        mkdir $helpDir
    }

    if {![file isdirectory $helpDir]} {
        error [concat "$helpDir is not a directory or does not exist. "  
                      "This should be the help root directory"]
    }
        
    set status [catch {set tmpFH [open $helpDir/AVeryVeryBigFileName w]}]
    if {$status != 0} {
        set truncFileNames 1
    } else {
        close $tmpFH
        unlink $helpDir/AVeryVeryBigFileName
        set truncFileNames 0
    }

    set nroffScanCT [scancontext create]

    scanmatch $nroffScanCT {'\\"@help:} {
        ExtractNroffHelp $matchInfo(handle) $matchInfo(line)
        continue
    }

    scanmatch $nroffScanCT {^'\\"@header} {
        ExtractNroffHeader $matchInfo(handle)
        continue
    }
    scanmatch $nroffScanCT {^'\\"@endhelp} {
        error [concat {@endhelp" without corresponding "@help:"} \
                 ", offset = $matchInfo(offset)"]
    }
    scanmatch $nroffScanCT {^'\\"@brief} {
        error [concat {"@brief" without corresponding "@help:"} \
                 ", offset = $matchInfo(offset)"]
    }

    set scriptScanCT [scancontext create]
    scanmatch $scriptScanCT {^#@help:} {
        ExtractScriptHelp $matchInfo(handle) $matchInfo(line)
    }

    if {[file extension $briefFile] != ".brf"} {
        error "Brief file \"$briefFile\" must have an extension \".brf\""
    }
    if [file exists $helpDir/$briefFile] {
        error "Brief file \"$helpDir/$briefFile\" already exists"
    }
    set briefHelpFH [open "|sort > $helpDir/$briefFile" w]

    foreach manFile [glob $sourceFiles] {
        set ext [file extension $manFile]
        if {$ext == ".tcl" || $ext == ".tlib"} {
            set status [catch {ProcessTclScript $manFile} msg]
        } else {
            set status [catch {ProcessNroffFile $manFile} msg]
        }
        if {$status != 0} {
            global errorInfo errorCode
            error "Error extracting help from: $manFile" $errorInfo $errorCode
        }
    }

    close $briefHelpFH
    chmod a-w,a+r $helpDir/$briefFile
    echo "Completed extraction of help files"
}



