1#!/bin/sh
2# -*-tcl-*-
3# the next line restarts using tclsh\
4exec tclsh "$0" "$@"
5
6#-------------------------------------------------------------------------
7# TITLE:
8#	expand.tcl
9#
10# VERSION:
11#       2.0
12#
13# AUTHOR:
14#	Will Duquette
15#
16# DESCRIPTION:
17#       Usage: tclsh expand.tcl [options] files....
18#
19#	Reads files, writing input to output.  Most text
20# 	is output unchanged.  Certain text is evaluated as Tcl code;
21# 	the result of the Tcl code, if any, is output.  If the Tcl
22# 	code results in an error, the error result is output.
23#
24#	Before reading any input, expand.tcl reads any exprules.tcl
25# 	file in the current directory, or alternatively a tcl file
26# 	specified by the "-rules" command line option.  This allows the
27# 	caller to define special formatting macros for general use
28#	and override them as needed.  The rules file can also read
29# 	arguments from the command line, after options are removed but
30# 	before the files are processed.
31#
32#	On an error in a macro, expand can "ignore" the macro,
33#       "output" the macro unchanged, "fail" (the default), halting
34#	processing, depending on the value of the "-error" option.
35#
36#	Output is written to stdout, by default; the "-out" option
37#       sends it to a file, instead.  If the specified file is "nul",
38#       then no output is written at all.  The rules can also control
39#       the output via the setoutput command.
40#
41#	Any text in brackets, e.g., "[" and "]" is treated as a Tcl
42#       command, and evaluated.  The bracket characters can be changed
43#       using ::expand::setbrackets.
44#
45#       Normally Expand reads the output files only once; a rules file
46#       can choose multiple passes using the ::expand::setpasses command.  The
47#       ::expand::exppass command returns the number of the current pass,
48#       starting at 1.
49#
50# LICENSE:
51#       Copyright (C) 2000 by William H. Duquette.  See license.txt,
52#       distributed with this file, for license information.
53#
54# CHANGE LOG:
55#
56#       06/27/98: Released V1.0 on web.
57#       06/27/98: Changed exp_extract to handle multi-character bracket
58#                 tokens.  Added exp_stripBrackets to remove multi-character
59#                 bracket tokens.
60#       06/27/98: Added function setbrackets to allow the user to choose the
61#                 bracket tokens.
62#       06/27/98: Added brand new command line option parser.  The new parser
63#                 can be used by the rules file's begin_hook.
64#
65#	06/28/98: Version 1.1 released.
66#
67#	06/29/98: Added init_hook.
68#       06/29/98: Added setoutput command.
69#       06/29/98: Added setpasses/exppass and multi-pass processing.
70#       06/29/98: Fixed potential bug in exp_getCmd: using "info complete"
71#                 with changed left and right brackets.
72#	06/30/98: Added -testmode flag: causes error output to go to 
73#                 stdout instead of stderr to aid testing.
74#       07/01/98: Added a tclsh80 starter at the top of the file.
75#       07/01/98: exp_error calls "exit 1" instead of "exit 0" again.
76#       07/02/98: Added expandText and include commands.
77#       07/03/98: Renamed exp_write to expwrite, and made it public,
78#                 for use with setoutput.
79#       07/07/98: Released Expand V1.2
80#
81#	10/10/99: Added raw_text_hook.
82#	01/15/00: Rewrote popArg, in an attempt to prevent an odd bug
83#		  that manifests only on certain platforms.
84#	01/15/00: Released Expand V1.3
85#
86#       02/03/00: Found a bug in expandText; it isn't safe to extract
87#                 the command name from an arbitrary Tcl script using
88#                 lindex, as many valid scripts aren't valid lists. I
89#                 now use scan instead of lindex.
90#
91#       04/17/00: Version 2 rewrite begins.  The code is cleaned up and 
92#                 placed in the ::expand:: namespace.
93#
94#       05/07/00: Version 2 rewrite ends (for now).
95
96#-------------------------------------------------------------------------
97# Namespace: all of the expand code exists in the ::expand:: namespace,
98# leaving the global namespace for the user's rules.
99
100namespace eval ::expand:: {
101    # Exported Commands
102    namespace export {[a-z]*}
103
104    # Expand Variables
105
106    # Macro bracketing sequences.
107    variable leftBracket "\["
108    variable rightBracket "\]"
109
110    # What to output when an error is detected: 
111    # "nothing", "macro", "error", "fail"
112    variable errorOutputMode fail
113
114    # Number of passes to make over the input
115    variable numberOfPasses 1
116
117    # The current output channel
118    variable outputChannel ""
119
120    # A command can push its context onto a stack, causing any text 
121    # that follows it to be saved separately.  Later on, a paired command 
122    # can pop the stack, acquiring the saved text and including it in its own
123    # output.
124    variable level 0
125    variable context
126    variable contextName
127    variable contextData
128    set context($level) ""
129    set contextName($level) ":0"
130
131    # Status variables
132    variable currentFileName ""
133    variable currentPass 0
134}
135
136#-------------------------------------------------------------------------
137# User settings:  These commands allow the users to set, and in some
138# cases retrieve, various expansion parameters.
139
140# lb
141#
142# Return the left bracket sequence.
143
144proc ::expand::lb {} {
145    variable leftBracket
146
147    return $leftBracket
148}
149
150# rb
151#
152# Return the right bracket sequence.
153
154proc ::expand::rb {} {
155    variable rightBracket
156
157    return $rightBracket
158}
159
160# setbrackets lb rb
161#
162# Set the bracket sequences
163proc ::expand::setbrackets {lb rb} {
164    variable leftBracket 
165    variable rightBracket
166
167    if {$lb == "" || $rb == ""} {
168        error "Empty string specified as left or right bracket."
169    }
170
171    set leftBracket $lb
172    set rightBracket $rb
173
174    return
175}
176
177# setErrorOutputMode mode
178#
179# Set the error output mode
180proc ::expand::setErrorOutputMode {mode} {
181    variable errorOutputMode 
182
183    if {![oneOf {fail nothing macro error} $mode]} {
184        error "Invalid error output mode '$mode'"
185    }
186
187    set errorOutputMode $mode
188}
189
190# Return the current file name
191proc ::expand::expfile {} {
192    variable currentFileName
193
194    return $currentFileName
195}
196
197# Return the number of the current pass.
198proc ::expand::exppass {} {
199    variable currentPass
200
201    return $currentPass
202}
203
204# Set the number of passes
205proc ::expand::setpasses {passes} {
206    variable numberOfPasses
207
208    set numberOfPasses $passes
209
210    if {$numberOfPasses < 1} {
211        error "setpasses: must be >= 1"
212    }
213}
214
215#-------------------------------------------------------------------------
216# User hooks: a rule set can redefine these hooks to do anything desired.
217# The init_hook doesn't contribute to the output, but the other hooks do.
218# Since the hooks do nothing by default, and are to be redefined by the
219# user, they are defined in the global name space.
220
221# Initialization Hook: called when the rule set is loaded.
222proc init_hook {} {}
223
224# Begin Hook: Called at the beginning of each pass.
225proc begin_hook {} {}
226
227# End Hook: Called at the end of each pass.
228proc end_hook {} {}
229
230# Begin File Hook: Called before each file is processed.
231proc begin_file_hook {fileName} {}
232
233# End File Hook: Called after each file is processed.
234proc end_file_hook {fileName} {}
235
236# Raw Text Hook: All plain (non-macro) text is passed through this
237# function.
238proc raw_text_hook {text} {return $text}
239
240#-------------------------------------------------------------------------
241# Context: Every expansion takes place in its own context; however, 
242# a macro can push a new context, causing the text it returns and all
243# subsequent text to be saved separately.  Later, a matching macro can
244# pop the context, acquiring all text saved since the first command,
245# and use that in its own output.
246
247# cpush name
248#
249# pushes an empty context onto the stack.  All output text will be added
250# to this context until it is popped.
251
252proc ::expand::cpush {name} {
253    variable level
254    variable context
255    variable contextName
256
257    incr level
258    set context($level) {}
259    set contextName($level) $name
260}
261
262# cis name
263#
264# Returns true if the current context has the given name.
265
266proc ::expand::cis {name} {
267    variable level
268    variable contextName
269
270    return [expr [string compare $name $contextName($level)] == 0]
271}
272
273# cname
274#
275# Returns the current context name.
276
277proc ::expand::cname {} {
278    variable level
279    variable contextName
280
281    return $contextName($level)
282}
283
284# csave name value
285#
286# Save or retrieve value in the current context
287
288proc ::expand::csave {name value} {
289    variable contextData
290    variable level
291    
292    set contextData($level-$name) $value
293}
294
295# cget name
296#
297# Get the value of a context variable
298proc ::expand::cget {name} {
299    variable contextData
300    variable level
301
302    if {![info exists contextData($level-$name)]} {
303        error "*** Error, context var $name doesn't exist in this context"
304    }
305
306    return $contextData($level-$name)
307}
308
309# cvar name
310#
311# Get a context variable's real name, e.g., for appending or lappending
312proc ::expand::cvar {name} {
313    variable contextData
314    variable level
315
316    if {![info exists contextData($level-$name)]} {
317        error "*** Error, context var $name doesn't exist in this context"
318    }
319
320    return ::expand::contextData($level-$name)
321}
322
323# cpop
324#
325# Pops a context level off of the stack, returning the accumulated text.
326
327proc ::expand::cpop {name} {
328    variable level
329    variable context
330    variable contextName
331    variable contextData
332
333    if {$level == 0} {
334        error "*** Error, context mismatch: got unexpected '$name'"
335    }
336
337    if {"$contextName($level)" != "$name"} {
338        error \
339      "*** Error, context mismatch: expected $contextName($level), got $name"
340    }
341
342    set result $context($level)
343    set context($level) ""
344    set contextName($level) ""
345
346    foreach name [array names contextData $level-*] {
347        unset contextData($name)
348    }
349
350    incr level -1
351
352    return $result
353}
354
355# ContextAppend text
356#
357# This private command appends text to the current context.  It is for
358# use only by the Expand code; macros should return their text.
359
360proc ::expand::ContextAppend {text} {
361    variable context
362    variable level
363
364    append context($level) $text
365}
366
367#-------------------------------------------------------------------------
368# Macro-expansion:  The following code is the heart of the program.
369# Given a text string, and the current variable settings, this code
370# returns an expanded string, with all macros replaced.
371#
372# If a fatal error is detected during expansion, expandText throws
373# an error for its caller to handle.   An error detected while 
374# expanding a particular macro is only fatal if the errorOutputMode
375# is "fail"; otherwise, the result of the expansion attempt is 
376# output according to the mode.
377#
378# All non-macro text is passed through the raw_text_hook.
379
380# Expands a string using the current macro definitions and Expand
381# variable settings.
382proc ::expand::expandText {inputString} {
383    variable errorOutputMode
384    global errorInfo
385
386    cpush expandText
387
388    while {[string length $inputString] > 0} {
389        set plainText [ExtractToToken inputString [lb] exclude]
390
391        # FIRST, If there was plain text, append it to the output, and 
392        # continue.
393        if {$plainText != ""} {
394            ContextAppend [raw_text_hook $plainText]
395            if {[string length $inputString] == 0} {
396                break
397            }
398        }
399
400        # NEXT, A macro is the next thing; process it.
401        if {[catch "GetMacro inputString" macro]} {
402            error "*** Error reading macro from input: $macro"
403        }
404
405        # Expand the macro, and output the result, or
406        # handle an error.
407        if {![catch "uplevel #0 [list $macro]" result]} {
408            ContextAppend $result 
409            continue
410        } 
411        
412        switch $errorOutputMode {
413            nothing { }
414            macro { 
415                ContextAppend "[lb]$macro[rb]" 
416            }
417            error {
418                ContextAppend "[lb]$macro[rb]\n"
419                ContextAppend "*** Error in preceding macro: $result\n$errorInfo"
420            }
421            fail   { 
422                error "*** Error in macro:\n[lb]$macro[rb]\n$result"
423            }
424        }
425    }
426
427    return [cpop expandText]
428}
429
430# ExtractToToken string token mode
431#
432# Extract text from a string, up to or including a particular
433# token.  Remove the extracted text from the string.
434# mode determines whether the found token is removed;
435# it should be "include" or "exclude".  The string is
436# modified in place, and the extracted text is returned.
437proc ::expand::ExtractToToken {string token mode} {
438    upvar $string theString
439
440    # First, determine the offset
441    switch $mode {
442        include { set offset [expr [string length $token] - 1] }
443        exclude { set offset -1 }
444        default { error "::expand::ExtractToToken: unknown mode $mode" }
445    }
446
447    # Next, find the first occurrence of the token.
448    set tokenPos [string first $token $theString]
449
450    # Next, return the entire string if it wasn't found, or just
451    # the part upto or including the character.
452    if {$tokenPos == -1} {
453        set theText $theString
454        set theString ""
455    } else {
456        set newEnd [expr $tokenPos + $offset]
457        set newBegin [expr $newEnd + 1]
458        set theText [string range $theString 0 $newEnd]
459        set theString [string range $theString $newBegin end]
460    }
461
462    return $theText
463}
464
465# Get the next complete command, removing it from the string.
466proc ::expand::GetMacro {string} {
467    upvar $string theString
468
469    # FIRST, it's an error if the string doesn't begin with a
470    # character.
471    if {[string first [lb] $theString] != 0} {
472        error "::expand::GetMacro: assertion failure, next text isn't a command! '$theString'"
473    }
474
475    # NEXT, extract a full macro
476    set macro [ExtractToToken theString [lb] include]
477    while {[string length $theString] > 0} {
478        append macro [ExtractToToken theString [rb] include]
479
480        # Verify that the command really ends with the [rb] characters,
481        # whatever they are.  If not, break because of unexpected
482        # end of file.
483        if {![IsBracketed $macro]} {
484            break;
485        }
486
487        set strippedMacro [StripBrackets $macro]
488
489        if {[info complete "puts \[$strippedMacro\]"]} {
490            return $strippedMacro
491        }
492    }
493
494    if {[string length $macro] > 40} {
495        set macro "[string range $macro 0 39]...\n"
496    }
497    error "*** Error, unexpected EOF in macro:\n$macro"
498}
499
500# Strip left and right bracket tokens from the ends of a macro,
501# provided that it's properly bracketed.
502proc ::expand::StripBrackets {macro} {
503    set llen [string length [lb]]
504    set rlen [string length [rb]]
505    set tlen [string length $macro]
506
507    return [string range $macro $llen [expr $tlen - $rlen - 1]]
508}
509
510# Return 1 if the macro is properly bracketed, and 0 otherwise.
511proc ::expand::IsBracketed {macro} {
512    set llen [string length [lb]]
513    set rlen [string length [rb]]
514    set tlen [string length $macro]
515
516    set leftEnd [string range $macro 0 [expr $llen - 1]]
517    set rightEnd [string range $macro [expr $tlen - $rlen] end]
518
519    if {$leftEnd != [lb]} {
520        return 0
521    } elseif {$rightEnd != [rb]} {
522        return 0
523    } else {
524        return 1
525    }
526}
527
528#-------------------------------------------------------------------------
529# File handling: these routines, some public and some private, handle
530# processing of files.
531
532# expand fileList outputFile
533#
534# This is the basic algorithm of the Expand tool.  Given a list of files
535# to expand, it executes the following sequence.  Return values of all
536# handlers, except for the initHandlers, is written to the current output
537# file.
538#
539# - For each pass,
540#     - Set ::expand::currentPass.
541#     - Call the begin_hook.
542#     - For each file in the file list,
543#         - Set ::expand::currentFileName
544#         - Call the begin_file_hook.
545#	  - read file and expand its contents
546#         - Call the end_file_hook.
547#     - Call the end_hook.
548# - Close the current output file.
549
550proc ::expand::expand {fileList outputFile} {
551    variable currentPass
552    variable numberOfPasses
553    variable currentFileName
554
555    for {set currentPass 1} {$currentPass <= $numberOfPasses} \
556            {incr currentPass} {
557
558        # First, if this is any pass but the last, set output to nul;
559        # otherwise, set output to the requested output file.
560        if {$currentPass < $numberOfPasses} {
561            setoutput nul
562        } else {
563            setoutput $outputFile
564        }
565
566        # Next, execute the beginning hook
567        set currentFileName ""
568        expwrite [begin_hook]
569
570        # Next, expand each of the files on the command line.
571        foreach file $fileList {
572            if {[catch "ExpandFile [list $file]" result]} {
573                puts stderr $result
574                exit 1
575            }
576            expwrite $result
577        }
578
579        # Next, execute the end hook
580        expwrite [end_hook]
581    }
582
583    # Next, close the output file.
584    setoutput nul
585}
586
587# ExpandFile
588#
589# Helper routine for ::expand::expand.  It expands a single file,
590# calling the begin and end file handlers and returning the expanded
591# result.
592
593proc ::expand::ExpandFile {fileName} {
594    variable currentFileName
595
596    # Set the current file
597    set currentFileName $fileName
598
599    # Call the begin_file_hook
600    set output [begin_file_hook $fileName]
601
602    # Expand the file
603    set contents [readFile $fileName]
604
605    if {[catch [list expandText $contents] result]} {
606        error "*** Error expanding $fileName:\n$result"
607    }
608
609    append output $result
610
611    # Call the endFileHandlers
612    append output [end_file_hook $fileName]
613
614    return $output
615}
616
617# include file
618#
619# Reads a file into memory, and expands its contents.
620
621proc ::expand::include {fileName} {
622    # Get the file's contents, and prepare to output it.
623    set contents [readFile $fileName]
624
625    if {[catch [list expandText $contents] result]} {
626        error "*** Error including $fileName:\n$result"
627    }
628
629    return $result
630}
631
632# readFile file
633#
634# Reads a file into memory, returning its contents.
635proc ::expand::readFile {fileName} {
636   # Open the file.
637    if {[catch "open $fileName" fin]} {
638        error "Could not read file '$fileName': $fin"
639    }
640
641    # Read the contents and close the file.
642    set contents [read $fin]
643    close $fin
644
645    return $contents
646}
647
648#-------------------------------------------------------------------------
649# Output Management
650
651# Set the output file
652proc ::expand::setoutput {fileName} {
653    variable outputChannel
654
655    # Close any existing file
656    if {$outputChannel != "" && $outputChannel != "stdout"} {
657        close $outputChannel
658    }
659
660    # Pick stdout, no output at all, or a real file
661    if {$fileName == ""} {
662        set outputChannel stdout
663    } elseif {$fileName == "nul"} {
664        set outputChannel ""
665    } else {
666        if {[catch "open $fileName w" outputChannel]} {
667            error "Could not open output file $fileName"
668        }
669    }
670
671    return
672}
673
674# Output a bunch of text to the output file.
675proc ::expand::expwrite {text} {
676    variable outputChannel
677
678    if {$outputChannel != ""} {
679        puts -nonewline $outputChannel $text
680    }
681}
682
683#-------------------------------------------------------------------------
684# getoptions: command line option parsing
685#
686# The getoptions function parses a list as a command line, removing
687# options and their values.  Any remaining tokens and options remain
688# in the list and can be parsed by another call to getoptions or in
689# any other way the caller prefers.
690#
691# getoptions is called as follows:
692#
693# getoptions arglist [-strict] [{optionDef... }]
694#
695# "arglist" is the name of a list variable, typically argv.  It is
696# passed by name, and modified in place.  If the "-strict" option
697# is specified, unrecognized options are flagged as errors.
698# The call may include any number of option definitions, including
699# none.  The call "getoptions argv -strict", for example, will ensure
700# that no options remain in the list contained in "argv".
701#
702# Option definitions may take the following forms.  In each, NAME is
703# the option name, which must begin with a "-" character, and VAR is
704# the name of a variable in the caller's scope to receive the option's value.
705#
706# {NAME VAR flag}
707#     If the option appears on the command line, the variable
708#     is set to 1, otherwise to 0.
709#
710# {NAME VAR enum VAL1 VAL2....}
711#     If the option appears on the command line, the next argument
712#     must be one of the enumerated values, VAL1, VAL2, etc.  The 
713#     variable is set to the value, or VAL1 if the option does not
714#     appear on the command line.  If the option's value is not one of
715#     the valid choices, an error message will be displayed and the
716#     program will halt.  None of the enumerated values may begin with
717#     a "-" character.
718#
719# {NAME VAR string DEFVALUE}
720#     The named variable is set to the value following the option on
721#     the command line.  If the option doesn't appear, the variable is
722#     set to the DEFVALUE.  The option's value may not begin with 
723#     "-" character, as if it does, the most likely explanation is
724#     that the option's real value is missing and the next argument is
725#     another option name.
726
727# Utility routine: pops an arg off of the front of an arglist.
728proc ::expand::popArg {arglist} {
729    upvar $arglist args
730
731    if {[llength $args] == 0} {
732        set arg ""
733    } elseif {[llength $args] == 1} {
734        set arg $args
735        set args ""
736    } else {
737        set arg [lindex $args 0]
738        set args [lrange $args 1 end]
739    }
740
741    return $arg
742}
743
744proc ::expand::getoptions {arglist strictOrDefs {defsOrNil ""}} {
745    # First, the arglist is called by name.
746    upvar $arglist args
747
748    # Next, strictOrDefs is either the "-strict" option or the 
749    # definition list.
750    if {$strictOrDefs == "-strict"} {
751        set strictFlag 1
752        set defList $defsOrNil
753    } else {
754        set strictFlag 0
755        set defList $strictOrDefs
756    }
757
758    # Next, get names of the options
759    set optNames {}
760    set optTypes {flag enum string}
761    set optLens {3 5 4}
762    foreach def $defList {
763        if {[llength $def] < 3} {
764            error "Error in option definition: $def"
765        }
766        lappend optNames [lindex $def 0]
767        set varName [lindex $def 1]
768        set optType [lindex $def 2]
769        set i [lsearch -exact $optTypes $optType]
770
771        if {$i == -1} {
772            error "Unknown option type: $optType"
773        }
774
775        if {[llength $def] < [lindex $optLens $i]} {
776            error "Error in option definition: $def"
777        }
778
779        upvar $varName theVar
780        switch $optType {
781            flag {set theVar 0}
782            enum -
783            string {set theVar [lindex $def 3]}
784        }
785    }
786
787    # Next, process the options on the command line.
788    set errorCount 0
789    set newList {}
790    for {set arg [popArg args]} {$arg != ""} {set arg [popArg args]} {
791        # First, does it look like an option?  If not, add it to the
792        # output list.
793        if {[string index $arg 0] != "-"} {
794            lappend newList $arg
795            continue
796        }
797
798        # Next, Is the argument unknown?  Flag an error or just skip it.
799        set i [lsearch -exact $optNames $arg] 
800        if {$i == -1} {
801            if {$strictFlag} {
802                puts stderr "*** Unknown option: $arg"
803                incr errorCount
804            } else {
805                lappend newList $arg
806            }
807
808            continue
809        }
810
811        # Next, process the argument
812        set def [lindex $defList $i]
813        set varName [lindex $def 1]
814        set optType [lindex $def 2]
815
816        upvar $varName theVar
817        switch $optType {
818            flag {
819                set theVar 1
820            }
821
822            enum {
823                set vals [lreplace $def 0 2]
824                set theVar [popArg args]
825                if {$theVar == "" || [string index $theVar 0] == "-"} {
826                    puts stderr "*** Missing option value: $arg"
827                    incr errorCount
828                    continue
829                }
830                if {[lsearch -exact $vals $theVar] == -1} {
831                    puts stderr "*** Invalid option value: $arg $theVar"
832                    incr errorCount
833                }
834            }
835
836            string {
837                set theVar [popArg args]
838                if {$theVar == "" || [string index $theVar 0] == "-"} {
839                    puts stderr "*** Missing option value: $arg"
840                    incr errorCount
841                }
842            }
843        }
844    }
845
846    # Next, if there are any errors, halt.
847    if {$errorCount > 0} {
848        exit 1
849    }
850
851    # Next, return the new argument list.
852    set args $newList
853    return
854}
855
856#-------------------------------------------------------------------------
857# Importing macros into the global namespace
858
859# GlobalizeMacros args
860#
861# args is a list of glob patterns matching the macros to be imported.
862# The prefix ::expand:: is added automatically.
863
864proc ::expand::GlobalizeMacros {args} {
865    set globList {}
866
867    foreach arg $args {
868        lappend globList ::expand::$arg
869    }
870
871    namespace eval :: "namespace import -force $globList"
872}
873
874#-------------------------------------------------------------------------
875# Standard Rule Set: 
876#
877# These are the rules that are always available.
878
879proc ::expand::standardRuleSet {} {
880    GlobalizeMacros cget cis cname cpop cpush csave cvar expandText expfile
881    GlobalizeMacros exppass expwrite getoptions include lb popArg rb
882    GlobalizeMacros readFile setErrorOutputMode setbrackets setoutput
883    GlobalizeMacros setpasses textToID
884}
885
886#-------------------------------------------------------------------------
887# Rule Set: Web Rules
888#
889# These macros are for creating HTML pages.  They are only defined when
890# webRuleSet is called.
891
892proc ::expand::webRuleSet {} {
893    GlobalizeMacros dot tag link mailto today
894}
895
896# Output a big black dot.
897proc ::expand::dot {} {
898    return "&#149;"
899}
900
901# Format an html tag.  name is the tag name, args is a list of
902# of attribute names and values
903proc ::expand::tag {name args} {
904    set result "<$name"
905    foreach {attr val} $args {
906        append result " $attr=\"$val\""
907    }
908    append result ">"
909}
910
911# Format a link.  If text is given, use it as the displayed text;
912# otherwise use the url.
913proc ::expand::link {url {text ""}} {
914    if {$text == ""} {
915        set text $url
916    }
917    
918    return "[tag a href $url]$text[tag /a]"
919}
920
921# Format an email URL
922proc ::expand::mailto {address {name ""}} {
923    if {$name == ""} {
924        set name $address
925    }
926    
927    return "[tag a href mailto:$address]$name[tag /a]"
928}
929
930# Return today's date.  Use dd MONTH yyyy unless some other format is
931# proposed.
932proc ::expand::today {{format ""}} {
933    set secs [clock seconds]
934    
935    if {$format == ""} {
936        set format "%d %B %Y"
937    }
938    return [string trimleft [clock format $secs -format $format] "0"]
939}
940
941
942#-------------------------------------------------------------------------
943# Miscellaneous utility commands
944
945# oneOf list value
946#
947# Checks to see if a value is in a list.
948
949proc ::expand::oneOf {list value} {
950    return [expr {[lsearch -exact $list $value] != -1}]
951}
952
953# Converts a generic string to an ID string.  Leading and trailing
954# whitespace and internal punctuation is removed, internal whitespace
955# is converted to "_", and the text is converted to lower case.
956proc ::expand::textToID {text} {
957    # First, trim any white space and convert to lower case
958   set text [string trim [string tolower $text]]
959
960    # Next, substitute "_" for internal whitespace, and delete any
961    # non-alphanumeric characters (other than "_", of course)
962    regsub -all {[ ]+} $text "_" text
963    regsub -all {[^a-z0-9_]} $text "" text
964
965    return $text
966}
967
968#-------------------------------------------------------------------------
969# Main-line code:  This is the implementation of the Expand tool
970# itself.  It is executed only if this is the top-level script.
971
972proc ::expand::ShowHelp { } {
973    puts {tclsh expand.tcl [options] files...
974
975    -help           Displays this text.
976    -rules file     Specify the name of the rules file 
977                    (exprules.tcl is the default)
978    -out file       Specify the name of the output file, or "nul" for 
979                    no output.  Output is to stdout, by default.
980    -errout mode    nothing, macro, error, or fail (fail is the default)
981    -web            Enable the optional web rule set.
982    files...        Names of files to process.}
983}
984
985if {"[info script]" == "$argv0"} {
986
987    # First, parse the command line
988    ::expand::getoptions argv {
989        {-help      ::expand::helpFlag        flag}
990        {-errout    ::expand::errorOutputMode enum   fail nothing macro error}
991        {-rules     ::expand::rulesFile       string "exprules.tcl"}
992        {-web       ::expand::webFlag         flag}
993        {-out       ::expand::outputFile      string ""}
994    }
995
996    # Next, if they asked for help or if there are no arguments left,
997    # show help and stop.
998    if {$::expand::helpFlag || [llength $argv] == 0} {
999        ::expand::ShowHelp
1000        exit 0
1001    }
1002
1003    # Next, load the standard macros
1004    ::expand::standardRuleSet
1005
1006    # Next, load optional rule sets.
1007    if {$::expand::webFlag} {
1008        ::expand::webRuleSet
1009    }
1010
1011    # Next, load the rules file. (Should only do it if file exists;
1012    # should die if there are any errors)
1013    if {[file exists $::expand::rulesFile]} {
1014        if {[catch "source $::expand::rulesFile" result]} {
1015            puts "*** Error in rules file $::expand::rulesFile: $result"
1016            exit 1
1017        }
1018    } elseif {$::expand::rulesFile != "exprules.tcl"} {
1019        puts "*** Rules file $rulesFile not found."
1020        exit 1
1021    }
1022
1023    # Next, call the init_hook.
1024    if {[catch init_hook result]} {
1025        puts "*** Error executing init_hook: $result"
1026        exit 1
1027    }
1028
1029    # Next, make sure the command line contains no additional options
1030    ::expand::getoptions argv -strict
1031
1032    # Next, process the files
1033    ::expand::expand $argv $::expand::outputFile
1034}
1035
1036
1037