# doctoc.tcl -- # # Implementation of doctoc objects for Tcl. # # Copyright (c) 2003-2009 Andreas Kupries # # See the file "license.terms" for information on usage and redistribution # of this file, and for a DISCLAIMER OF ALL WARRANTIES. # # RCS: @(#) $Id: doctoc.tcl,v 1.21 2009/07/23 17:03:51 andreas_kupries Exp $ package require Tcl 8.2 package require textutil::expander # @mdgen OWNER: api_toc.tcl # @mdgen OWNER: checker_toc.tcl # @mdgen OWNER: mpformats/*.tcl # @mdgen OWNER: mpformats/*.msg # @mdgen OWNER: mpformats/toc.* # @mdgen OWNER: mpformats/man.macros namespace eval ::doctools {} namespace eval ::doctools::toc { # Data storage in the doctools::toc module # ------------------------------- # # One namespace per object, containing # 1) A list of additional search paths for format definition files. # This list extends the list of standard paths known to the module. # The paths in the list are searched before the standard paths. # 2) Configuration information # a) string: The format to use when converting the input. # 4) Name of the interpreter used to perform the syntax check of the # input (= allowed order of formatting commands). # 5) Name of the interpreter containing the code coming from the format # definition file. # 6) Name of the expander object used to interpret the input to convert. # commands is the list of subcommands recognized by the doctoc objects variable commands [list \ "cget" \ "configure" \ "destroy" \ "format" \ "map" \ "search" \ "warnings" \ "parameters" \ "setparam" \ ] # Only export the toplevel commands namespace export new search help # Global data # 1) List of standard paths to look at when searching for a format # definition. Extensible. # 2) Location of this file in the filesystem variable paths [list] variable here [file dirname [info script]] } # ::doctools::toc::search -- # # Extend the list of paths used when searching for format definition files. # # Arguments: # path Path to add to the list. The path has to exist, has to be a # directory, and has to be readable. # # Results: # None. # # Sideeffects: # The specified path is added to the front of the list of search # paths. This means that the new path is search before the # standard paths set at module initialization time. proc ::doctools::toc::search {path} { variable paths if {![file exists $path]} {return -code error "doctools::toc::search: path does not exist"} if {![file isdirectory $path]} {return -code error "doctools::toc::search: path is not a directory"} if {![file readable $path]} {return -code error "doctools::toc::search: path cannot be read"} set paths [linsert $paths 0 $path] return } # ::doctools::toc::help -- # # Return a string containing short help # regarding the existing formatting commands. # # Arguments: # None. # # Results: # A string. proc ::doctools::toc::help {} { return "formatting commands\n\ * toc_begin - begin of table of contents\n\ * toc_end - end of toc\n\ * division_start - begin of toc division\n\ * division_end - end of toc division\n\ * item - toc element\n\ * vset - set/get variable values\n\ * include - insert external file\n\ * lb, rb - left/right brackets\n\ " } # ::doctools::toc::new -- # # Create a new doctoc object with a given name. May configure the object. # # Arguments: # name Name of the doctoc object. # args Options configuring the new object. # # Results: # name Name of the doctools created proc ::doctools::toc::new {name args} { if { [llength [info commands ::$name]] } { return -code error "command \"$name\" already exists, unable to create doctoc object" } if {[llength $args] % 2 == 1} { return -code error "wrong # args: doctools::new name ?opt val...??" } # The arguments seem to be ok, setup the namespace for the object namespace eval ::doctools::toc::doctoc$name { variable paths [list] variable file "" variable format "" variable formatfile "" variable format_ip "" variable chk_ip "" variable expander "[namespace current]::ex" variable ex_ok 0 variable msg [list] variable map ; array set map {} variable param [list] } # Create the command to manipulate the object # $name -> ::doctools::toc::DocTocProc $name interp alias {} ::$name {} ::doctools::toc::DocTocProc $name # If the name was followed by arguments use them to configure the # object before returning its handle to the caller. if {[llength $args] > 1} { # Use linsert trick to make the command a pure list. eval [linsert $args 0 _configure $name] } return $name } ########################## # Private functions follow # ::doctools::toc::DocTocProc -- # # Command that processes all doctoc object commands. # Dispatches any object command to the appropriate internal # command implementing its functionality. # # Arguments: # name Name of the doctoc object to manipulate. # cmd Subcommand to invoke. # args Arguments for subcommand. # # Results: # Varies based on command to perform proc ::doctools::toc::DocTocProc {name {cmd ""} args} { # Do minimal args checks here if { [llength [info level 0]] == 2 } { error "wrong # args: should be \"$name option ?arg arg ...?\"" } # Split the args into command and args components if { [llength [info commands ::doctools::toc::_$cmd]] == 0 } { variable commands set optlist [join $commands ", "] set optlist [linsert $optlist "end-1" "or"] return -code error "bad option \"$cmd\": must be $optlist" } return [eval [list ::doctools::toc::_$cmd $name] $args] } ########################## # Method implementations follow (these are also private commands) # ::doctools::toc::_cget -- # # Retrieve the current value of a particular option # # Arguments: # name Name of the doctoc object to query # option Name of the option whose value we are asking for. # # Results: # The value of the option proc ::doctools::toc::_cget {name option} { _configure $name $option } # ::doctools::toc::_configure -- # # Configure a doctoc object, or query its configuration. # # Arguments: # name Name of the doctoc object to configure # args Options and their values. # # Results: # None if configuring the object. # A list of all options and their values if called without arguments. # The value of one particular option if called with a single argument. proc ::doctools::toc::_configure {name args} { if {[llength $args] == 0} { # Retrieve the current configuration. upvar #0 ::doctools::toc::doctoc${name}::file file upvar #0 ::doctools::toc::doctoc${name}::format format set res [list] lappend res -file $file lappend res -format $format return $res } elseif {[llength $args] == 1} { # Query the value of one particular option. switch -exact -- [lindex $args 0] { -file { upvar #0 ::doctools::toc::doctoc${name}::file file return $file } -format { upvar #0 ::doctools::toc::doctoc${name}::format format return $format } default { return -code error \ "doctools::toc::_configure: Unknown option \"[lindex $args 0]\", expected\ -file, or -format" } } } else { # Reconfigure the object. if {[llength $args] % 2 == 1} { return -code error "wrong # args: doctools::toc::_configure name ?opt val...??" } foreach {option value} $args { switch -exact -- $option { -file { upvar #0 ::doctools::toc::doctoc${name}::file file set file $value } -format { if {[catch { set fmtfile [LookupFormat $name $value] SetupFormatter $name $fmtfile upvar #0 ::doctools::toc::doctoc${name}::format format set format $value } msg]} { return -code error "doctools::toc::_configure: -format: $msg" } } default { return -code error \ "doctools::toc::_configure: Unknown option \"$option\", expected\ -file, or -format" } } } } return "" } # ::doctools::toc::_destroy -- # # Destroy a doctoc object, including its associated command and data storage. # # Arguments: # name Name of the doctoc object to destroy. # # Results: # None. proc ::doctools::toc::_destroy {name} { # Check the object for sub objects which have to destroyed before # the namespace is torn down. namespace eval ::doctools::toc::doctoc$name { if {$format_ip != ""} {interp delete $format_ip} if {$chk_ip != ""} {interp delete $chk_ip} # Expander objects have no delete/destroy method. This would # be a leak if not for the fact that an expander object is a # namespace, and we have arranged to make it a sub namespace of # the doctoc object. Therefore tearing down our object namespace # also cleans up the expander object. # if {$expander != ""} {$expander destroy} } namespace delete ::doctools::toc::doctoc$name interp alias {} ::$name {} return } # ::doctools::toc::_map -- # # Add a mapping from symbolic to actual filename to the object. # # Arguments: # name Name of the doctoc object to use # sfname Symbolic filename to map # afname Actual filename # # Results: # None. proc ::doctools::toc::_map {name sfname afname} { upvar #0 ::doctools::toc::doctoc${name}::map map set map($sfname) $afname return } # ::doctools::toc::_format -- # # Convert some text in doctools format # according to the configuration in the object. # # Arguments: # name Name of the doctoc object to use # text Text to convert. # # Results: # The conversion result. proc ::doctools::toc::_format {name text} { upvar #0 ::doctools::toc::doctoc${name}::format format if {$format == ""} { return -code error "$name: No format was specified" } upvar #0 ::doctools::toc::doctoc${name}::format_ip format_ip upvar #0 ::doctools::toc::doctoc${name}::chk_ip chk_ip upvar #0 ::doctools::toc::doctoc${name}::ex_ok ex_ok upvar #0 ::doctools::toc::doctoc${name}::expander expander upvar #0 ::doctools::toc::doctoc${name}::passes passes upvar #0 ::doctools::toc::doctoc${name}::msg warnings if {!$ex_ok} {SetupExpander $name} if {$chk_ip == ""} {SetupChecker $name} # assert (format_ip != "") set warnings [list] if {[catch {$format_ip eval toc_initialize}]} { return -code error "Could not initialize engine" } set result "" for { set p $passes ; set n 1 } { $p > 0 } { incr p -1 ; incr n } { if {[catch {$format_ip eval [list toc_setup $n]}]} { catch {$format_ip eval toc_shutdown} return -code error "Could not initialize pass $n of engine" } $chk_ip eval ck_initialize if {[catch {set result [$expander expand $text]} msg]} { catch {$format_ip eval toc_shutdown} # Filter for checker errors and reduce them to the essential message. if {![regexp {^Error in} $msg]} {return -code error $msg} #set msg [join [lrange [split $msg \n] 2 end]] if {![regexp {^--> \(FmtError\) } $msg]} {return -code error "Doctoc $msg"} set msg [lindex [split $msg \n] 0] regsub {^--> \(FmtError\) } $msg {} msg return -code error $msg } $chk_ip eval ck_complete } if {[catch {set result [$format_ip eval [list toc_postprocess $result]]}]} { return -code error "Unable to post process final result" } if {[catch {$format_ip eval toc_shutdown}]} { return -code error "Could not shut engine down" } return $result } # ::doctools::toc::_search -- # # Add a search path to the object. # # Arguments: # name Name of the doctoc object to extend # path Search path to add. # # Results: # None. proc ::doctools::toc::_search {name path} { if {![file exists $path]} {return -code error "$name search: path does not exist"} if {![file isdirectory $path]} {return -code error "$name search: path is not a directory"} if {![file readable $path]} {return -code error "$name search: path cannot be read"} upvar #0 ::doctools::toc::doctoc${name}::paths paths set paths [linsert $paths 0 $path] return } # ::doctools::toc::_warnings -- # # Return the warning accumulated during the last invocation of 'format'. # # Arguments: # name Name of the doctoc object to query # # Results: # A list of warnings. proc ::doctools::toc::_warnings {name} { upvar #0 ::doctools::toc::doctoc${name}::msg msg return $msg } # ::doctools::_parameters -- # # Returns a list containing the parameters provided # by the selected formatting engine. # # Arguments: # name Name of the doctools object to query # # Results: # A list of parameter names proc ::doctools::toc::_parameters {name} { upvar #0 ::doctools::toc::doctoc${name}::param param return $param } # ::doctools::_setparam -- # # Set a named engine parameter to a value. # # Arguments: # name Name of the doctools object to query # param Name of the parameter to set. # value Value to set the parameter to. # # Results: # None. proc ::doctools::toc::_setparam {name param value} { upvar #0 ::doctools::toc::doctoc${name}::format_ip format_ip if {$format_ip == {}} { return -code error \ "Unable to set parameters without a valid format" } $format_ip eval [list toc_varset $param $value] return } ########################## # Support commands # ::doctools::toc::LookupFormat -- # # Search a format definition file based upon its name # # Arguments: # name Name of the doctoc object to use # format Name of the format to look for. # # Results: # The file containing the format definition proc ::doctools::toc::LookupFormat {name format} { # Order of searching # 1) Is the name of the format an existing file ? # If yes, take this file. # 2) Look for the file in the directories given to the object itself.. # 3) Look for the file in the standard directories of this package. if {[file exists $format]} { return $format } upvar #0 ::doctools::toc::doctoc${name}::paths opaths foreach path $opaths { set f [file join $path toc.$format] if {[file exists $f]} { return $f } } variable paths foreach path $paths { set f [file join $path toc.$format] if {[file exists $f]} { return $f } } return -code error "Unknown format \"$format\"" } # ::doctools::toc::SetupFormatter -- # # Create and initializes an interpreter containing a # formatting engine # # Arguments: # name Name of the doctoc object to manipulate # format Name of file containing the code of the engine # # Results: # None. proc ::doctools::toc::SetupFormatter {name format} { # Create and initialize the interpreter first. # Use a transient variable. Interrogate the # engine and check its response. Bail out in # case of errors. Only if we pass the checks # we tear down the old engine and make the new # one official. variable here set mpip [interp create -safe] ; # interpreter for the formatting engine #set mpip [interp create] ; # interpreter for the formatting engine $mpip invokehidden source [file join $here api_toc.tcl] #$mpip eval [list source [file join $here api_toc.tcl]] interp alias $mpip dt_source {} ::doctools::toc::Source $mpip [file dirname $format] interp alias $mpip dt_read {} ::doctools::toc::Read $mpip [file dirname $format] interp alias $mpip puts_stderr {} ::puts stderr $mpip invokehidden source $format #$mpip eval [list source $format] # Check the engine for useability in doctools. foreach api { toc_numpasses toc_initialize toc_setup toc_postprocess toc_shutdown toc_listvariables toc_varset } { if {[$mpip eval [list info commands $api]] == {}} { interp delete $mpip error "$format error: API incomplete, cannot use this engine" } } if {[catch { set passes [$mpip eval toc_numpasses] }]} { interp delete $mpip error "$format error: Unable to query for number of passes" } if {![string is integer $passes] || ($passes < 1)} { interp delete $mpip error "$format error: illegal number of passes \"$passes\"" } if {[catch { set parameters [$mpip eval toc_listvariables] }]} { interp delete $mpip error "$format error: Unable to query for list of parameters" } # Passed the tests. Tear down existing engine, # and checker. The latter is destroyed because # of its aliases into the formatter, which are # now invalid. It will be recreated during the # next call of 'format'. upvar #0 ::doctools::toc::doctoc${name}::formatfile formatfile upvar #0 ::doctools::toc::doctoc${name}::format_ip format_ip upvar #0 ::doctools::toc::doctoc${name}::chk_ip chk_ip upvar #0 ::doctools::toc::doctoc${name}::expander expander upvar #0 ::doctools::toc::doctoc${name}::passes xpasses upvar #0 ::doctools::toc::doctoc${name}::param xparam if {$chk_ip != {}} {interp delete $chk_ip} if {$format_ip != {}} {interp delete $format_ip} set chk_ip "" set format_ip "" # Now link engine API into it. interp alias $mpip dt_format {} ::doctools::toc::GetFormat $name interp alias $mpip dt_user {} ::doctools::toc::GetUser $name interp alias $mpip dt_fmap {} ::doctools::toc::MapFile $name foreach cmd {cappend cget cis cname cpop cpush cset lb rb} { interp alias $mpip ex_$cmd {} $expander $cmd } set format_ip $mpip set formatfile $format set xpasses $passes set xparam $parameters return } # ::doctools::toc::SetupChecker -- # # Create and initializes an interpreter for checking the usage of # doctoc formatting commands # # Arguments: # name Name of the doctoc object to manipulate # # Results: # None. proc ::doctools::toc::SetupChecker {name} { # Create an interpreter for checking the usage of doctoc formatting commands # and initialize it: Link it to the interpreter doing the formatting, the # expander object and the configuration information. All of which # is accessible through the token/handle (name of state/object array). variable here upvar #0 ::doctools::toc::doctoc${name}::chk_ip chk_ip if {$chk_ip != ""} {return} upvar #0 ::doctools::toc::doctoc${name}::expander expander upvar #0 ::doctools::toc::doctoc${name}::format_ip format_ip set chk_ip [interp create] ; # interpreter hosting the formal format checker # Make configuration available through command, then load the code base. foreach {cmd ckcmd} { dt_search SearchPaths dt_error FmtError dt_warning FmtWarning } { interp alias $chk_ip $cmd {} ::doctools::toc::$ckcmd $name } $chk_ip eval [list source [file join $here checker_toc.tcl]] # Simple expander commands are directly routed back into it, no # checking required. foreach cmd {cappend cget cis cname cpop cpush cset lb rb} { interp alias $chk_ip $cmd {} $expander $cmd } # Link the formatter commands into the checker. We use the prefix # 'fmt_' to distinguish them from the checking commands. foreach cmd { toc_begin toc_end division_start division_end item comment plain_text } { interp alias $chk_ip fmt_$cmd $format_ip fmt_$cmd } return } # ::doctools::toc::SetupExpander -- # # Create and initializes the expander for input # # Arguments: # name Name of the doctoc object to manipulate # # Results: # None. proc ::doctools::toc::SetupExpander {name} { upvar #0 ::doctools::toc::doctoc${name}::ex_ok ex_ok if {$ex_ok} {return} upvar #0 ::doctools::toc::doctoc${name}::expander expander ::textutil::expander $expander $expander evalcmd [list ::doctools::toc::Eval $name] $expander textcmd plain_text set ex_ok 1 return } # ::doctools::toc::SearchPaths -- # # API for checker. Returns list of search paths for format # definitions. Used to look for message catalogs as well. # # Arguments: # name Name of the doctoc object to query. # # Results: # None. proc ::doctools::toc::SearchPaths {name} { upvar #0 ::doctools::toc::doctoc${name}::paths opaths variable paths set p $opaths foreach s $paths {lappend p $s} return $p } # ::doctools::toc::FmtError -- # # API for checker. Called when an error occurred. # # Arguments: # name Name of the doctoc object to query. # text Error message # # Results: # None. proc ::doctools::toc::FmtError {name text} { return -code error "(FmtError) $text" } # ::doctools::toc::FmtWarning -- # # API for checker. Called when a warning was generated # # Arguments: # name Name of the doctoc object # text Warning message # # Results: # None. proc ::doctools::toc::FmtWarning {name text} { upvar #0 ::doctools::toc::doctoc${name}::msg msg lappend msg $text return } # ::doctools::toc::Eval -- # # API for expander. Routes the macro invocations # into the checker interpreter # # Arguments: # name Name of the doctoc object to query. # # Results: # None. proc ::doctools::toc::Eval {name macro} { upvar #0 ::doctools::toc::doctoc${name}::chk_ip chk_ip # Handle the [include] command directly if {[string match include* $macro]} { set macro [$chk_ip eval [list subst $macro]] foreach {cmd filename} $macro break return [ExpandInclude $name $filename] } return [$chk_ip eval $macro] } # ::doctools::toc::ExpandInclude -- # # Handle inclusion of files. # # Arguments: # name Name of the doctoc object to query. # path Name of file to include and expand. # # Results: # None. proc ::doctools::toc::ExpandInclude {name path} { # Look for the file relative to the directory of the # main file we are converting. If that fails try to # use the current working directory. Throw an error # if the file couldn't be found. upvar #0 ::doctools::toc::doctoc${name}::file file set ipath [file normalize [file join [file dirname $file] $path]] if {![file exists $ipath]} { set ipath $path if {![file exists $ipath]} { return -code error "Unable to fine include file \"$path\"" } } set chan [open $ipath r] set text [read $chan] close $chan upvar #0 ::doctools::toc::doctoc${name}::expander expander set saved $file set file $ipath set res [$expander expand $text] set file $saved return $res } # ::doctools::toc::GetUser -- # # API for formatter. Returns name of current user # # Arguments: # name Name of the doctoc object to query. # # Results: # String, name of current user. proc ::doctools::toc::GetUser {name} { global tcl_platform return $tcl_platform(user) } # ::doctools::toc::GetFormat -- # # API for formatter. Returns format information # # Arguments: # name Name of the doctoc object to query. # # Results: # Format information proc ::doctools::toc::GetFormat {name} { upvar #0 ::doctools::toc::doctoc${name}::format format return $format } # ::doctools::toc::MapFile -- # # API for formatter. Maps symbolic to actual filename in a toc # item. If no mapping is found it is assumed that the symbolic # name is also the actual name. # # Arguments: # name Name of the doctoc object to query. # fname Symbolic name of the file. # # Results: # Actual name of the file. proc ::doctools::toc::MapFile {name fname} { upvar #0 ::doctools::toc::doctoc${name}::map map if {[info exists map($fname)]} { return $map($fname) } return $fname } # ::doctools::toc::Source -- # # API for formatter. Used by engine to ask for # additional script files support it. # # Arguments: # name Name of the doctoc object to change. # # Results: # Boolean flag. proc ::doctools::toc::Source {ip path file} { $ip invokehidden source [file join $path [file tail $file]] #$ip eval [list source [file join $path [file tail $file]]] return } proc ::doctools::toc::Read {ip path file} { #puts stderr "$ip (read $path $file)" return [read [set f [open [file join $path [file tail $file]]]]][close $f] } #------------------------------------ # Module initialization namespace eval ::doctools::toc { # Reverse order of searching. First to search is specified last. # FOO/doctoc.tcl # => FOO/mpformats #catch {search [file join $here lib doctools mpformats]} #catch {search [file join [file dirname $here] lib doctools mpformats]} catch {search [file join $here mpformats]} } package provide doctools::toc 1.1.2