docnook.tcl: Add shebang
[oweals/cde.git] / cde / programs / dtdocbook / doc2sdl / docbook.tcl
1 #!/usr/bin/tclsh
2 # set the global variables
3 set nextId        0  ;# gets incremented before each use
4
5 set errorCount    0  ;# number of user errors
6 set warningCount  0  ;# number of user warnings
7
8 set havePartIntro 0  ;# need to emit a hometopic
9
10 set firstPInBlock 0  ;# allows a different SSI for first P
11 set inBlock       "" ;# holds "</BLOCK>\n" when in a BLOCK
12 set inVirpage     "" ;# holds "</VIRPAGE>\n" when in a VIRPAGE
13 set needFData     "" ;# holds "<FDATA>\n" if needed (starting a FORM)
14 set inP           0  ;# flag that we're in an SDL paragraph
15
16 set formStack     {} ;# need to stack FORMs when they contain others
17
18 set listStack     {} ;# holds type of list and spacing for ListItem
19
20 # create some constants for converting list count to ordered label
21 set ROMAN0   [list "" I II III IV V VI VII VIII IX]
22 set ROMAN10  [list "" X XX XXX XL L LX LXX LXXX XC]
23 set ROMAN100 [list "" C CC CCC CD D DC DCC DCCC CM]
24 set roman0   [list "" i ii iii iv v vi vii viii ix]
25 set roman10  [list "" x xx xxx xl l lx lxx lxxx xc]
26 set roman100 [list "" c cc ccc cd d dc dcc dccc cm]
27 set ALPHABET [list "" A B C D E F G H I J K L M N O P Q R S T U V W X Y Z]
28 set alphabet [list "" a b c d e f g h i j k l m n o p q r s t u v w x y z]
29 set DIGITS   [list  0 1 2 3 4 5 6 7 8 9]
30 set NZDIGITS [list "" 1 2 3 4 5 6 7 8 9]
31
32 # specify the "level" value to be given to VIRPAGEs (based on SSI);
33 # the indexes for this associative array are also used to determine
34 # whether the closing of a DocBook Title should re-position the
35 # snbLocation (because the SNB follows HEADs, if any)
36 set virpageLevels(FOOTNOTE)     0
37 set virpageLevels(TITLE)        0
38 set virpageLevels(AUTHORGROUP)  0
39 set virpageLevels(ABSTRACT)     0
40 set virpageLevels(REVHISTORY)   0
41 set virpageLevels(LEGALNOTICE)  0
42 set virpageLevels(PARTINTRO)    1
43 set virpageLevels(CHAPTER)      2
44 set virpageLevels(APPENDIX)     2
45 set virpageLevels(BIBLIOGRAPHY) 2
46 set virpageLevels(GLOSSARY)     2
47 set virpageLevels(INDEX)        2
48 set virpageLevels(LOT)          2
49 set virpageLevels(PREFACE)      2
50 set virpageLevels(REFENTRY)     2
51 set virpageLevels(REFERENCE)    2
52 set virpageLevels(TOC)          2
53 set virpageLevels(SECT1)        3
54 set virpageLevels(SECT2)        4
55 set virpageLevels(SECT3)        5
56 set virpageLevels(SECT4)        6
57 set virpageLevels(SECT5)        7
58
59 # assume the first ID used is SDL-RESERVED1 - if we get a INDEXTERM
60 # before anything has started, default to the assumed ID
61 set mostRecentId "SDL-RESERVED1"
62
63 # a counter for use in pre-numbering footnotes - will create an
64 # associative array indexed by "FOOTNOTE ID=" values to hold
65 # the number of the FOOTNOTE for use by FOOTNOTEREF
66 set footnoteCounter 0
67
68 # the absolute byte offset into the output file where the SNB should be
69 # inserted by the second pass - the location and snb get saved at
70 # the end of each VIRPAGE with a little special handling for the
71 # SDLDOC SNB, the entire snb gets written to the .snb file at
72 # the close of the document after the saved locations get incremented
73 # by the size of the index
74 set snbLocation 0
75
76 # normally, we dafault paragraphs to no TYPE= attribute; when in an
77 # EXAMPLE, for instance, we need to default to TYPE="LITERAL"
78 set defaultParaType ""
79
80
81 # print internal error message and exit
82 proc InternalError {what} {
83     global errorInfo
84
85     error $what
86 }
87
88
89 # print a warning message
90 proc UserWarning {what location} {
91     global warningCount
92
93     puts stderr "DtDocBook User Warning: $what"
94     if {$location} {
95         PrintLocation
96     }
97     incr warningCount
98 }
99
100
101 # print an error message plus the location in the source file of the
102 # error; if we get more than 100 errors, quit
103 proc UserError {what location} {
104     global errorCount
105
106     puts stderr "DtDocBook User Error: $what"
107     if {$location} {
108         PrintLocation
109     }
110     if {[incr errorCount] >= 100} {
111         puts stderr "Too many errors - quitting"
112         exit 1
113     }
114 }
115
116
117 # set up a default output string routine so everything works even
118 # if run outside of instant(1)
119 if {[info commands OutputString] == ""} {
120     proc OutputString {string} {
121         puts -nonewline "$string"
122     }
123 }
124
125
126 # set up a default string compare routine so everything works even
127 # if run outside of instant(1); it won't really be i18n safe, but
128 # it'll give us a dictionary sort
129 if {[info commands CompareI18NStrings] == ""} {
130     proc CompareI18NStrings {string1 string2} {
131         set string1 [string toupper $string1]
132         set string2 [string toupper $string2]
133         if {$string1 > $string2} {
134             return 1
135         } else if {$string1 < $string2} {
136             return -1
137         } else {
138             return 0
139         }
140     }
141 }
142
143
144 # emit a string to the output stream
145 proc Emit {string} {
146     OutputString $string
147 }
148
149
150 # push an item onto a stack (a list); return item pushed
151 proc Push {stack item} {
152     upvar $stack s
153     lappend s $item
154     return $item
155 }
156
157
158 # pop an item from a stack (i.e., a list); return the popped item
159 proc Pop {stack} {
160     upvar $stack s
161     set top [llength $s]
162     if {!$top} {
163         InternalError "Stack underflow in Pop"
164     }
165     incr top -1
166     set item [lindex $s $top]
167     incr top -1
168     set s [lrange $s 0 $top]
169     return $item
170 }
171
172
173 # return the top of a stack (the stack is a list)
174 proc Peek {stack} {
175     upvar $stack s
176     set top [llength $s]
177     incr top -1
178     set item [lindex $s $top]
179 }
180
181
182 # replace the top of the stack with the new item; return the item
183 proc Poke {stack item} {
184     upvar $stack s
185     set top [llength $s]
186     incr top -1
187     set s [lreplace $s $top $top $item]
188     return $item
189 }
190
191
192 # emit an ID and save it for reference as the most recently emitted ID;
193 # the saved value will be used to mark locations for index entries
194 proc Id {name} {
195     global mostRecentId
196
197     set mostRecentId $name
198     return "ID=\"$name\""
199 }
200
201
202 # emit an ANCHOR into the SDL stream; if the passed id is empty, don't
203 # emit the anchor
204 proc Anchor {id} {
205     if {$id != ""} {
206         Emit "<ANCHOR [Id $id]>"
207     }
208 }
209
210
211 # emit an ANCHOR into the SDL stream; if the passed id is empty, don't
212 # emit the anchor; if we're not in an SDL P yet, start one and use
213 # the id there rather than emitting an SDL ANCHOR
214 proc AnchorInP {id} {
215     global inP
216
217     if {$id != ""} {
218         if {!$inP} {
219             StartParagraph $id "P" ""
220         } else {
221             Emit "<ANCHOR [Id $id]>"
222         }
223     }
224 }
225
226
227 # set up containers for the IDs of the blocks holding marks - clear
228 # on entry to each <virpage> but re-use within the <virpage> as much as
229 # possible; we need two each of the regular and loose versions because
230 # we need to alternate to avoid the <form> runtime code thinking we're
231 # trying to span columns
232 #
233 # specify a routine to (re-)initialize all the variables for use
234 # in ListItem
235 proc ReInitPerMarkInfo {} {
236     global validMarkArray
237
238     foreach mark [array names validMarkArray] {
239         global FIRSTTIGHT${mark}Id
240         set    FIRSTTIGHT${mark}Id  ""
241
242         global FIRSTLOOSE${mark}Id
243         set    FIRSTLOOSE${mark}Id  ""
244
245         global TIGHT${mark}Id0
246         set    TIGHT${mark}Id0 ""
247
248         global TIGHT${mark}Id1
249         set    TIGHT${mark}Id1 ""
250
251         global LOOSE${mark}Id0
252         set    LOOSE${mark}Id0 ""
253
254         global LOOSE${mark}Id1
255         set    LOOSE${mark}Id1 ""
256
257         global TIGHT${mark}num
258         set    TIGHT${mark}num 1
259
260         global LOOSE${mark}num
261         set    LOOSE${mark}num 1
262     }
263 }
264
265
266 # add a new mark to the mark array and initialize all the variables
267 # that depend on the mark; the index for the mark is just the mark
268 # itself with the square brackets removed and whitespace deleted;
269 # we've already guaranteed that the mark will be of the form
270 # "[??????]" (open-square, 6 characters, close-square) and that this
271 # mark isn't in the array already
272 proc AddToMarkArray {mark} {
273     global validMarkArray
274
275     set m [string range $mark 1 6]
276     set m [string trim $m]
277
278     set validMarkArray($m) $mark
279
280     global FIRSTTIGHT${m}Id
281     set    FIRSTTIGHT${m}Id  ""
282
283     global FIRSTLOOSE${m}Id
284     set    FIRSTLOOSE${m}Id  ""
285
286     global TIGHT${m}Id0
287     set    TIGHT${m}Id0 ""
288
289     global TIGHT${m}Id1
290     set    TIGHT${m}Id1 ""
291
292     global LOOSE${m}Id0
293     set    LOOSE${m}Id0 ""
294
295     global LOOSE${m}Id1
296     set    LOOSE${m}Id1 ""
297
298     global TIGHT${m}num
299     set    TIGHT${m}num 1
300
301     global LOOSE${m}num
302     set    LOOSE${m}num 1
303
304     return $m
305 }
306
307
308 # start a new paragraph; start a block if necessary
309 proc StartParagraph {id ssi type} {
310     global inBlock firstPInBlock inP defaultParaType
311
312     # close any open paragraph
313     if {$inP} { Emit "</P>\n" }
314
315     # if not in a BLOCK, open one
316     if {$inBlock == ""} { StartBlock "" "" "" 1 }
317
318     Emit "<P"
319     if {$id != ""} { Emit " [Id $id]" }
320
321     # don't worry about whether we're the first para if there's no SSI
322     if {$ssi != ""} {
323         set firstString ""
324         if {$firstPInBlock} {
325             if {$ssi == "P"} {
326                 set firstString 1
327             }
328             set firstPInBlock 0
329         }
330         Emit " SSI=\"$ssi$firstString\""
331     }
332
333     if {$type == ""} {
334         Emit $defaultParaType
335     } else {
336         Emit " TYPE=\"$type\""
337     }
338
339     Emit ">"
340
341     set inP 1
342     set inBlock "</P>\n</BLOCK>\n"
343 }
344
345
346 # conditionally start a paragraph - that is, only start a new
347 # paragraph if we aren't in one already
348 proc StartParagraphMaybe {id ssi type} {
349     global inP
350
351     if {$inP} {
352         Anchor $id
353     } else {
354         StartParagraph $id $ssi $type
355     }
356 }
357
358
359 # start a compound paragraph - a compound paragraph is when a Para
360 # contains some other element that requires starting its own SDL
361 # BLOCK or FORM, e.g., VariableList; we need to create a FORM to hold
362 # the Para and its parts - put the id and ssi on the FORM rather than
363 # the contained Ps.
364 proc StartCompoundParagraph {id ssi type} {
365     global firstPInBlock
366
367     if {$ssi != ""} {
368         if {$firstPInBlock} {
369             set firstString 1
370         } else {
371             set firstString ""
372         }
373         PushForm "" $ssi$firstString $id
374     } else {
375         PushForm "" "" $id
376     }
377
378     StartParagraph "" "" ""
379 }
380
381
382 # given the path of parentage of an element, return its n'th ancestor
383 # (parent == 1), removing the child number (if any); e.g., convert
384 # "PART CHAPTER(0) TITLE" into "CHAPTER" if level is 2
385 proc Ancestor {path level} {
386     if {$level < 0} { return "_UNDERFLOW_" }
387
388     set last [llength $path]
389     incr last -1
390
391     if {$level > $last} { return "_OVERFLOW_" }
392
393     # invert "level" about "last" so we count from the end
394     set level [expr "$last - $level"]
395
396     set parent [lindex $path $level]
397     set parent [lindex [split $parent "("] 0] ;# remove child #
398 }
399
400
401 # start a HEAD element for the DocBook Title - use the parent's
402 # GI in the SSI= of the HEAD except that all titles to things in
403 # their own topic (VIRPAGE) will use an SSI of CHAPTER-TITLE;
404 # if we are in a topic with a generated id (e.g., _glossary or
405 # _title), we might have saved an id or two in savedId to be
406 # emitted in the HEAD
407 proc Title {id parent} {
408     global virpageLevels partID inP savedId
409
410     Emit "<HEAD"
411
412     if {$id != ""} {
413         Emit " ID=\"$id\""
414     }
415
416     # if we are the Title of a PartIntro, we'd like to emit the
417     # partID as an anchor so linking to the volume will succeed;
418     # add it to the list of saved ids to be emitted
419     if {$parent == "PARTINTRO"} {
420         lappend savedId $partID
421     }
422
423     # make the HEAD for all topics (VIRPAGE) have an SSI of
424     # "CHAPTER-HEAD", use LEVEL to distinguish between them
425     set topicNames [array names virpageLevels]
426     foreach name $topicNames {
427         if {$parent == $name} {
428             set parent CHAPTER
429             break
430         }
431     }
432
433     Emit " SSI=\"$parent-TITLE\">"
434
435     # being in a HEAD is equivalent to being in a P for content model
436     # but we use "incr" instead of setting inP directly so that if we
437     # are in a P->HEAD, we won't prematurely clear inP when leaving
438     # the HEAD
439     incr inP
440
441     if {[info exists savedId]} {
442         foreach id $savedId {
443             Anchor $id
444         }
445         unset savedId
446     }
447 }
448
449
450 # close a HEAD element for a DocBook Title - if the Title is one for
451 # a DocBook element that gets turned into an SDL VIRPAGE, set the
452 # location for the insertion of an SNB (if any) to follow the HEAD
453 proc CloseTitle {parent} {
454     global snbLocation virpageLevels inP
455
456     Emit "</HEAD>\n"
457
458     # we incremented inP on entry to the HEAD so decrement it here
459     incr inP -1
460
461     # get a list of DocBook elements that start VIRPAGEs
462     set names [array names virpageLevels]
463
464     # add the start of the help volume, PART, to the list
465     lappend names PART
466
467     # if our parent is a VIRPAGE creator or the start of the document,
468     # we must be dealing with the heading of a VIRPAGE or with the
469     # heading of the SDLDOC so move the spot where we want to include
470     # the SNB to immediately after this HEAD
471     foreach name $names {
472         if {$name == $parent} {
473             set snbLocation [tell stdout]
474             break
475         }
476     }
477 }
478
479
480 # open an SGML tag - add punctuation as guided by the class attribute
481 proc StartSgmlTag {id class} {
482     switch $class {
483         ELEMENT     {set punct "&<"}
484         ATTRIBUTE   {set punct ""}
485         GENENTITY   {set punct "&&"}
486         PARAMENTITY {set punct "%"}
487     }
488     Emit $punct
489 }
490
491
492 # close an SGML tag - add punctuation as guided by the class attribute
493 proc EndSgmlTag {class} {
494     switch $class {
495         ELEMENT     {set punct ">"}
496         ATTRIBUTE   {set punct ""}
497         GENENTITY   {set punct ";"}
498         PARAMENTITY {set punct ";"}
499     }
500     Emit $punct
501 }
502
503
504 # end a trademark, append a symbol if needed
505 proc EndTradeMark {class} {
506     switch $class {
507         SERVICE    {set punct ""}
508         TRADE      {set punct "<SPC NAME=\"\[trade \]\">"}
509         REGISTERED {set punct "<SPC NAME=\"\[reg   \]\">"}
510         COPYRIGHT  {set punct "<SPC NAME=\"\[copy  \]\">"}
511     }
512     Emit "</KEY>$punct"
513 }
514
515
516 # handle the BridgeHead tag; emit a FORM to hold a HEAD and put the
517 # BridgeHead there - use the procedure Title to do all the work, the
518 # renderas attributre simply become the parent to Title
519 proc StartBridgeHead {id renderas} {
520     PushForm "" "" ""
521
522     # default renderas to CHAPTER - arbitrarily
523     if {$renderas == "OTHER"} {
524         set renderas CHAPTER
525     }
526     Title $id $renderas
527 }
528
529
530 # end a BridgeHead; we need to close out the SDL HEAD and close the
531 # FORM - use CloseTitle to close out the HEAD but give it a null
532 # parent so it doesn't try to save the SNB now
533 proc EndBridgeHead {} {
534     CloseTitle ""
535     PopForm
536 }
537
538
539 # end a paragraph
540 proc EndParagraph {} {
541     global inP inBlock
542
543     if {$inP} {
544         Emit "</P>\n"
545     }
546
547     # we set inBlock to </P></BLOCK> in StartParagraph so we need
548     # to remove the </P> here; if we're continuing a paragraph
549     # inBlock will have been set to "" when we closed the BLOCK to
550     # open the embedded FORM so we need to leave it empty to cause
551     # a new BLOCK to be opened
552     if {$inBlock != ""} {
553         set inBlock "</BLOCK>\n"
554     }
555
556     # and flag that we're not in a paragraph anymore
557     set inP 0
558 }
559
560
561 # continue a PARA that was interrupted by something from %object.gp;
562 # first pop the FORM that held the indent attributes for the object
563 # then start a new paragraph with an SSI that indicates we are
564 # continuing
565 proc ContinueParagraph {} {
566     PopForm
567     StartParagraph "" "P-CONT" ""
568 }
569
570
571 # start a new BLOCK element; close the old one, if any;
572 # return the ID in case we allocated one and someone else wants it
573 proc StartBlock {class ssi id enterInForm} {
574     global needFData inBlock formStack nextId firstPInBlock inP
575
576     # if we are the first BLOCK in a FORM, emit the FDATA tag
577     Emit $needFData; set needFData ""
578
579     # close any open block and flag that we're opening one
580     # but that we haven't seen a paragraph yet
581     Emit $inBlock
582     set inBlock "</BLOCK>\n"
583     set inP 0
584
585     # if a FORM is in progress, add our ID to the row vector,
586     # FROWVEC - create an ID if one wasn't provided
587     if {$enterInForm && [llength $formStack] != 0} {
588         if {$id == ""} { set id "SDL-RESERVED[incr nextId]" }
589         AddRowVec $id
590     }
591
592     # open the BLOCK
593     Emit "<BLOCK"
594     if {$id    != ""} { Emit " [Id $id]" }
595     if {$class != ""} { Emit " CLASS=\"$class\"" }
596     if {$ssi   != ""} { Emit " SSI=\"$ssi\"" }
597     Emit ">\n"
598
599     # and flag that the next paragraph is the first in a block
600     set firstPInBlock 1
601
602     return $id
603 }
604
605
606 # close any open BLOCK - no-op if not in a BLOCK otherwise emit the
607 # BLOCK etag or both BLOCK and P etags if there's an open paragraph
608 proc CloseBlock {} {
609     global inBlock inP
610
611     if {$inBlock != ""} {
612         Emit $inBlock   ;# has been prefixed with </P> if needed
613         set inBlock ""
614         set inP 0
615     }
616 }
617
618
619 # add another FROWVEC element to the top of the form stack
620 proc AddRowVec {ids} {
621     global formStack
622
623     Push formStack "[Pop formStack]<FROWVEC CELLS=\"$ids\">\n"
624 }
625
626
627 # start a new FORM element within a THead, TBody or TFoot ("push"
628 # because they're recursive); return the ID in case we allocated one;
629 # do not enter the ID in the parent's FROWVEC, we'll do that later
630 # from the rowDope that we build to compute horizontal spans and
631 # vertical straddles
632 proc PushFormCell {ssi id} {
633     global needFData formStack nextId
634
635     Emit $needFData     ;# in case we're the first in an old FORM
636     set needFData "<FDATA>\n" ;# and were now starting a new FORM
637
638     # close any open BLOCK
639     CloseBlock
640
641     # make sure we have an ID
642     if {$id == ""} { set id "SDL-RESERVED[incr nextId]" }
643
644     # add a new (empty) string to the formStack list (i.e., push)
645     Push formStack {}
646
647     Emit "<FORM"
648     if {$id  != ""} { Emit " [Id $id]" }
649     Emit " CLASS=\"CELL\""
650     if {$ssi != ""} { Emit " SSI=\"$ssi\"" }
651     Emit ">\n"
652
653     return $id
654 }
655
656
657 # start a new FORM element ("push" because they're recursive);
658 # return the ID in case we allocated one
659 proc PushForm {class ssi id} {
660     global needFData formStack nextId
661
662     Emit $needFData     ;# in case we're the first in an old FORM
663     set needFData "<FDATA>\n" ;# and were now starting a new FORM
664
665     # close any open BLOCK
666     CloseBlock
667
668     if {[llength $formStack] != 0} {
669         # there is a <form> in progress
670         if {$id == ""} { set id "SDL-RESERVED[incr nextId]" }
671         AddRowVec $id
672     }
673
674     # add a new (empty) string to the formStack list (i.e., push)
675     Push formStack {}
676
677     Emit "<FORM"
678     if {$id    != ""} { Emit " [Id $id]" }
679     if {$class != ""} { Emit " CLASS=\"$class\"" }
680     if {$ssi   != ""} { Emit " SSI=\"$ssi\"" }
681     Emit ">\n"
682
683     return $id
684 }
685
686
687 # start a new FORM element to hold a labeled list item ("push"
688 # because they're recursive), adding it to an already open two
689 # column FORM, if any; we assume the first ID is the block holding
690 # the label and always defined on entry but we return the second
691 # ID in case we allocated one
692 proc PushFormItem {ssi id1 id2} {
693     global needFData formStack nextId
694
695     Emit $needFData ;# in case we're the first in an old FORM
696     set needFData "<FDATA>\n"  ;# and were now starting a new FORM
697
698     # close any open BLOCK
699     CloseBlock
700
701     if {$id2 == ""} { set id2 "SDL-RESERVED[incr nextId]" }
702
703     if {[llength $formStack] != 0} {
704         # there is a <form> in progress
705         if {$id2 == ""} { set id2 "SDL-RESERVED[incr nextId]" }
706         AddRowVec "$id1 $id2"
707     }
708
709     # add a new (empty) string to the formStack list (i.e., push)
710     Push formStack {}
711
712     Emit "<FORM [Id $id2] CLASS=\"ITEM\""
713     if {$ssi   != ""} { Emit " SSI=\"$ssi\"" }
714     Emit ">\n"
715
716     return $id2
717 }
718
719
720 # close out a THead, TBody or TFoot; create the FROWVEC from the
721 # rowDope - save it if we aren't popping the FORM yet (which happens
722 # if no ColSpec elements were given at the THead or TFoot level and
723 # we're merging one, the other or both with the TBody), emit the
724 # saved ROWVEC, if any, and newly created one if we are popping the
725 # FORM in which case we also want to blow away the top of the
726 # formStack; we can also blow away the current rowDope here since
727 # we write or save the FROWVEC and we're done with the dope vector
728 proc PopTableForm {parent gi popForm} {
729     global formStack
730
731     # get the proper row descriptor(s) and number of columns
732     if {$parent == "ENTRYTBL"} {
733         upvar #0 entryTableRowDope      rowDope
734         upvar #0 entryTableSavedFRowVec fRowVec
735         global entryTableAttributes
736         set nCols $entryTableAttributes(cols)
737     } else {
738         upvar #0 tableGroupRowDope      rowDope
739         upvar #0 tableGroupSavedFRowVec fRowVec
740         global tableGroupAttributes
741         set nCols $tableGroupAttributes(cols)
742     }
743
744     # flush the unused formStack entry if we're actually popping
745     if {$popForm} {
746         Pop formStack
747     }
748
749     # determine whether we are a "header", i.e., inside a TFoot or
750     # THead
751     if {$gi == "TBODY"} {
752         set hdr ""
753     } else {
754         set hdr " HDR=\"YES\""
755     }
756
757     # if actually popping the FORM here (i.e., writing the FSTYLE),
758     # emit the FSTYLE wrapper
759     if {$popForm} {
760         Emit "</FDATA>\n<FSTYLE"
761         if {$nCols > 1} {
762             Emit " NCOLS=\"$nCols\""
763         }
764         Emit ">\n"
765     }
766     set currentRow 1
767     set nRows $rowDope(nRows)
768     while {$currentRow <= $nRows} {
769         append fRowVec "<FROWVEC$hdr CELLS=\""
770         append fRowVec $rowDope(row$currentRow)
771         append fRowVec "\">\n"
772         incr currentRow
773     }
774     unset rowDope
775     # if actually popping the FORM here (i.e., writing the FSTYLE),
776     # emit the FROWVEC elements, zero out the saved fRowVec and close
777     # the FSTYLE wrapper
778     if {$popForm} {
779         Emit $fRowVec
780         set fRowVec ""
781         Emit "</FSTYLE>\n</FORM>\n"
782     }
783 }
784
785
786 # close out one FORM on the stack; if there hasn't been a block added
787 # to the FORM, create an empty one to make it legal SDL
788 proc PopForm {} {
789     global formStack
790
791     if {[Peek formStack] == ""} {
792         # oops, empty FROWVEC means empty FORM so add an empty BLOCK
793         StartBlock "" "" "" 1
794     }
795
796     # close any open BLOCK
797     CloseBlock
798
799     # write out the saved FROWVEC information wrapped in an FSTYLE
800     set openStyle "</FDATA>\n<FSTYLE>\n"
801     set closeStyle "</FSTYLE>\n</FORM>"
802     Emit "$openStyle[Pop formStack]$closeStyle\n"
803 }
804
805
806 # close out one N columned FORM on the stack; if there hasn't been a
807 # block added to the FORM, create an empty one to make it legal SDL
808 proc PopFormN {nCols} {
809     global formStack
810
811     if {[Peek formStack] == ""} {
812         # oops, empty FROWVEC means empty FORM so add an empty BLOCK
813         # and bring this down to a single column FORM containing only
814         # the new BLOCK
815         StartBlock "" "" "" 1
816         set nCols 1
817     }
818
819     # close any open BLOCK
820     CloseBlock
821
822     # write out the saved FROWVEC information wrapped in an FSTYLE
823     set openStyle "</FDATA>\n<FSTYLE NCOLS=\"$nCols\">\n"
824     set closeStyle "</FSTYLE>\n</FORM>"
825     Emit "$openStyle[Pop formStack]$closeStyle\n"
826 }
827
828
829 # check the Role attribute on lists to verify that it's either "LOOSE"
830 # or "TIGHT"; return upper cased version of verified Role
831 proc CheckSpacing {spacing} {
832     set uSpacing [string toupper $spacing]
833     switch $uSpacing {
834         LOOSE   -
835         TIGHT   {return $uSpacing}
836     }
837     UserError "Bad value (\"$role\") for Role attribute in a list" yes
838     return LOOSE
839 }
840
841
842 # start a simple list - if Type is not INLINE, we need to save the
843 # Ids of the BLOCKs we create and lay them out in a HORIZONTAL or
844 # VERTICAL grid when we have them all
845 proc StartSimpleList {id type spacing parent} {
846     global listStack firstString
847
848     if {$type == "INLINE"} {
849         StartParagraphMaybe $id P ""
850     } else {
851         # if we are inside a Para, we need to issue a FORM to hang the
852         # indent attributes on
853         if {$parent == "PARA"} {
854             PushForm "" "INSIDE-PARA" ""
855         }
856
857         # insure "spacing" is upper case and valid (we use it in the SSI)
858         set spacing [CheckSpacing $spacing]
859
860         # save the list type and spacing for use by <Member>;
861         set listDope(type)     simple
862         set listDope(spacing) $spacing
863         Push listStack [array get listDope]
864
865         PushForm LIST SIMPLE-$spacing $id
866         set firstString "FIRST-"
867     }
868 }
869
870
871 # end a simple list - if Type was INLINE, we're done, otherwise, we
872 # need to lay out the grid based on Type and Columns
873 proc EndSimpleList {columns type parent} {
874     global listStack lastList listMembers
875
876     if {$columns == 0} {
877         UserWarning "must have at least one column in a simple list" yes
878         set columns 1
879     }
880
881     if {$type != "INLINE"} {
882         # get the most recently opened list and remove it from the stack
883         array set lastList [Pop listStack]
884
885         # calculate the number of rows and lay out the BLOCK ids
886         # as per the type attribute
887         set length [llength $listMembers]
888         set rows   [expr ($length + $columns - 1) / $columns]
889         set c 0
890         set r 0
891         set cols $columns
892         if {$type == "HORIZ"} {
893             incr cols -1
894             while {$r < $rows} {
895                 set ids [lrange $listMembers $c [incr c $cols]]
896                 AddRowVec $ids
897                 incr c
898                 incr r
899             }
900         } else {
901             set lastRowLength [expr $cols - (($rows * $cols) - $length)]
902             incr rows -1
903             while {$r <= $rows} {
904                 set i   $r
905                 set ids ""
906                 set c   0
907                 if {$r == $rows} {
908                     set cols $lastRowLength
909                 }
910                 while {$c < $cols} {
911                     lappend ids [lindex $listMembers $i]
912                     incr i $rows
913                     if {$c < $lastRowLength} {
914                         incr i
915                     }
916                     incr c
917                 }
918                 AddRowVec $ids
919                 incr r
920             }
921         }
922         unset listMembers
923
924         # close the open FORM using the newly generated ROWVECs
925         PopFormN $columns
926
927         # if we are inside a Para, we need to close the FORM we issued for
928         # hanging the indent attributes
929         if {$parent == "PARA"} {
930             ContinueParagraph
931         }
932     }
933 }
934
935
936 # collect another Member of a SimpleList; if we're a Vert(ical) or
937 # Horiz(ontal) list, don't put the BLOCK's id on the list's FORM
938 # yet - we need to collect them all and lay them out afterward in
939 # EndSimpleList; if we're an Inline list, don't create a BLOCK, we'll
940 # add punctuation to separate them in EndMember
941 proc StartMember {id type} {
942     global nextId listStack firstString listMembers
943
944     if {$type == "INLINE"} {
945         Anchor $id
946     } else {
947         # put it in a BLOCK, make sure we have an id and add it to
948         # the list of members
949         if {$id == ""} {
950             set id SDL-RESERVED[incr nextId]
951         }
952         lappend listMembers $id
953
954         # get the current list info
955         array set listTop [Peek listStack]
956         set spacing $listTop(spacing)
957
958         # use an SSI of, e.g., FIRST-LOOSE-SIMPLE
959         StartBlock ITEM $firstString$spacing-SIMPLE $id 0
960         StartParagraph "" P ""
961         set firstString ""
962     }
963 }
964
965
966 # end a SimpleList Member; if it's an Inline list, emit the
967 # punctuation ("", ", " or "and") based on the position of the
968 # Member in the list - otherwise, do nothing and the StartBlock from
969 # the next Member or the PopFormN in EndSimpleList will close the
970 # current one out
971 proc EndMember {type punct} {
972     if {$type == "INLINE"} {
973         Emit $punct
974     }
975 }
976
977
978 # check the value of a ITEMIZEDLIST MARK - issue warning and default 
979 # it to BULLET if it's unrecognized
980 proc ValidMark {mark} {
981     global validMarkArray
982
983     if {[string toupper $mark] == "PLAIN"} { return PLAIN }
984
985     # if an SDATA entity was used, it'll have spurious "\|" at the
986     # beginning and the end added by [n]sgmls
987     if {[string match {\\|????????\\|} $mark]} {
988         set mark [string range $mark 2 9]
989     }
990
991     if {![string match {\[??????\]} $mark]} {
992         UserError "Unknown list mark \"$mark\" specified, using PLAIN" yes
993         return PLAIN
994     } else {
995         foreach m [array names validMarkArray] {
996             if {$validMarkArray($m) == $mark} {return $m}
997         }
998         return [AddToMarkArray $mark]
999     }
1000 }
1001
1002
1003 # start an itemized list
1004 proc ItemizedList {id mark spacing parent} {
1005     global listStack firstString
1006
1007     # if we are inside a Para, we need to issue a FORM to hang the
1008     # indent attributes on
1009     if {$parent == "PARA"} {
1010         PushForm "" "INSIDE-PARA" ""
1011     }
1012
1013     # make sure we recognize the mark
1014     set mark [ValidMark $mark]
1015
1016     # insure "spacing" is upper case and valid (we use it in the SSI)
1017     set spacing [CheckSpacing $spacing]
1018
1019     # save the list type, mark and spacing for use by <ListItem>
1020     set listDope(type)   itemized
1021     set listDope(spacing) $spacing
1022     set listDope(mark)    $mark
1023     Push listStack [array get listDope]
1024
1025     # create a FORM to hold the list items
1026     if {$mark == "PLAIN"} {
1027         PushForm LIST "PLAIN-$spacing" $id
1028     } else {
1029         PushForm LIST "MARKED-$spacing" $id
1030     }
1031
1032     set firstString "FIRST-"
1033 }
1034
1035
1036 # turn absolute item count into proper list number e.g., 2, B, or II
1037 proc MakeOrder {numeration count} {
1038     global ROMAN0 ROMAN10 ROMAN100
1039     global roman0 roman10 roman100
1040     global ALPHABET alphabet
1041     global NZDIGITS DIGITS
1042
1043     if {$count == ""} { return "" }
1044
1045     if {$count > 999} { set count 999 } ;# list too big - cap it
1046
1047     # initialize the 3 digits of the result value
1048     set c1 0
1049     set c2 0
1050     set c3 0
1051
1052     # first get the 3 digits in the proper base (26 or 10)
1053     switch -exact $numeration {
1054         UPPERALPHA -
1055         LOWERALPHA {
1056             set c3 [expr "$count % 26"]
1057             if {$c3 == 0} { set c3 26 }
1058             if {[set count [expr "$count / 26"]]} {
1059                 set c2 [expr "$count % 26"]
1060                 if {$c2 == 0} { set c2 26 }
1061                 set c1 [expr "$count / 26"]
1062             }
1063         }
1064         UPPERROMAN -
1065         LOWERROMAN -
1066         default {
1067             set c3 [expr "$count % 10"]
1068             if {[set count [expr "$count / 10"]]} {
1069                 set c2 [expr "$count % 10"]
1070                 if {[set count [expr "$count / 10"]]} {
1071                     set c1 [expr "$count % 10"]
1072                 }
1073             }
1074         }
1075     }
1076
1077     # then point to proper conversion list(s)
1078     switch -exact $numeration {
1079         UPPERALPHA {
1080             set c1List $ALPHABET
1081             set c2List $ALPHABET
1082             set c3List $ALPHABET
1083         }
1084         LOWERALPHA {
1085             set c1List $alphabet
1086             set c2List $alphabet
1087             set c3List $alphabet
1088         }
1089         UPPERROMAN {
1090             set c3List $ROMAN0
1091             set c2List $ROMAN10
1092             set c1List $ROMAN100
1093         }
1094         LOWERROMAN {
1095             set c3List $roman0
1096             set c2List $roman10
1097             set c1List $roman100
1098         }
1099         default {
1100             set c1List $DIGITS
1101             set c2List $DIGITS
1102             set c3List $DIGITS
1103             if {$c1 == 0} {
1104                 set c1List $NZDIGITS
1105                 if {$c2 == 0} {
1106                     set c2List $NZDIGITS
1107                 }
1108             }
1109         }
1110     }
1111
1112 # and do the conversion
1113 set    string [lindex $c1List $c1]
1114 append string [lindex $c2List $c2]
1115 append string [lindex $c3List $c3]
1116 append string .
1117
1118 return $string
1119 }
1120
1121
1122 # start an ordered list
1123 proc OrderedList {id numeration inheritNum continue spacing parent} {
1124     global listStack lastList firstString
1125
1126     # if we are inside a Para, we need to issue a FORM to hang the
1127     # indent attributes on
1128     if {$parent == "PARA"} {
1129         PushForm "" "INSIDE-PARA" ""
1130     }
1131
1132     # make sure the INHERIT param is compatible with enclosing list
1133     if {$inheritNum == "INHERIT"} {
1134         if {[llength $listStack] > 0} {
1135             array set outerList [Peek listStack]
1136             if {$outerList(type) != "ordered"} {
1137                 UserError "Can only inherit numbering from an ordered list" yes
1138                 set inheritNum IGNORE
1139             }
1140         } else {
1141             UserError \
1142                   "Attempt to inherit a list number with no previous list" yes
1143             set inheritNum IGNORE
1144         }
1145     }
1146
1147     # make sure the CONTINUE param is compatible with previous list;
1148     # also inherit numeration here if unset (else error if different)
1149     # and we're continuing
1150     if {$continue == "CONTINUES"} {
1151         if {![array exists lastList]} {
1152             # nothing to inherit from
1153             UserError "Attempt to continue a list with no previous list" yes
1154             set continue RESTARTS
1155         } elseif {$lastList(type) != "ordered"} {
1156             UserError "Only ordered lists can be continued" yes
1157             set continue RESTARTS
1158         } elseif {$numeration == ""} {
1159             set numeration $lastList(numeration)
1160         }  elseif {$lastList(numeration) != $numeration} {
1161             UserError "Can't continue a list with different numeration" yes
1162             set continue RESTARTS
1163         }
1164     }
1165
1166     # if no numeration specified, default to Arabic
1167     if {$numeration == ""} {
1168         set numeration ARABIC
1169     }
1170
1171     set count 0          ;# assume we are restarting the item count
1172     set inheritString "" ;# fill in later if set
1173
1174     if {$continue == "CONTINUES"} {
1175         # continuing means use the old inherit string (if any) and
1176         # pick up with the old count
1177         set count $lastList(count)
1178         if {($lastList(inheritString) != "") && ($inheritNum != "INHERIT")} {
1179             UserError \
1180                "Must continue inheriting if continuing list numbering" yes
1181             set inheritNum INHERIT
1182         }
1183     }
1184
1185     if {$inheritNum == "INHERIT"} {
1186         # inheriting a string to preface the current number - e.g., "A.1."
1187         set inheritString $outerList(inheritString)
1188         append inheritString \
1189             [MakeOrder $outerList(numeration) $outerList(count)]
1190     }
1191
1192     # insure "spacing" is upper case and valid (we use it in the SSI)
1193     set spacing [CheckSpacing $spacing]
1194
1195     # save the list type and spacing for use by <ListItem>
1196     set listDope(type)           ordered
1197     set listDope(spacing)       $spacing
1198     set listDope(numeration)    $numeration
1199     set listDope(inheritString) $inheritString
1200     set listDope(count)         $count
1201     Push listStack [array get listDope]
1202
1203     # create a FORM to hold the list items
1204     PushForm LIST "ORDER-$spacing" $id
1205
1206     set firstString "FIRST-"
1207 }
1208
1209
1210 # start a variable list (i.e., labeled list)
1211 proc VariableList {id role parent} {
1212     global listStack firstString
1213
1214     # if we are inside a Para, we need to issue a FORM to hang the
1215     # indent attributes on
1216     if {$parent == "PARA"} {
1217         PushForm "" "INSIDE-PARA" ""
1218     }
1219
1220     # parse out the possible role values (loose/tight and
1221     # wrap/nowrap)
1222     set role [split [string toupper $role]]
1223     set role1 [lindex $role 0]
1224     set role2 ""
1225     set length [llength $role]
1226     if {$length > 1} {
1227         set role2 [lindex $role 1]
1228     }
1229     if {$length > 2} {
1230         UserError "Too many values (> 2) for Role in a VARIABLELIST" yes
1231     }
1232     set spacing ""
1233     set wrap ""
1234     switch $role1 {
1235         LOOSE   -
1236         TIGHT   {set spacing $role1}
1237         WRAP    -
1238         NOWRAP  {set wrap $role1}
1239         default {UserError "Bad value for Role ($role1) in a VARIABLELIST" yes
1240                 }
1241     }
1242     switch $role2 {
1243         ""      {#}
1244         LOOSE   -
1245         TIGHT   {if {$spacing == ""} {
1246                      set spacing $role2
1247                  } else {
1248                      UserError "Only specify LOOSE/TIGHT once per Role" yes
1249                  }
1250                 }
1251         WRAP    -
1252         NOWRAP  {if {$wrap == ""} {
1253                      set wrap $role2
1254                  } else {
1255                      UserError "Only specify WRAP/NOWRAP once per Role" yes
1256                  }
1257                 }
1258         default {UserError "Bad value for Role ($role2) in a VARIABLELIST" yes
1259                 }
1260     }
1261     if {$spacing == ""} {
1262         set spacing "LOOSE"
1263     }
1264     if {$wrap == ""} {
1265         set wrap "NOWRAP"
1266     }
1267
1268     # insure "spacing" is upper case and valid (we use it in the SSI)
1269     set spacing [CheckSpacing $spacing]
1270
1271     # save the list type and spacing for use by <ListItem>;
1272     # also save a spot for the current label ID
1273     set listDope(type)     variable
1274     set listDope(spacing) $spacing
1275     set listDope(labelId) $id
1276     set listDope(wrap)    $wrap
1277     Push listStack [array get listDope]
1278
1279     # create a FORM to hold the list items
1280     PushForm LIST "VARIABLE-$spacing" $id
1281
1282     set firstString "FIRST-"
1283 }
1284
1285
1286 # open a variable list entry - create a BLOCK to hold the term(s)
1287 proc VarListEntry {id} {
1288     global firstString listStack nextId
1289
1290     # get the list spacing, i.e., TIGHT or LOOSE
1291     array set listDope [Peek listStack]
1292     set spacing $listDope(spacing)
1293
1294     # make sure we have an ID for the label (it goes in a FORM)
1295     # save the ID for use in PushFormItem
1296     if {$id == ""} {
1297         set id SDL-RESERVED[incr nextId]
1298     }
1299     array set listDope [Pop listStack]
1300     set listDope(labelId) $id
1301     Push listStack [array get listDope]
1302
1303     StartBlock ITEM "$firstString$spacing-TERMS" $id 0
1304 }
1305
1306 # process a term in a variablelist
1307 proc StartTerm {id} {
1308     global listStack
1309
1310     # get the current list info
1311     array set listTop [Peek listStack]
1312     set wrap $listTop(wrap)
1313
1314     set lined ""
1315     if {$wrap == "NOWRAP"} {
1316         set lined "LINED"
1317     }
1318
1319     StartParagraph $id "P" $lined
1320 }
1321
1322
1323 # process an item in an ordered, variable or itemized list
1324 proc ListItem {id override} {
1325     global listStack firstString nextId needFData validMarkArray
1326
1327     # get the current list info
1328     array set listTop [Peek listStack]
1329     set spacing $listTop(spacing)
1330
1331     # if it's an itemized list, are we overriding the mark?
1332     if {$listTop(type) == "itemized"} {
1333         if {$override == "NO"} {
1334             set mark $listTop(mark)
1335         } elseif {$override == ""} {
1336             set mark PLAIN
1337         } else {
1338             set mark [ValidMark $override]
1339         }
1340     }
1341
1342     if {($listTop(type) == "itemized") && ($mark != "PLAIN")} {
1343         # marked itemized list, try to reuse an existing mark <BLOCK>
1344         if {$firstString == ""} {
1345             # not a FIRST, calculate the next id index - we flip
1346             # between 0 and 1 to avoid column span in viewer
1347             set numName $spacing${mark}num  ;# get index name
1348             upvar #0 $numName idNum
1349             set idNum [expr  "-$idNum + 1"]  ;# flip it
1350         }
1351         if {$firstString != ""} {
1352             set idName FIRST$spacing${mark}Id
1353         } else {
1354             set idName $spacing${mark}Id$idNum
1355         }
1356         upvar #0 $idName labelId
1357         if {$labelId == ""} {
1358             # need to create a <BLOCK> and save the id
1359             set labelId "SDL-RESERVED[incr nextId]"
1360             Emit $needFData; set needFData ""
1361             Emit "<BLOCK [Id $labelId] CLASS=\"ITEM\""
1362             Emit " TIMING=\"ASYNC\" "
1363             Emit "SSI=\"$firstString$spacing-MARKED\""
1364             Emit ">\n<P SSI=\"P1\"><SPC NAME=\"$validMarkArray($mark)\""
1365             Emit "></P>\n</BLOCK>\n"
1366         }
1367     }
1368
1369     # emit the SSI and label for an ordered list
1370     if {$listTop(type) == "ordered"} {
1371         #  start a block for the label
1372         set labelId "SDL-RESERVED[incr nextId]"
1373         Emit $needFData; set needFData ""
1374         Emit "<BLOCK [Id $labelId] CLASS=\"ITEM\" SSI=\""
1375
1376         # create, e.g., FIRST-LOOSE-ORDERED
1377         Emit "$firstString$spacing-ORDERED\">\n"
1378
1379         # emit the label (inherit string followed by order string)
1380         # and close the block
1381         Emit "<P SSI=\"P1\">"
1382         Emit $listTop(inheritString)
1383         Emit [MakeOrder $listTop(numeration) [incr listTop(count)]]
1384         Emit "</P>\n</BLOCK>\n"
1385
1386         # then update the top of the list stack
1387         Poke listStack [array get listTop]
1388     }
1389
1390     # or just get the label id for a variable (labeled) list - the
1391     # label was emitted in another production
1392     if {$listTop(type) == "variable"} {
1393         set labelId $listTop(labelId)
1394     }
1395
1396     # emit a one (for PLAIN) or two column FORM to wrap this list item
1397     set ssi "$firstString$spacing"
1398     if {($listTop(type) == "itemized") && ($mark == "PLAIN")} {
1399         PushForm ITEM $ssi $id
1400     } else {
1401         PushFormItem $ssi $labelId $id
1402     }
1403     set firstString ""
1404 }
1405
1406
1407 # start a segmented list, e.g.,
1408 #   foo:  fooItem1
1409 #   bar:  barItem1
1410 #
1411 #   foo:  fooItem2
1412 #   bar:  barItem2
1413 proc SegmentedList {id spacing parent} {
1414     global listStack firstString
1415
1416     # if we are inside a Para, we need to issue a FORM to hang the
1417     # indent attributes on
1418     if {$parent == "PARA"} {
1419         PushForm "" "INSIDE-PARA" ""
1420     }
1421
1422     # insure "spacing" is upper case and valid (we use it in the SSI)
1423     set spacing [CheckSpacing $spacing]
1424
1425     # save the list type and spacing for use by <ListItem>;
1426     set listDope(type)     segmented
1427     set listDope(spacing) $spacing
1428     Push listStack [array get listDope]
1429
1430     # create a FORM to hold the list items
1431     PushForm LIST "SEGMENTED-$spacing" $id
1432
1433     set firstString "FIRST-"
1434 }
1435
1436 # emit the SegTitle elements, each in its own BLOCK - we'll reuse
1437 # them on each Seg of each SegListItem
1438 proc StartSegTitle {id} {
1439     global firstString listStack segTitleList nextId
1440
1441     # get the list spacing, i.e., TIGHT or LOOSE
1442     array set listDope [Peek listStack]
1443     set spacing $listDope(spacing)
1444
1445     # make sure we have an ID for the label (it goes in a FORM)
1446     # save the ID for use in PushFormItem
1447     if {$id == ""} {
1448         set id SDL-RESERVED[incr nextId]
1449     }
1450     lappend segTitleList $id
1451
1452     # start the block but don't put in on the FORM, we'll put this
1453     # BLOCK and the one containing the SegListItem.Seg into a two
1454     # column form later
1455     StartBlock ITEM "$firstString$spacing-SEGTITLE" $id 0
1456     set firstString ""
1457
1458     StartParagraph "" SEGTITLE ""
1459 }
1460
1461
1462 # start a SegListItem - save the id (if any) of the SegListItem to
1463 # be emitted as an anchor in the first Seg
1464 proc StartSegListItem {id} {
1465     global segListItemNumber segListItemId firstString
1466
1467     set segListItemId     $id
1468     set segListItemNumber 0
1469     set firstString       "FIRST-"
1470 }
1471
1472
1473 # process a Seg in a SegListItem - get the corresponding SegTitle ID
1474 # and create a BLOCK for the item then put the pair into the FORM that
1475 # was created back in SegmentedList
1476 proc StartSeg {id isLastSeg} {
1477     global segTitleList segListItemNumber segListItemId firstString
1478     global listStack nextId
1479
1480     set nTitles [llength $segTitleList]
1481     if {$segListItemNumber >= $nTitles} {
1482         UserError \
1483            "More Seg than SegTitle elements in a SegmentedList" yes
1484         return
1485     }
1486     if {$isLastSeg} {
1487         if {[expr "$segListItemNumber" + 1] != $nTitles} {
1488             UserError \
1489                "More SegTitle than Seg elements in a SegmentedList" yes
1490         }
1491     }
1492
1493     # get the current list info
1494     array set listTop [Peek listStack]
1495     set spacing $listTop(spacing)
1496
1497     # open a BLOCK and P to hold the Seg content; put any user
1498     # supplied Id on the BLOCK and the saved segListItem Id (if
1499     # any) on the P.
1500     set itemId $id
1501     if {$id == ""} {
1502         set itemId "SDL-RESERVED[incr nextId]"
1503     }
1504     StartBlock ITEM $firstString$spacing $itemId 0
1505     set firstString ""
1506     StartParagraph $segListItemId P ""
1507     set segListItemId ""
1508
1509     # we've already guaranteed that we don't overflow the list
1510     set titleId [lindex $segTitleList $segListItemNumber]
1511     incr segListItemNumber
1512
1513     # add the title and item to a row vector (FROWVEC)
1514     AddRowVec "$titleId $itemId"
1515 }
1516
1517
1518 # close a list
1519 proc EndList {parent} {
1520     global listStack lastList
1521
1522     # get the most recently opened list and remove it from the stack
1523     array set lastList [Pop listStack]
1524
1525     if {($lastList(type) == "itemized") && ($lastList(mark) == "PLAIN") } {
1526         PopForm
1527     } else {
1528         PopFormN 2
1529     }
1530
1531     # if we are inside a Para, we need to close the FORM we issued for
1532     # hanging the indent attributes
1533     if {$parent == "PARA"} {
1534         ContinueParagraph
1535     }
1536 }
1537
1538
1539 # start a super- or sub-scripted phrase; if there's an ID, emit the
1540 # anchor before the SPHRASE
1541 proc StartSPhrase {id gi} {
1542     Anchor $id
1543     switch $gi {
1544         SUPERSCRIPT {set type SUPER}
1545         SUBSCRIPT   {set type SUB}
1546     }
1547
1548     Emit "<KEY CLASS=\"EMPH\" SSI=\"SUPER-SUB\"><SPHRASE CLASS=\"$type\">"
1549 }
1550
1551 # end a super- or sub-scripted phrase
1552 proc EndSPhrase {} {
1553     Emit "</SPHRASE></KEY>"
1554 }
1555
1556
1557 # start an admonition (note/caution/warning/tip/important),
1558 # emit a FORM to hold it and the HEAD for the icon (if any);
1559 # if the admonition has no Title, emit one using the GI of the
1560 # admonition; map Tip to Note and Important to Caution
1561 proc StartAdmonition {id gi haveTitle} {
1562     PushForm "" ADMONITION $id
1563
1564     # select the icon
1565     switch $gi {
1566         NOTE      -
1567         TIP       {set icon "graphics/noteicon.pm"}
1568         CAUTION   -
1569         IMPORTANT {set icon "graphics/cauticon.pm"}
1570         WARNING   {set icon "graphics/warnicon.pm"}
1571     }
1572     set snbId [AddToSNB GRAPHIC $icon]
1573
1574     # emit the icon wrapped in a head for placement
1575     Emit "<HEAD SSI=\"ADMONITION-ICON\"><SNREF>"
1576     Emit "<REFITEM RID=\"$snbId\" CLASS=\"ICON\"></REFITEM>\n"
1577     Emit "</SNREF></HEAD>"
1578
1579     # emit a title if none provided
1580     if {!$haveTitle} {
1581         Emit "<HEAD SSI=\"ADMONITION-TITLE\">$gi</HEAD>\n"
1582     }
1583 }
1584
1585
1586 # start a Procedure - emit a <FORM> to hold it
1587 proc StartProcedure {id} {
1588     PushForm "" PROCEDURE $id
1589 }
1590
1591
1592 # start a Step inside a Procedure, emit another FORM to hold it
1593 proc StartStep {id} {
1594     PushForm "" STEP $id
1595 }
1596
1597
1598 # start a SubStep inside a Stop, emit yet another FORM to hold it
1599 proc StartSubStep {id} {
1600     PushForm "" SUBSTEP $id
1601 }
1602
1603
1604 # start a Part; make the PARTGlossArray be the current glossary array
1605 proc StartPart {id} {
1606     global partID glossStack
1607
1608     set partID $id
1609
1610     # make sure the glossary array exists but is empty
1611     Push glossStack PARTGlossArray
1612     upvar #0 [Peek glossStack] currentGlossArray
1613     set currentGlossArray(foo) ""
1614     unset currentGlossArray(foo)
1615 }
1616
1617
1618 # end a Part; check for definitions for all glossed terms
1619 proc EndPart {} {
1620     global glossStack
1621
1622     # get a convenient handle on the glossary array
1623     upvar #0 [Peek glossStack] currentGlossArray
1624
1625     # check that all the glossed terms have been defined
1626     foreach name [array names currentGlossArray] {
1627         if {[lindex $currentGlossArray($name) 1] != "defined"} {
1628             set glossString [lindex $currentGlossArray($name) 2]
1629             UserError "No glossary definition for \"$glossString\"" no
1630         }
1631     }
1632
1633     # delete this glossary array
1634     unset currentGlossArray
1635 }
1636
1637
1638 # create and populate a dummy home page title - if no Title was
1639 # specified we want it to be "Home Topic"
1640 proc SynthesizeHomeTopicTitle {} {
1641     global partID
1642     global localizedAutoGeneratedStringArray
1643
1644     Title $partID PARTINTRO
1645     set message "Home Topic"
1646     Emit $localizedAutoGeneratedStringArray($message)
1647     CloseTitle PARTINTRO
1648 }
1649
1650
1651 # create and populate a dummy home page because there was no
1652 # PartIntro in this document
1653 proc SynthesizeHomeTopic {} {
1654     global partID
1655     global localizedAutoGeneratedStringArray
1656
1657
1658     StartNewVirpage PARTINTRO ""
1659     SynthesizeHomeTopicTitle
1660     StartParagraph $partID P ""
1661     set message "No home topic (PartIntro) was specified by the author."
1662     Emit $localizedAutoGeneratedStringArray($message)
1663     EndParagraph
1664 }
1665
1666
1667 # start a virpage for, e.g., a SECTn - close the previous first;
1668 # compute the level rather than specifying it in the transpec to allow
1669 # one specification to do for all SECTn elements; if level=2 and we
1670 # haven't emitted a PartIntro (aka HomeTopic), emit one
1671 proc StartNewVirpage {ssi id} {
1672     global nextId virpageLevels inVirpage firstPInBlock
1673     global indexLocation snbLocation savedSNB currentSNB
1674     global lastList language charset docId havePartIntro partIntroId
1675     global emptyCells
1676     global manTitle manVolNum manDescriptor manNames manPurpose
1677
1678     # get the LEVEL= value for this VIRPAGE (makes for a shorter
1679     # transpec to not have to specify level there)
1680     if {[info exists virpageLevels($ssi)]} {
1681         set level $virpageLevels($ssi)
1682     } else {
1683         set level 0
1684     }
1685
1686     # if we are opening the PartIntro, use the generated ID (which
1687     # may be the assigned ID, if present) and flag that we've seen
1688     # the home topic
1689     if {$ssi == "PARTINTRO"} {
1690         set ssi CHAPTER
1691         set id  $partIntroId
1692         set havePartIntro 1
1693     }
1694
1695     # if we haven't seen a PartIntro but we're trying to create a
1696     # level 2 VIRPAGE, emit a dummy PartInto
1697     if {($level == 2) && !$havePartIntro} {
1698         SynthesizeHomeTopic
1699     }
1700
1701     if {[string match {SECT[1-5]} $ssi]} {
1702         # make Chapter and all Sect? have an SSI of "CHAPTER", use LEVEL
1703         # to distinguish between them
1704         set ssi CHAPTER
1705     } else {
1706         # make Reference, RefEntry and all RefSect? have an SSI of
1707         # "REFERENCE", use LEVEL to distinguish between them
1708         if {$ssi == "REFENTRY"} {
1709             set $ssi REFERENCE
1710         } else {
1711             if {[string match {REFSECT[1-3]} $ssi]} { set ssi REFERENCE }
1712         }
1713     }
1714     if {($ssi == "REFERENCE") || ($ssi == "REFENTRY")} {
1715         # assume no section, we'll get one in RefMeta.ManVolNum, if any
1716         set manTitle      ""
1717         set manVolNum     ""
1718         set manDescriptor ""
1719         set manNames      ""
1720         set manPurpose    ""
1721     }
1722
1723     # close an open BLOCK, if any
1724     CloseBlock
1725     
1726     # close any open VIRPAGE
1727     Emit $inVirpage; set inVirpage "</VIRPAGE>\n"
1728
1729     # if the first paragraph on the page is a compound para, we want
1730     # to emit a FORM with an SSI="P1" so set the first P flag
1731     set firstPInBlock 1
1732
1733     # stash away the SNB for this VIRPAGE (or SDLDOC) - make an
1734     # associative array of the file location and the SNB data so
1735     # we can update the file location by adding the INDEX size before
1736     # writing the .snb file
1737     set names [array names currentSNB]
1738     if {[llength $names] != 0} {
1739         foreach name $names {
1740             # split the name into the GI and xid of the SNB entry
1741             set colonLoc [string first "::" $name]
1742             set type [string range $name 0 [incr colonLoc -1]]
1743             set data [string range $name [incr colonLoc 3] end]
1744
1745             # emit the entry
1746             append tempSNB "<$type ID=\"$currentSNB($name)\" "
1747             switch $type {
1748                 GRAPHIC   -
1749                 AUDIO     -
1750                 VIDEO     -
1751                 ANIMATE   -
1752                 CROSSDOC  -
1753                 MAN-PAGE  -
1754                 TEXTFILE  { set command "XID" }
1755                 SYS-CMD   { set command "COMMAND" }
1756                 CALLBACK  { set command "DATA" }
1757             }
1758             append tempSNB "$command=\"$data\">\n"
1759         }
1760         set savedSNB($snbLocation) $tempSNB
1761         unset currentSNB
1762     }
1763
1764     if {[array exists lastList]} {
1765         unset lastList ;# don't allow lists to continue across virpage
1766     }
1767
1768     # delete the list of empty cells used for indefined Entries in
1769     # tables - we can only re-use them on the same virpage
1770     if {[array exists emptyCells]} {
1771         unset emptyCells
1772     }
1773
1774     # we have to create new BLOCKs to hold the marks on the new page
1775     ReInitPerMarkInfo
1776
1777     if {$id == ""} { set id "SDL-RESERVED[incr nextId]" }
1778     Emit "<VIRPAGE [Id $id] LEVEL=\"$level\" "
1779     Emit "LANGUAGE=\"$language\" "
1780     Emit "CHARSET=\"$charset\" "
1781     Emit "DOC-ID=\"$docId\" "
1782     Emit "SSI=\"$ssi\">\n"
1783
1784     set snbLocation [tell stdout] ;# assume no HEAD for now
1785 }
1786
1787
1788 # save the virpageLevels setting for this ssi (if any) and unset it
1789 # then call StartNewVirpage; on return, restore the virpagelevels
1790 # setting and continue - this will force the virpage to be a level 0
1791 # virpage and not show up in the TOC
1792 proc StartNewLevel0Virpage {ssi id} {
1793     global virpageLevels
1794
1795     if {[info exists virpageLevels($ssi)]} {
1796         set savedLevel $virpageLevels($ssi)
1797         unset virpageLevels($ssi)
1798     }
1799
1800     StartNewVirpage $ssi $id
1801
1802     if {[info exists savedLevel]} {
1803         set virpageLevels($ssi) $savedLevel
1804     }
1805 }
1806
1807
1808 # call StartNewVirpage, then if the user supplied ID is not same as
1809 # the default ID for that topic, emit an empty paragragh to contain
1810 # the user supplied ID; also, convert the ID of
1811 # SDL-RESERVED-LEGALNOTICE to SDL-RESERVED-COPYRIGHT for backwards
1812 # compatibility, preserve the original default ID so we're consistent
1813 # on this release too
1814 proc StartNewVirpageWithID {ssi id defaultID haveTitle} {
1815     global savedId
1816
1817     # do we need to replace LEGALNOTICE with COPYRIGHT?
1818     set legalNotice 0
1819     if {[string toupper $defaultID] == "SDL-RESERVED-LEGALNOTICE"} {
1820         set defaultID SDL-RESERVED-COPYRIGHT
1821         set legalNotice 1
1822     }
1823
1824     StartNewVirpage $ssi $defaultID
1825
1826     # if no user supplied ID but we changed the default, emit the
1827     # original default on the empty paragraph
1828     if {($id == "") && $legalNotice} {
1829         set id SDL-RESERVED-LEGALNOTICE
1830         set legalNotice 0
1831     }
1832
1833     # id is either user supplied or the original default (if changed);
1834     # if the VIRPAGE has a HEAD (Title), save this id (these ids) and
1835     # emit it (them) there, otherwise, emit an empty paragraph with
1836     # the id as its id
1837     if {$id != ""} {
1838         if {[string toupper $id] != [string toupper $defaultID]} {
1839             if {$haveTitle} {
1840                 set savedId $id
1841                 if {$legalNotice} {
1842                     # had both a user supplied ID and we changed the default
1843                     lappend savedId SDL-RESERVED-LEGALNOTICE
1844                 }
1845             } else {
1846                 StartParagraph $id "" ""
1847                 if {$legalNotice} {
1848                     # had both a user supplied ID and we changed the default
1849                     Anchor SDL-RESERVED-LEGALNOTICE
1850                 }
1851                 EndParagraph
1852             }
1853         }
1854     }
1855 }
1856
1857
1858 # start a VIRPAGE for an appendix; if there's no ROLE=NOTOC, use the
1859 # virpage level from the level array, otherwise, use level 0
1860 proc StartAppendix {ssi id role} {
1861     global virpageLevels
1862  
1863     set uRole [string toupper $role]
1864
1865     if {$uRole == "NOTOC"} {
1866         set saveAppendixLevel $virpageLevels(APPENDIX)
1867         set virpageLevels(APPENDIX) 0
1868     } elseif {$role != ""} {
1869         UserError "Bad value (\"$role\") for Role attribute in Appendix" yes
1870     }
1871
1872     StartNewVirpage $ssi $id
1873
1874     if {$uRole == "NOTOC"} {
1875         set virpageLevels(APPENDIX) $saveAppendixLevel
1876     }
1877 }
1878
1879
1880 # start a new VIRPAGE for a topic that may contain a glossary; if
1881 # there is a glossary, start a new one and make it the current glossary,
1882 # otherwise, make the parent's glossary the current one.
1883 proc StartGlossedTopic {gi id haveGlossary} {
1884     global glossStack
1885
1886     if {$haveGlossary} {
1887         # save the glossary array name so we can get back here
1888         # when this topic is done
1889         Push glossStack ${gi}GlossArray
1890
1891         # start a new (empty) glossary array for this glossary
1892         upvar #0 ${gi}GlossArray currentGlossArray
1893         set currentGlossArray(foo) ""
1894         unset currentGlossArray(foo)
1895     }
1896
1897     StartNewVirpage $gi $id
1898 }
1899
1900
1901 # end a topic that may contain a glossary; if it did, check that all
1902 # glossed terms have been defined and remove the array - restore the
1903 # previous glossary array
1904 proc EndGlossedTopic {haveGlossary} {
1905     global glossStack
1906
1907     # get a convenient handle on the glossary array
1908     upvar #0 [Peek glossStack] currentGlossArray
1909
1910     if {$haveGlossary} {
1911         # check that all the glossed terms have been defined
1912         foreach name [array names currentGlossArray] {
1913             if {[lindex $currentGlossArray($name) 1] != "defined"} {
1914                 set glossString [lindex $currentGlossArray($name) 2]
1915                 UserError "No glossary definition for \"$glossString\"" no
1916             }
1917         }
1918
1919         # delete this glossary array and restore the previous one
1920         unset currentGlossArray
1921         Pop glossStack
1922     }
1923 }
1924
1925
1926 # alternate OutputString routine for when in a glossed term - merely
1927 # buffer the output rather than sending to the output stream; we'll
1928 # emit the SDL when the whole term has been seen
1929 proc GlossOutputString {string} {
1930     global glossBuffer
1931
1932     append glossBuffer $string
1933 }
1934
1935
1936 # prepare to link a glossed term to its definition in the glossary -
1937 # at this point, we simply divert the output into a buffer
1938 proc StartAGlossedTerm {} {
1939     global glossBuffer
1940
1941     set glossBuffer ""
1942     rename OutputString SaveGlossOutputString
1943     rename GlossOutputString OutputString
1944 }
1945
1946
1947 # strip any SDL markup from the string, upper case it  and return
1948 # the result;  takes advantage of the fact that we never split
1949 # start or end tags across lines (operates a line at a time)
1950 proc StripMarkup {markup} {
1951     set exp {(^|([^&]*))</?[A-Z]+[^>]*>}
1952     set stripped ""
1953     set mList [split $markup "\n"];      # split into a list of lines
1954     set listLen [llength $mList]
1955     while {[incr listLen -1] >= 0} {
1956         set mString [lindex $mList 0];   # get the first line from the
1957         set mList [lreplace $mList 0 0]; # list and delete it
1958         if {[string length $mString] == 0} {
1959             # empty line of pcdata (no markup)
1960             append stripped "\n"
1961             continue
1962         }
1963         # force to upper case and delete all start and end tags
1964         set mString [string toupper $mString]
1965         while {[regsub -all $exp $mString {\1} mString]} {#}
1966         if {[string length $mString] == 0} {
1967             # empty line after removing markup; skip it
1968             continue
1969         }
1970         append stripped $mString "\n";   # concat this line to result
1971     }
1972     return $stripped
1973 }
1974
1975
1976 # done collecting a glossed term - if we're not NOGLOSS, emit the SDL
1977 # wrapped in a LINK; save the term, baseform (if any) and the ID
1978 # used in the link - we'll define the ID in the glossary itself
1979 proc EndAGlossedTerm {id role} {
1980     global glossBuffer nextId glossStack
1981
1982     # get a convenient handle on the glossary array
1983     upvar #0 [Peek glossStack] currentGlossArray
1984
1985     # get the original output routine back
1986     rename OutputString GlossOutputString
1987     rename SaveGlossOutputString OutputString
1988
1989     set qualifier [string toupper [string range $role 0 8]]
1990     if {$qualifier == "NOGLOSS"} {
1991         Emit "<KEY CLASS=\"TERM\" SSI=\"GLOSSARY\">"
1992         Emit $glossBuffer
1993         Emit "</KEY>"
1994     } else {
1995         if {$qualifier == "BASEFORM="} {
1996             set glossString [string range $role 9 end]
1997         } else {
1998             set glossString $glossBuffer
1999         }
2000
2001         # trim whitespace from the front and back of the string to be
2002         # glossed, also turn line feeds into spaces and compress out
2003         # duplicate whitespace
2004         set glossString [string trim $glossString]
2005         set glossString [split $glossString '\n']
2006         set tmpGlossString $glossString
2007         set glossString [lindex $tmpGlossString 0]
2008         foreach str [lrange $tmpGlossString 1 end] {
2009             if {$str != ""} {
2010                 append glossString " " [string trim $str]
2011             }
2012         }
2013
2014         # upper case the glossary entry and strip it of markup to
2015         # use as an index so we get a case insensitive match - we'll
2016         # save the original string too for error messages; if there's
2017         # no glossary entry yet, issue an ID - the second entry in
2018         # the list will be set to "defined" when we see the definition
2019         set glossIndex [StripMarkup $glossString]
2020         if {[info exists currentGlossArray($glossIndex)]} {
2021             set refId [lindex $currentGlossArray($glossIndex) 0]
2022         } else {
2023             set refId SDL-RESERVED[incr nextId]
2024             set currentGlossArray($glossIndex) [list $refId "" $glossString]
2025         }
2026
2027         # now we can emit the glossed term wrapped in a popup link
2028         Emit "<LINK WINDOW=\"POPUP\" RID=\"$refId\">"
2029         Emit "<KEY CLASS=\"TERM\" SSI=\"GLOSSARY\">"
2030         Emit $glossBuffer
2031         Emit "</KEY></LINK>"
2032     }
2033 }
2034
2035
2036 # done collecting a term in a glossary - emit the anchor, if not
2037 # already done; if we are to be followed by alternate names (i.e.,
2038 # Abbrev and/or Acronym), emit the opening paren, otherwise, close
2039 # the open KEY
2040 proc EndATermInAGlossary {id} {
2041     global glossBuffer nextId nGlossAlts glossStack
2042     global strippedGlossIndex
2043
2044     # get a convenient handle on the glossary array
2045     upvar #0 [Peek glossStack] currentGlossArray
2046
2047     # get the original output routine back
2048     rename OutputString GlossOutputString
2049     rename SaveGlossOutputString OutputString
2050
2051     # emit the user supplied ID
2052     Anchor $id
2053
2054     # trim whitespace from the front and back of the string to be
2055     # placed in the glossary, also turn line feeds into spaces and
2056     # compress out duplicate whitespace
2057     set glossString [split $glossBuffer '\n']
2058     set tmpGlossString $glossString
2059     set glossString [lindex $tmpGlossString 0]
2060     foreach str [lrange $tmpGlossString 1 end] {
2061         if {$str != ""} {
2062             append glossString " " [string trim $str]
2063         }
2064     }
2065
2066     # create an upper cased version of the glossed string with markup
2067     # removed to use as a case insensitive index to the array
2068     set strippedGlossIndex [StripMarkup $glossString]
2069
2070     # get or create the generated ID; update the glossary array to
2071     # reflect that we now have a definition
2072     if {[info exists currentGlossArray($strippedGlossIndex)]} {
2073         set id [lindex $currentGlossArray($strippedGlossIndex) 0]
2074         set defined [lindex $currentGlossArray($strippedGlossIndex) 1]
2075         if {$defined == "defined"} {
2076             UserError \
2077                 "multiple definitions for glossary term \"$glossBuffer\"" yes
2078             set id SDL-RESERVED[incr nextId]
2079         }
2080     } else {
2081         set id SDL-RESERVED[incr nextId]
2082     }
2083     set currentGlossArray($strippedGlossIndex) \
2084         [list $id defined $glossString "" ""]
2085
2086     # emit the generated ID
2087     Anchor $id
2088     Emit "<KEY CLASS=\"TERM\" SSI=\"GLOSSARY\">"
2089     Emit "$glossBuffer"
2090     if {$nGlossAlts != 0} {
2091         Emit " ("
2092     } else {
2093         Emit "</KEY>"
2094         unset nGlossAlts
2095     }
2096 }
2097
2098
2099 proc EndAcronymInGlossary {id} {
2100     global nGlossAlts
2101
2102     if {[incr nGlossAlts -1] != 0} {
2103         Emit ", "
2104     } else {
2105         Emit ")</KEY>"
2106         unset nGlossAlts
2107     }
2108 }
2109
2110
2111 proc EndAbbrevInGlossary {id} {
2112     global nGlossAlts
2113
2114     Emit ")"</KEY"
2115     unset nGlossAlts
2116 }
2117
2118
2119 # start an entry in a glossary or glosslist; divert the output - we
2120 # need to sort the terms before emitting them
2121 proc StartGlossEntry {id nAlternates nDefs} {
2122     global nGlossAlts nGlossDefs currentGlossDef
2123     global glossEntryBuffer
2124
2125     # this helps when determining if a comma is needed after an alt
2126     # (either an Abbrev or an Acronym)
2127     set nGlossAlts $nAlternates
2128
2129     # this lets us know when to close the FORM holding the GlossDef+
2130     set nGlossDefs $nDefs
2131     set currentGlossDef 0
2132
2133     set glossEntryBuffer ""
2134     rename OutputString SaveGlossEntryOutputString
2135     rename GlossEntryOutputString OutputString
2136
2137     PushForm "" GLOSSENTRY $id
2138     StartParagraph "" "" ""
2139 }
2140
2141
2142 # alternate OutputString routine for when in a GlossEntry - merely
2143 # buffer the output rather than sending to the output stream; we'll
2144 # save this text for emission when the entire GlossDiv, Glossary or
2145 # GlossList has been processed and we've sorted the entries.
2146 proc GlossEntryOutputString {string} {
2147     global glossEntryBuffer
2148
2149     append glossEntryBuffer $string
2150 }
2151
2152
2153 # end an entry in a glossary or glosslist; save the entry in the
2154 # glossarray so we can later sort it for output
2155 proc EndGlossEntry {sortAs} {
2156     global glossEntryBuffer strippedGlossIndex glossStack
2157
2158     PopForm
2159
2160     # get the original output routine back
2161     rename OutputString GlossEntryOutputString
2162     rename SaveGlossEntryOutputString OutputString
2163
2164     # get a convenient handle on the glossary array and element
2165     upvar #0 [Peek glossStack] currentGlossArray
2166     upvar  0 currentGlossArray($strippedGlossIndex) currentEntryList
2167
2168     # save any user supplied sort key and the content of this glossary
2169     # entry for use when all entries are defined to sort them and emit
2170     # them in the sorted order
2171     set currentEntryList \
2172         [lreplace $currentEntryList 3 4 $sortAs $glossEntryBuffer]
2173
2174 }
2175
2176
2177 # the current batch of glossary entries (to a Glossary, GlossList or
2178 # GlossDiv has been saved in the glossArray - we need to sort them
2179 # based on the sortAs value if given (list index 3) or the index into
2180 # the glossArray of no sortAs was provided; when sorted, we can emit
2181 # entries (list index 4) in the new order and delete the emitted text
2182 # so that we don't try to emit it again (we want to save the
2183 # glossArray until, e.g., all GlossDiv elements are processed so we
2184 # can tell if all glossed terms have been defined); do a PopForm
2185 # when we're done if requested (for, e.g., GlossList)
2186 proc SortAndEmitGlossary {popForm} {
2187     global glossStack
2188
2189     # get a convenient handle on the glossary array
2190     upvar #0 [Peek glossStack] currentGlossArray
2191
2192     # start with an empty sortArray
2193     set sortArray(foo) ""
2194     unset sortArray(foo)
2195
2196     set names [array names currentGlossArray]
2197     foreach name $names {
2198         upvar 0 currentGlossArray($name) glossEntryList
2199
2200         # skip this array entry if we've already emitted it; mark as
2201         # emitted after we've extracted the content for emission
2202         if {[set content [lindex $glossEntryList 4]] == ""} {
2203             continue; # already been processed
2204         }
2205         set glossEntryList [lreplace $glossEntryList 4 4 ""]
2206
2207         # sort by the GlossTerm content or sortAs, if provided
2208         if {[set sortAs [lindex $glossEntryList 3]] == ""} {
2209             set sortAs $name
2210         }
2211
2212         # append the content in case we have equal sort values
2213         append sortArray($sortAs) $content
2214     }
2215
2216     set names [lsort -command CompareI18NStrings [array names sortArray]]
2217     foreach name $names {
2218         Emit $sortArray($name)
2219     }
2220
2221     if {[string toupper $popForm] == "POPFORM"} {
2222         PopForm
2223     }
2224 }
2225
2226
2227 # start a "See ..." in a glossary; if there was an otherterm, duplicate
2228 # its content and wrap it in a link to the GlossTerm holding the content
2229 proc StartGlossSee {id otherterm} {
2230     global localizedAutoGeneratedStringArray
2231
2232     StartBlock "" GLOSSSEE $id 1
2233     StartParagraph "" "" ""
2234     set message "See"
2235     Emit $localizedAutoGeneratedStringArray($message)
2236     Emit " "
2237     if {$otherterm != ""} {
2238         Emit "<LINK RID=\"$otherterm\">"
2239     }
2240 }
2241
2242
2243 # check the target of an OtherTerm attribute in a GlossSee to verify
2244 # that it is indeed the ID of a GlossTerm inside a GlossEntry
2245 proc CheckOtherTerm {id gi parent} {
2246     global glossType
2247
2248     set errorMess "Other term (\"$id\") referenced from a"
2249
2250     if {$gi != "GLOSSTERM"} {
2251         UserError "$errorMess $glossType must be a GlossTerm" yes
2252     } elseif {$parent != "GLOSSENTRY"} {
2253         UserError "$errorMess GlossSee must be in a GlossEntry" yes
2254     }
2255 }
2256
2257
2258 # start a definition in a glossary; we wrap a FORM around the whole
2259 # group of GlossDef elements in the GlossEntry
2260 proc StartGlossDef {id} {
2261     global nGlossDefs currentGlossDef
2262
2263     if {$currentGlossDef == 0} {
2264         PushForm "" GLOSSDEF $id
2265     }
2266     StartBlock "" "" $id 1
2267 }
2268
2269
2270 # end a definition in a glossary; if this is the last definition,
2271 # close the FORM that holds the group
2272 proc EndGlossDef {} {
2273     global nGlossDefs currentGlossDef
2274
2275     if {[incr currentGlossDef] == $nGlossDefs} {
2276         PopForm
2277         unset nGlossDefs currentGlossDef
2278     }
2279 }
2280
2281
2282 # start a "See Also ..." in a glossary definition; if there was an
2283 # otherterm, duplicate its content and wrap it in a link to the
2284 # GlossTerm holding the content
2285 proc StartGlossSeeAlso {id otherterm} {
2286     global localizedAutoGeneratedStringArray
2287
2288     StartBlock "" GLOSSSEE $id 1
2289     StartParagraph "" "" ""
2290     set message "See Also"
2291     Emit $localizedAutoGeneratedStringArray($message)
2292     Emit " "
2293     if {$otherterm != ""} {
2294         Emit "<LINK RID=\"$otherterm\">"
2295     }
2296 }
2297
2298
2299 # end a "See ..." or a "See Also ..." in a glossary definition; if there
2300 # was an otherterm, end the link to it
2301 proc EndGlossSeeOrSeeAlso {otherterm} {
2302     if {$otherterm != ""} {
2303         Emit "</LINK>"
2304     }
2305 }
2306
2307
2308 # alternate OutputString routine for when in IndexTerm - merely
2309 # buffer the output rather than sending to the output stream (index
2310 # entries get emitted into the index, not where they are defined)
2311 proc IndexOutputString {string} {
2312     global indexBuffer
2313
2314     append indexBuffer $string
2315 }
2316
2317
2318 # alternate Id routine for when in IndexTerm - merely
2319 # return the string rather than also setting the "most recently used"
2320 # variable.  The markup inside the IndexTerm goes into the index
2321 # not the current virpage so we don't want to use the ids here
2322 proc IndexId {name} {
2323     return "ID=\"$name\""
2324 }
2325
2326
2327 # start an index entry 
2328 proc StartIndexTerm {id} {
2329     global indexBuffer inP inBlock
2330
2331     if {$id != ""} {
2332         if {$inP} {
2333             Anchor $id
2334         } elseif {$inBlock != ""} {
2335             StartParagraph "" "P" ""
2336             Anchor $id
2337             EndParagraph
2338         }
2339
2340     }
2341
2342     # prepare to buffer the output while in IndexTerm
2343     set indexBuffer ""
2344     rename OutputString DefaultOutputString
2345     rename IndexOutputString OutputString
2346     rename Id DefaultId
2347     rename IndexId Id
2348 }
2349
2350
2351 # add an index sub-entry 
2352 proc AddIndexEntry {loc} {
2353     global indexBuffer indexVals indexArray
2354
2355     # trim superfluous whitespace at the beginning and end of the
2356     # indexed term
2357     set indexBuffer [string trim $indexBuffer]
2358
2359     # get an array index and determine whether 1st, 2nd or 3rd level
2360     set index [join $indexVals ", "]
2361     set level [llength $indexVals]
2362     set value [lindex $indexVals [expr "$level - 1"]]
2363
2364     # look for the string we want to put into the index; if the string
2365     # isn't there, add it - if it's there, verify that the content
2366     # being indexed is marked up the same as the last time we saw it
2367     # and that the primary/secondary/tertiary fields are split the
2368     # same way (bad check for now, we really need to save the
2369     # individual values) and add the location ID to the list of locs.
2370     set names [array names indexArray]
2371     if {$names == ""} {
2372         set indexArray($index) [list $level $value $loc $indexBuffer]
2373     } else {
2374         foreach i $names {
2375             set found 0
2376             if {$i == $index} {
2377                 set thisIndex $indexArray($index)
2378                 if {$indexBuffer != [lindex $thisIndex 3]} {
2379                     UserError "Indexing same terms with different markup" yes
2380                 }
2381                 if {$level != [lindex $thisIndex 0]} {
2382                     UserError "Index botch: levels don't match" yes
2383                 }
2384                 if {$loc != ""} {
2385                     set locs [lindex $thisIndex 2]
2386                     if {$locs != ""} { append locs " " }
2387                     append locs "$loc" 
2388                     set thisIndex [lreplace $thisIndex 2 2 $locs]
2389                     set indexArray($index) $thisIndex
2390                 }
2391                 set found 1
2392                 break
2393             }
2394         }
2395         if {!$found} {
2396             set indexArray($index) [list $level $value $loc $indexBuffer]
2397         }
2398     }
2399     set indexBuffer ""
2400 }
2401
2402
2403 # end an index entry 
2404 proc EndIndexTerm {} {
2405     global mostRecentId
2406
2407     AddIndexEntry $mostRecentId
2408
2409     # start emitting to output stream again
2410     rename OutputString        IndexOutputString
2411     rename DefaultOutputString OutputString
2412     rename Id        IndexId
2413     rename DefaultId Id
2414 }
2415
2416
2417 # start a primary index term
2418 proc StartPrimaryIndexEntry {id cdata} {
2419     global indexVals
2420
2421     set indexVals [list [string trim $cdata]]
2422 }
2423
2424
2425 # end a primary index term
2426 proc EndPrimaryIndexEntry {} {
2427 }
2428
2429
2430 # start a secondary index term
2431 proc StartSecondaryIndexEntry {id cdata} {
2432     global indexVals
2433
2434     AddIndexEntry "" ;# make sure our primary is defined
2435     lappend indexVals [string trim $cdata]
2436 }
2437
2438
2439 # end a secondary index term
2440 proc EndSecondaryIndexEntry {} {
2441 }
2442
2443
2444 # start a tertiary index term
2445 proc StartTertiaryIndexEntry {id cdata} {
2446     global indexVals
2447
2448     AddIndexEntry "" ;# make sure our secondary is defined
2449     lappend indexVals [string trim $cdata]
2450 }
2451
2452
2453 # end a tertiary index term
2454 proc EndTertiaryIndexEntry {} {
2455 }
2456
2457
2458 # compute the proper string for LOCS= in an index entry - primarily,
2459 # we want to avoid emitting the LOCS= if there are no locations
2460 # defined for this entry
2461 proc Locs {entry} {
2462     set locs [lindex $entry 2]
2463     if {$locs != ""} {
2464         return " LOCS=\"$locs\""
2465     }
2466     return ""
2467 }
2468
2469
2470 # open a .idx file and write the index into it
2471 proc WriteIndex {} {
2472     global baseName indexArray
2473
2474     set file [open "${baseName}.idx" w]
2475
2476     # sort the index using our special I18N safe sort function that
2477     # gives us a dictionary (case insensitive) sort
2478     set names [lsort -command CompareI18NStrings [array names indexArray]]
2479
2480     if {[set length [llength $names]]} {
2481         set oldLevel 0
2482         puts $file "<INDEX COUNT=\"$length\">"
2483         foreach name $names {
2484             set thisEntry $indexArray($name)
2485             switch [lindex $thisEntry 0] {
2486                 1 { switch $oldLevel {
2487                       1 { puts $file "</ENTRY>" }
2488                       2 { puts $file "</ENTRY>\n</ENTRY>" }
2489                       3 { puts $file "</ENTRY>\n</ENTRY>\n</ENTRY>" }
2490                     }
2491                   }
2492                 2 { switch $oldLevel {
2493                       2 { puts $file "</ENTRY>" }
2494                       3 { puts $file "</ENTRY>\n</ENTRY>" }
2495                     }
2496                   }
2497                 3 { if {$oldLevel == 3} { puts $file "</ENTRY>" } }
2498             }
2499             puts -nonewline $file "<ENTRY[Locs $thisEntry]>"
2500             puts -nonewline $file [lindex $thisEntry 3]
2501             set oldLevel [lindex $thisEntry 0]
2502         }
2503
2504         switch $oldLevel {
2505             1 { puts $file "</ENTRY>" }
2506             2 { puts $file "</ENTRY>\n</ENTRY>" }
2507             3 { puts $file "</ENTRY>\n</ENTRY>\n</ENTRY>" }
2508         }
2509         puts $file "</INDEX>"
2510     }
2511
2512     close $file
2513 }
2514
2515
2516 # called at the beginning of CHAPTER on each FOOTNOTE element - save
2517 # their numbering for use by FOOTNOTEREF and emit a VIRPAGE for each
2518 # note
2519 proc GatherFootnote {id} {
2520     global footnoteArray footnoteCounter nextId
2521
2522     incr footnoteCounter
2523     if {$id != ""} {
2524         set footnoteArray($id) $footnoteCounter
2525     } else {
2526         set id SDL-RESERVED[incr nextId]
2527     }
2528
2529     StartNewVirpage FOOTNOTE $id
2530 }
2531
2532
2533 # emit the footnote number of the id surrounded by a <LINK> so we can
2534 # get to it; skip out if there's no id to reference
2535 proc FootnoteRef {idref} {
2536     global footnoteArray
2537
2538     if {$idref != ""} {
2539         if {[info exists footnoteArray($idref)]} {
2540             Emit "<LINK RID=\"$idref\" WINDOW=\"popup\">"
2541             Emit "<KEY CLASS=\"EMPH\" SSI=\"FOOTNOTE\">"
2542             Emit "$footnoteArray($idref)</KEY></LINK>"
2543         }
2544     }
2545 }
2546
2547
2548 # add an element to the current SNB - try to reuse an entry if
2549 # possible
2550 proc AddToSNB {type data} {
2551     global currentSNB nextId
2552
2553     set index "$type::$data"
2554
2555     if {[info exists currentSNB($index)]} {
2556         set snbId $currentSNB($index)
2557     } else {
2558         set snbId "SDL-RESERVED[incr nextId]"
2559         set currentSNB($index) $snbId
2560     }
2561 return $snbId
2562 }
2563
2564
2565 # emit a DocBook Graphic element - create an SNB entry and point to
2566 # it here
2567 proc Graphic {id entityref fileref gi} {
2568     global inP
2569
2570     if {$gi == "GRAPHIC"} {
2571         set class FIGURE
2572     } else {
2573         set class IN-LINE
2574     }
2575
2576     # if "entityref" is present, it overrides "fileref"
2577     if {$entityref != ""} {
2578         # need to remove "<OSFILE ASIS>" (or equivalent for different
2579         # system identifiers) from the beginning of the entity name
2580         # if nsgmls was used for the original parse; the regular
2581         # expression below should work by simply ignoring any leading
2582         # angle bracket delimited string
2583         regsub {^(<.*>)(.*)$} $entityref {\2} entityref
2584         set file $entityref
2585     } else {
2586         set file $fileref
2587     }
2588
2589     if {$file == ""} {
2590         UserError "No file name or entity specified for $gi" yes
2591     }
2592
2593     # if not in a paragraph, start one
2594     if {($gi == "GRAPHIC") && (!$inP)} { StartParagraph "" "P" "" }
2595  
2596     set snbId [AddToSNB GRAPHIC $file]
2597
2598     Emit "<SNREF>"
2599     Emit "<REFITEM RID=\"$snbId\" CLASS=\"$class\"></REFITEM>\n"
2600     Emit "</SNREF>"
2601 }
2602
2603
2604 # emit a deferred link; we deferred it when we saw that it was first
2605 # in a Para and that it contained only an InlineGraphic - we had
2606 # to wait for the InlineGraphic to come along to see if it not only
2607 # met the contextual constraints but also had a Remap=Graphic
2608 # attribute
2609 proc EmitDeferredLink {} {
2610     global deferredLink
2611
2612     if {![array exists deferredLink]} return
2613
2614     switch $deferredLink(gi) {
2615         LINK  {StartLink  "" $deferredLink(linkend)   $deferredLink(type)}
2616         OLINK {StartOLink "" $deferredLink(localinfo) $deferredLink(type)}
2617     }
2618
2619     unset deferredLink
2620 }
2621
2622
2623 # emit an InlineGraphic that might be remapped to a Graphic (via
2624 # Remap=) and might have text wrapped around it (if it's first in
2625 # a Para or first in a [OU]Link that is itself first in a Para)
2626 proc InFlowGraphic {id entityref fileref parent remap role} {
2627     global deferredLink
2628
2629     # we only map InlineGraphic to Graphic if we're either the first
2630     # thing in a Para or the only thing in a link which is itself
2631     # the first thing in a Para
2632     set ok 0
2633     set haveDeferredLink [array exists deferredLink]
2634     switch $parent {
2635         PARA      {set ok 1}
2636         LINK      -
2637         OLINK     -
2638         ULINK     {set ok $haveDeferredLink}
2639     }
2640     if {!$ok} {
2641         Graphic $id $entityref $fileref INLINEGRAPHIC
2642         return
2643     }
2644
2645     set uRemap [string toupper $remap]
2646     if {$uRemap == "GRAPHIC"} {
2647         set uRole [string toupper $role]
2648         switch $uRole {
2649             LEFT  -
2650             ""    {set role "LEFT"}
2651             RIGHT {set role "RIGHT"}
2652             default {
2653                 set badValMess "Bad value (\"$role\") for Role attribute"
2654                 UserError "$badValMess in InlineGraphic" yes
2655                 set role "LEFT"
2656                 }
2657         }
2658         if {$haveDeferredLink} {
2659             set linkID " ID=\"$deferredLink(id)\""
2660             if {$deferredLink(gi) == "ULINK"} {
2661                 unset deferredLink
2662                 set haveDeferredLink 0
2663             }
2664         } else {
2665             set linkID ""
2666         }
2667         Emit "<HEAD$linkID SSI=\"GRAPHIC-$role\">"
2668         if {$haveDeferredLink} {
2669             EmitDeferredLink
2670         }
2671         Graphic $id $entityref $fileref GRAPHIC
2672         if {$haveDeferredLink} {
2673             EndLink
2674         }
2675         Emit "</HEAD>"
2676         return
2677     } elseif {$remap != ""} {
2678         set badValMess "Bad value (\"$remap\") for Remap attribute"
2679         UserError "$badValMess in InlineGraphic" yes
2680     }
2681
2682     Graphic $id $entityref $fileref INLINEGRAPHIC
2683 }
2684
2685
2686 # start a figure; for now, ignore Role (as it was ignored in HelpTag)
2687 # but make sure Role contains only legal values
2688 proc StartFigure {id role} {
2689     if {$role != ""} {
2690         set uRole [string toupper $role]
2691         switch $uRole {
2692             LEFT     -
2693             CENTER   -
2694             RIGHT   {set i 0}
2695             default {
2696                 set badValMess "Bad value for Role (\"$role\") attribute"
2697                 UserError "$badValMess in Figure" yes
2698             }
2699         }
2700     }
2701
2702     PushForm "" "FIGURE" $id
2703 }
2704
2705
2706 # emit a CiteTitle in a KEY with the SSI set to the PubWork attr.
2707 proc CiteTitle {id type} {
2708     Emit "<KEY CLASS=\"PUB-LIT\""
2709     if {$id != ""} {
2710         Emit " ID=\"$id\""
2711     }
2712     Emit " SSI=\"$type\">"
2713 }
2714
2715
2716 # start a KEY element - each parameter is optional (i.e, may be "")
2717 proc StartKey {id class ssi} {
2718     Emit "<KEY"
2719     if {$id != ""} {
2720         Emit " ID=\"$id\""
2721     }
2722     if {$class != ""} {
2723         Emit " CLASS=\"$class\""
2724     }
2725     if {$ssi != ""} {
2726         Emit " SSI=\"$ssi\""
2727     }
2728     Emit ">"
2729 }
2730
2731 # start an emphasis with role=heading; want want a different ssi
2732 # so we can make it bold rather than italic for use as a list
2733 # heading
2734 proc StartHeading {id role} {
2735     set role [string toupper $role]
2736     if {$role != "HEADING"} {
2737         if {$role != ""} {
2738             UserWarning "Bad value for Role (!= \"Heading\") in EMPHASIS" yes
2739         }
2740         set ssi EMPHASIS
2741     } else {
2742         set ssi LIST-HEADING
2743     }
2744     StartKey $id EMPH $ssi
2745 }
2746
2747
2748 # start an Example or InformalExample - we need to put ourselves
2749 # in a mode where lines and spacing are significant
2750 proc Example {id} {
2751     global defaultParaType
2752
2753     set defaultParaType " TYPE=\"LITERAL\""
2754     PushForm "" "EXAMPLE" $id
2755 }
2756
2757
2758 # close an Example or InformalExample - put ourselves back in
2759 # the normal (non-literal) mode
2760 proc CloseExample {} {
2761     global defaultParaType
2762
2763     set defaultParaType ""
2764     PopForm
2765 }
2766
2767
2768 # start a Table or InformalTable - save the global attributes and
2769 # open a FORM to hold the table
2770 proc StartTable {id colSep frame label rowSep} {
2771     global tableAttributes
2772
2773     set tableAttributes(colSep) $colSep
2774     set tableAttributes(label)  $label
2775     set tableAttributes(rowSep) $rowSep
2776
2777     PushForm TABLE "TABLE-$frame" $id
2778
2779     # create a list of ids of empty blocks to be used to fill in
2780     # undefined table cells
2781 }
2782
2783
2784 # check the "char" attribute - we only support "." at this time;
2785 # return "." if char="." and "" otherwise; issue warning if char
2786 # is some character other than "."
2787 proc CheckChar {char} {
2788     if {($char != "") && ($char != ".")} {
2789         UserError "Only \".\" supported for character alignment" yes
2790         return ""
2791     }
2792     return $char
2793 }
2794
2795
2796 # start a TGROUP - prepare to build a list of column specifications
2797 # and an array of span specifications to be accessed by name; a column
2798 # specification may be numbered, in which case default (all #IMPLIED)
2799 # column specifications will be inserted to come up to the specified
2800 # number - if there are already more column specifications than the
2801 # given number, it's an error; open a FORM to hold the TGroup
2802 proc StartTGroup {id align char cols colSep rowSep nColSpecs} {
2803     global tableGroupAttributes tableAttributes
2804     global tableGroupColSpecs tableGroupSpanSpecs
2805     global numberOfColSpecs colNames haveTFoot
2806     global needTGroupTHeadForm needTFootForm
2807     global tableGroupSavedFRowVec
2808
2809     set numberOfColSpecs $nColSpecs
2810
2811     # do a sanity check on the number of columns, there must be
2812     # at least 1
2813     if {$cols <= 0} {
2814         UserError "Unreasonable number of columns ($cols) in TGroup" yes
2815         set cols 1
2816     }
2817
2818     # check for more COLSPECs than COLS - error if so
2819     if {$nColSpecs > $cols} {
2820         UserError "More ColSpecs defined than columns in the TGroup" yes
2821     }
2822
2823     set tableGroupAttributes(align) $align
2824     set tableGroupAttributes(char)  [CheckChar $char]
2825     set tableGroupAttributes(cols)  $cols
2826     if {$colSep == ""} {
2827         set tableGroupAttributes(colSep) $tableAttributes(colSep)
2828     } else {
2829         set tableGroupAttributes(colSep) $colSep
2830     }
2831     if {$rowSep == ""} {
2832         set tableGroupAttributes(rowSep) $tableAttributes(rowSep)
2833     } else {
2834         set tableGroupAttributes(rowSep) $rowSep
2835     }
2836
2837     # make sure we have a blank colName array so we don't get errors
2838     # if we try to read or delete it when there have been no named
2839     # ColSpecs in this tableGroup - use a numeric key since that is
2840     # not a NMTOKEN and so can never be a colName - note that all
2841     # colNames share a common name space within each tGroup.
2842     set colNames(0) ""
2843
2844     # create an empty column specification list for this TGroup;
2845     # if no ColSpec definitions at this level, set them all to the
2846     # defaults - take advantage of the fact that the function ColSpec
2847     # will create default column specifications to fill out up to an
2848     # explicitly set ColNum
2849     set tableGroupColSpecs ""
2850     if {$nColSpecs == 0} {
2851         ColSpec "" TGROUP "" "" "" $cols "" "" ""
2852     }
2853
2854     PushForm TABLE TGROUP $id
2855
2856     # set a flag to indicate that we haven't seen a TFoot yet; this
2857     # flag is used in EndRow and StartCell to determine if a Row is
2858     # the last row in this TGroup (the last row will be in the TFoot,
2859     # if present, otherwise it will be in the TBody)
2860     set haveTFoot 0
2861
2862     # initialize variables used to determine if we need separate FORM
2863     # elements for THead or TFoot - if ColSpec elements are not given
2864     # at those levels, they can go in the same FORM as the TBody and
2865     # we can guarantee that the columns will line up
2866     set needTGroupTHeadForm 0
2867     set needTFootForm       0
2868
2869     # and initialize a variable to hold saved FROWVEC elements across
2870     # THead, TBody and TFoot in case we are merging them into one or
2871     # two FORM elements rather than putting each in its own
2872     set tableGroupSavedFRowVec ""
2873 }
2874
2875
2876 # close a table group; delete the info arrays and lists and close the
2877 # FORM
2878 proc EndTGroup {} {
2879     global tableGroupAttributes tableGroupColSpecs tableGroupSpanSpecs
2880     global haveTFoot
2881
2882     unset tableGroupAttributes
2883     unset tableGroupColSpecs
2884     if {[info exists tableGroupSpanSpecs]} {
2885         unset tableGroupSpanSpecs
2886     }
2887     PopForm
2888
2889     # see the explanation for this variable under StartTGroup
2890     unset haveTFoot
2891 }
2892
2893
2894 # process one of a series of column specifications - use the parent GI
2895 # to determine which column specifications we're dealing with; fill up
2896 # to the specified column number with default COLSPECs, using the
2897 # TGROUP, THEAD or TFOOT values as defaults; any COLSPEC values not
2898 # specified in the parameter list should also be defaulted
2899 proc ColSpec {grandparent parent align char colName colNum
2900                                         colSep colWidth rowSep} {
2901     # the number of currently defined colSpecs in this context
2902     global numberOfColSpecs
2903     global colNames
2904
2905     # get the proper list of ColSpecs for the current context
2906     if {$grandparent == "ENTRYTBL"} {
2907         set gpName entryTable
2908     } else {
2909         set gpName tableGroup
2910     }
2911     switch  $parent {
2912         THEAD    { upvar #0 ${gpName}HeadColSpecs colSpecs }
2913         TGROUP   { upvar #0 tableGroupColSpecs colSpecs    }
2914         TFOOT    { upvar #0 tableFootColSpecs  colSpecs    }
2915         ENTRYTBL { upvar #0 entryTableColSpecs colSpecs    }
2916     }
2917
2918     # get the proper number of columns (either from TGroup or EntryTbl);
2919     # a THead could be in either a TGroup or EntryTbl so we need
2920     # to check the grandparent if we aren't at the top level
2921     if {$parent == "TGROUP"} {
2922         upvar #0 tableGroupAttributes attributes
2923     } elseif {$parent == "ENTRYTBL"} {
2924         upvar #0 entryTableAttributes attributes
2925     } elseif {$grandparent == "ENTRYTBL"} {
2926         upvar #0 entryTableAttributes attributes
2927     } else {
2928         upvar #0 tableGroupAttributes attributes
2929     }
2930     set nCols $attributes(cols)
2931
2932     # check for more COLSPECs than COLS - we've already issued an error if so
2933     append colSpecs ""
2934     set currentLength [llength $colSpecs]
2935     if {$currentLength >= $nCols} {
2936         return
2937     }
2938
2939     # create a default ColSpec
2940     set thisColSpec(align)    $attributes(align)
2941     set thisColSpec(char)     $attributes(char)
2942     set thisColSpec(colName)  ""
2943     set thisColSpec(colSep)   $attributes(colSep)
2944     set thisColSpec(colWidth) "1*"
2945     set thisColSpec(rowSep)   $attributes(rowSep)
2946
2947     # back fill with default COLSPECs if given an explicit COLNUM and
2948     # it's greater than our current position
2949     incr currentLength
2950     if {($colNum != "")} {
2951         if {($colNum < $currentLength)} {
2952             set badValMess1 "Explicit colNum ($colNum) less than current"
2953             set badValMess2 "number of ColSpecs ($currentLength)"
2954             UserError "$badValMess1 $badValMess2" yes
2955             return
2956         } else {
2957             while {$currentLength < $colNum} {
2958                 set thisColSpec(colNum) $currentLength
2959                 lappend colSpecs [array get thisColSpec]
2960                 incr currentLength
2961             }
2962         }
2963     }
2964     set colNum $currentLength
2965
2966     # set this COLSPEC, we've already set the defaults
2967     if {$align != ""} {
2968         set thisColSpec(align)    $align
2969     }
2970     if {$char != ""} {
2971         set thisColSpec(char)     [CheckChar $char]
2972     }
2973     set thisColSpec(colName)      $colName
2974     if {$colName != ""} {
2975         # save name to num mapping for later lookup by Entry
2976         set colNames($colName) $colNum
2977     }
2978     set thisColSpec(colNum)       $colNum
2979     if {$colSep != ""} {
2980         set thisColSpec(colSep)   $colSep
2981     }
2982     if {$colWidth != ""} {
2983         set thisColSpec(colWidth) $colWidth
2984     }
2985     if {$rowSep != ""} {
2986         set thisColSpec(rowSep)   $rowSep
2987     }
2988     if {$colNum == $nCols} {
2989         set thisColSpec(colSep) 0; # ignore COLSEP on last column
2990     }
2991     lappend colSpecs [array get thisColSpec]
2992
2993     # fill out to the number of columns if we've run out of COLSPECs
2994     if {[incr numberOfColSpecs -1] <= 0} {
2995         # restore the default COLSPEC
2996         set thisColSpec(align)    $attributes(align)
2997         set thisColSpec(char)     $attributes(char)
2998         set thisColSpec(colName)  ""
2999         set thisColSpec(colSep)   $attributes(colSep)
3000         set thisColSpec(colWidth) "1*"
3001         set thisColSpec(rowSep)   $attributes(rowSep)
3002
3003         while {$colNum < $nCols} {
3004             incr colNum
3005             set thisColSpec(colNum) $colNum
3006             if {$colNum == $nCols} {
3007                 set thisColSpec(colSep) 0; # ignore on last column
3008             }
3009             lappend colSpecs [array get thisColSpec]
3010         }
3011     }
3012 }
3013
3014
3015 # process a SpanSpec - we can't take defaults yet because the Namest
3016 # and Nameend attributes may refer to ColSpecs that don't get defined
3017 # until a TFoot or THead
3018 proc SpanSpec {parent align char colSep nameEnd nameSt rowSep spanName} {
3019     if {$parent == "TGROUP"} {
3020         upvar #0 tableGroupSpanSpecs spanSpecs
3021     } else {
3022         upvar #0 entryTableSpanSpecs spanSpecs
3023     }
3024
3025     set thisSpanSpec(align)   $align
3026     set thisSpanSpec(char)    [CheckChar $char]
3027     set thisSpanSpec(colSep)  $colSep
3028     set thisSpanSpec(nameEnd) $nameEnd
3029     set thisSpanSpec(nameSt)  $nameSt
3030     set thisSpanSpec(rowSep)  $rowSep
3031
3032     if {[info exists spanSpecs($spanName)]} {
3033         UserError "duplicate span name \"$spanName\"" yes
3034         return
3035     }
3036
3037     set spanSpecs($spanName) [array get thisSpanSpec]
3038 }
3039
3040
3041 # make a list of empty strings for use as an empty Row
3042 proc MakeEmptyRow {nCols} {
3043     set thisList ""
3044     while {$nCols > 0} {
3045         lappend thisList ""
3046         incr nCols -1
3047     }
3048     return $thisList
3049 }
3050
3051
3052 # given a ColSpec list, compute a COLW= vector for SDL;
3053 # the idea is to assume the page is 9360 units wide - that's
3054 # 6.5 inches in points at approximately 1/72 in. per point,
3055 # subtract all the absolute widths and divide the remnant by
3056 # the number of proportional width values then re-add the absolute
3057 # widths back in to the proper columns;  this technique should
3058 # make pages that are exactly 6.5 in. in printing surface look just
3059 # right and then go proportional from there
3060 proc ComputeCOLW {colSpecList} {
3061
3062     set nCols [llength $colSpecList]
3063
3064     # build lists of just the ColWidth specs - one for the proporional
3065     # values and one for the absolutes
3066     set index 0
3067     set totalProps 0
3068     set totalAbs   0
3069     while {$index < $nCols} {
3070         array set thisColSpec [lindex $colSpecList $index]
3071         set colWidth $thisColSpec(colWidth)
3072         set colWidth [string trimleft $colWidth]
3073         set colWidth [string trimright $colWidth]
3074         set colWidth [string tolower $colWidth]
3075         set widths [split $colWidth '+']
3076         set nWidths [llength $widths]
3077         set propWidth 0
3078         set absWidth  0
3079         set wIndex    0
3080         while {$wIndex < $nWidths} {
3081             set thisWidth [lindex $widths $wIndex]
3082             if {[scan $thisWidth "%f%s" val qual] != 2} {
3083                 UserError "Malformed ColWidth \"$thisWidth\"" yes
3084                 incr wIndex
3085                 continue
3086             }
3087             set thisProp 0
3088             set thisAbs  0
3089             switch -exact $qual {
3090                 *  {set thisProp $val}
3091                 pt {set thisAbs [expr "$val *  1 * 20"]}
3092                 pi {set thisAbs [expr "$val * 12 * 20"]}
3093                 cm {set thisAbs [expr "$val * 28 * 20"]}
3094                 mm {set thisAbs [expr "$val *  3 * 20"]}
3095                 in {set thisAbs [expr "$val * 72 * 20"]}
3096             }
3097             set propWidth [expr "$propWidth + $thisProp"]
3098             set absWidth  [expr "$absWidth  + $thisAbs"]
3099             incr wIndex
3100         }
3101         lappend propWidths $propWidth
3102         lappend absWidths  $absWidth
3103         set totalProps [expr "$totalProps + $propWidth"]
3104         set totalAbs   [expr "$totalAbs   + $absWidth"]
3105         incr index
3106     }
3107     if {$totalProps == 0} {
3108         # we need at least some proportionality; assume each cell
3109         # had been set to 1* to distribute evenly
3110         set totalProps $nCols
3111         set index 0
3112         if {[info exists propWidths]} {
3113             unset propWidths
3114         }
3115         while {$index < $nCols} {
3116             lappend propWidths 1
3117             incr index
3118         }
3119     }
3120     set tableWidth 9360
3121     if {$totalAbs > $tableWidth} {
3122         set tableWidth $totalAbs
3123     }
3124     set propAvail [expr "$tableWidth - $totalAbs"]
3125     set oneProp   [expr "$propAvail / $totalProps"]
3126
3127     # now we know what a 1* is worth and we know the absolute size
3128     # requests, create a ColWidth by adding the product of the
3129     # proportional times a 1* plus any absolute request; we'll allow
3130     # 20% growth and shrinkage
3131     set index 0
3132     set space ""
3133     while {$index < $nCols} {
3134         set thisAbs  [lindex $absWidths  $index]
3135         set thisProp [lindex $propWidths $index]
3136         set thisWidth [expr "$thisAbs + ($thisProp * $oneProp)"]
3137         set thisSlop [expr "$thisWidth * 0.2"]
3138         # make thisWidth an integer
3139         set dotIndex [string last "." $thisWidth]
3140         if {$dotIndex == 0} {
3141             set thisWidth 0
3142         } elseif {$dotIndex > 0} {
3143             incr dotIndex -1
3144             set thisWidth [string range $thisWidth 0 $dotIndex]
3145         }
3146         # make thisSlop an integer
3147         set dotIndex [string last "." $thisSlop]
3148         if {$dotIndex == 0} {
3149             set thisSlop 0
3150         } elseif {$dotIndex > 0} {
3151             incr dotIndex -1
3152             set thisSlop [string range $thisSlop 0 $dotIndex]
3153         }
3154         append returnValue "$space$thisWidth,$thisSlop"
3155         set space " "
3156         incr index
3157     }
3158
3159     return $returnValue
3160 }
3161
3162
3163
3164 # given a ColSpec list, compute a COLJ= vector for SDL;
3165 proc ComputeCOLJ {colSpecList} {
3166
3167     set nCols [llength $colSpecList]
3168
3169     set space ""
3170     set index 0
3171     while {$index < $nCols} {
3172         array set thisColSpec [lindex $colSpecList $index]
3173         switch -exact $thisColSpec(align) {
3174             LEFT    -
3175             JUSTIFY -
3176             ""      { set thisColJ l}
3177             CENTER  { set thisColJ c}
3178             RIGHT   { set thisColJ r}
3179             CHAR    { set thisColJ d}
3180         }
3181         append returnValue "$space$thisColJ"
3182
3183         set space " "
3184         incr index
3185     }
3186
3187     return $returnValue
3188 }
3189
3190
3191 # given a ColSpec, create the COLW= and COLJ= attributes; check the
3192 # list of current TOSS entries to see if one matches - if so, return
3193 # its SSI= else add it and create an SSI= to return
3194 proc CreateOneTOSS {ssi vAlign colSpec} {
3195     global newTOSS nextId
3196
3197     set colW [ComputeCOLW $colSpec]
3198     set colJ [ComputeCOLJ $colSpec]
3199     set names [array names newTOSS]
3200     foreach name $names {
3201         array set thisTOSS $newTOSS($name)
3202         if {[string compare $colW $thisTOSS(colW)]} {
3203             if {[string compare $colJ $thisTOSS(colJ)]} {
3204                 if {[string compare $vAlign $thisTOSS(vAlign)]} {
3205                     return $name
3206                 }
3207             }
3208         }
3209     }
3210
3211     # no matching colW,colJ, add an entry
3212     if {$ssi == ""} {
3213         set ssi HBF-SDL-RESERVED[incr nextId]
3214     }
3215     set thisTOSS(colW)   $colW
3216     set thisTOSS(colJ)   $colJ
3217     set thisTOSS(vAlign) $vAlign
3218     set newTOSS($ssi) [array get thisTOSS]
3219     return $ssi
3220 }
3221
3222
3223 # save values from TFoot, we'll actually process TFoot after TBody
3224 # but we need to know whether we have a TFoot and whether that TFoot
3225 # has ColSpec elements in order to push/pop a FORM for the TBody if
3226 # so
3227 proc PrepForTFoot {nColSpecs} {
3228     global haveTFoot needTFootForm
3229
3230     set haveTFoot 1
3231     set needTFootForm [expr "$nColSpecs > 0"]
3232 }
3233
3234
3235 # start a table header, footer or body - create a FORM to hold the rows;
3236 # create an empty row to be filled in by the Entry elements - set the
3237 # current row and number of rows to 1
3238 proc StartTHeadTFootTBody {parent gi haveTHead id vAlign nRows nColSpecs} {
3239     global numberOfColSpecs haveTFoot
3240     global needTFootForm
3241
3242     if {$parent == "ENTRYTBL"} {
3243         upvar #0 entryTableRowDope     rowDope
3244         upvar #0 needEntryTblTHeadForm needTHeadForm
3245         global entryTableAttributes
3246         set nCols $entryTableAttributes(cols)
3247         set entryTableAttributes(vAlign) $vAlign
3248         set entryTableAttributes(rows)   $nRows
3249     } else {
3250         upvar #0 tableGroupRowDope   rowDope
3251         upvar #0 needTGroupTHeadForm needTHeadForm
3252         global tableGroupAttributes
3253         set nCols $tableGroupAttributes(cols)
3254         set tableGroupAttributes(vAlign) $vAlign
3255         set tableGroupAttributes(rows)   $nRows
3256     }
3257
3258     set numberOfColSpecs $nColSpecs
3259
3260     # get the proper list of ColSpecs for the current context
3261     if {$parent == "ENTRYTBL"} {
3262         set parentName entryTable
3263     } else {
3264         set parentName tableGroup
3265     }
3266     switch  $gi {
3267         THEAD {upvar #0 ${parentName}HeadColSpecs colSpecs}
3268         TBODY {upvar #0 ${parentName}ColSpecs colSpecs}
3269         TFOOT {upvar #0 tableFootColSpecs colSpecs }
3270     }
3271
3272     # if no ColSpec definitions at this level, copy the parent's
3273     # ColSpec definition to here
3274     if {$nColSpecs == 0} {
3275         switch  $gi {
3276             THEAD   {upvar #0 ${parentName}ColSpecs parentColSpecs}
3277             TFOOT   {upvar #0 tableGroupColSpecs parentColSpecs}
3278         }
3279         if {$gi != "TBODY"} {
3280             set colSpecs $parentColSpecs
3281         }
3282     }
3283
3284     # if we have ColSpec elements on a THead, we'll need to put it
3285     # in its own FORM; we saved this value for TFoot earlier
3286     # because TFoot precedes TBody in the content model but doesn't
3287     # get processed until after TBody (as EndText: to TGroup)
3288     if {$gi == "THEAD"} {
3289         set needTHeadForm [expr "$nColSpecs > 0"]
3290     }
3291
3292     # determine whether we need to push a new FORM here - we always
3293     # have to push a FORM for a THead, we only push one for TBody
3294     # if THead needed its own or there was no THead and we only push
3295     # one for TFoot if it needs its own
3296     if {!$haveTHead} {
3297         set needTBodyForm 1
3298     } else {
3299         set needTBodyForm $needTHeadForm
3300     }
3301     set doit 0
3302     switch $gi {
3303         THEAD {set doit 1}
3304         TBODY {set doit $needTBodyForm}
3305         TFOOT {set doit $needTFootForm}
3306     }
3307
3308     # and push it, if so
3309     if {$doit} {
3310         set ssi [CreateOneTOSS $id "" $colSpecs]
3311         PushForm TABLE "$ssi" $id
3312     }
3313
3314     set rowDope(nRows)      0
3315     set rowDope(currentRow) 0
3316 }
3317
3318
3319 # end a table header footer or body - delete the global row
3320 # information and close the FORM; also delete the ColSpec info for
3321 # this THead or TFoot (TBody always uses the parent's)
3322 proc EndTHeadTFootTBody {parent gi} {
3323     global numberOfColSpecs needTFootForm haveTFoot
3324
3325     if {$parent == "ENTRYTBL"} {
3326         upvar #0 needEntryTblTHeadForm needTHeadForm
3327     } else {
3328         upvar #0 needTGroupTHeadForm needTHeadForm
3329     }
3330
3331     # determine whether we want to terminate this FORM here - we
3332     # only terminate the THead FORM if it needed its own, we only
3333     # terminate the TBody FORM if the TFoot needs its own or there
3334     # is no TFoot and we always terminate the FORM for TFoot
3335     if {($parent == "ENTRYTBL") || !$haveTFoot} {
3336         set needTBodyForm 1
3337     } else {
3338         set needTBodyForm $needTFootForm
3339     }
3340     set doit 0
3341     switch $gi {
3342         THEAD {set doit $needTHeadForm}
3343         TBODY {set doit $needTBodyForm}
3344         TFOOT {set doit 1}
3345     }
3346     PopTableForm $parent $gi $doit
3347
3348     # blow away the list of ColSpecs for the current context
3349     switch  $gi {
3350         THEAD    { if {$parent == "ENTRYTBL"} {
3351                        global entryTableHeadColSpecs
3352                        unset entryTableHeadColSpecs
3353                    } else {
3354                        global tableGroupHeadColSpecs
3355                        unset tableGroupHeadColSpecs
3356                    }
3357                  }
3358         TFOOT    { global tableFootColSpecs
3359                    unset tableFootColSpecs
3360                  }
3361     }
3362 }
3363
3364
3365 # start a table row - save the attribute values for when we
3366 # actually emit the entries of this row; when we emit the first
3367 # entry we'll emit the ID on the rowSep FORM that we create for each
3368 # Entry and set the ID field to "" so we only emit the ID once
3369 proc StartRow {grandparent parent id rowSep vAlign} {
3370     if {$grandparent == "ENTRYTBL"} {
3371         upvar #0 entryTableRowDope rowDope
3372         global entryTableAttributes
3373         set nCols $entryTableAttributes(cols)
3374         if {$vAlign == ""} {
3375             set vAlign $entryTableAttributes(vAlign)
3376         }
3377     } else {
3378         upvar #0 tableGroupRowDope rowDope
3379         global tableGroupAttributes
3380         set nCols $tableGroupAttributes(cols)
3381         if {$vAlign == ""} {
3382             set vAlign $tableGroupAttributes(vAlign)
3383         }
3384     }
3385     upvar 0 rowDope(currentRow) currentRow
3386     upvar 0 rowDope(nRows)      nRows
3387
3388     set rowDope(id)     $id
3389     set rowDope(rowSep) $rowSep
3390     set rowDope(vAlign) $vAlign
3391
3392     incr currentRow
3393     if {![info exists rowDope(row$currentRow)]} {
3394         set rowDope(row$currentRow) [MakeEmptyRow $nCols]
3395         incr nRows
3396     }
3397 }
3398
3399 # a debugging procedure
3400 proc DumpRowDope {rowDopeName} {
3401     upvar 1 $rowDopeName rowDope
3402
3403     puts stderr "rowDope:"
3404     set index 0
3405     while {[incr index] <= $rowDope(nRows)} {
3406         puts stderr \
3407             "    $index: ([llength $rowDope(row$index)]) $rowDope(row$index)"
3408     }
3409 }
3410
3411
3412 # end a table row
3413 proc EndRow {grandparent parent} {
3414     global emptyCells nextId haveTFoot
3415
3416     # this row could be in a TGroup or an EntryTbl
3417     if {$grandparent == "ENTRYTBL"} {
3418         upvar #0 entryTableRowDope rowDope
3419         global entryTableAttributes
3420         set nCols    $entryTableAttributes(cols)
3421         set nRowDefs $entryTableAttributes(rows)
3422     } else {
3423         upvar #0 tableGroupRowDope rowDope
3424         global tableGroupAttributes
3425         set nCols    $tableGroupAttributes(cols)
3426         set nRowDefs $tableGroupAttributes(rows)
3427     }
3428
3429     # get the proper list of ColSpecs for the current context
3430     switch  $parent {
3431         THEAD    { if {$grandparent == "ENTRYTBL"} {
3432                        upvar #0 entryTableHeadColSpecs colSpecs
3433                    } else {
3434                        upvar #0 tableGroupHeadColSpecs colSpecs
3435                    }
3436                  }
3437         TBODY    { if {$grandparent == "ENTRYTBL"} {
3438                        upvar #0 entryTableColSpecs colSpecs
3439                    } else {
3440                        upvar #0 tableGroupColSpecs colSpecs
3441                    }
3442                  }
3443         TFOOT    { upvar #0 tableFootColSpecs  colSpecs }
3444     }
3445
3446     # go over the row filing empty cells with an empty FORM containing
3447     # an empty BLOCK.  The FORM SSI= is chosen to give a RowSep based
3448     # upon the current ColSpec and rowDope, if we are on the last row
3449     # we want to set the RowSep to 0 unless there were more rows
3450     # created via the MoreRows attribute of Entry or EntryTbl forcing
3451     # the table to be longer than the number of Rows specified in which
3452     # case we want to fill in all those rows too and only force RowSep
3453     # to 0 on the last one; the inner BLOCK SSI= is chosen to give a
3454     # ColSep based upon the current ColSpec and Row definition - if
3455     # the column is the last one in the row, the ColSep is set to 0
3456     set currentRow $rowDope(currentRow)
3457     if {$currentRow == $nRowDefs} {
3458         set moreRows [expr "$rowDope(nRows) - $nRowDefs"]
3459     } else {
3460         set moreRows 0
3461     }
3462     upvar 0 rowDope(row$currentRow) thisRow
3463     upvar 0 rowDope(row[expr "$currentRow - 1"]) prevRow
3464     while {$moreRows >= 0} {
3465         set colIndex 0
3466         while {$colIndex < $nCols} {
3467             set thisCellId [lindex $thisRow $colIndex]
3468             if {$thisCellId == ""} {
3469                 array set thisColSpec [lindex $colSpecs $colIndex]
3470                 set desiredCell(colSep) $thisColSpec(colSep)
3471                 set desiredCell(rowSep) $thisColSpec(rowSep)
3472                 if {$rowDope(rowSep) != ""} {
3473                     set desiredCell(rowSep) $rowDope(rowSep)
3474                 }
3475                 if {$colIndex == $nCols} {
3476                     set desiredCell(colSep) 0
3477                 }
3478                 if {($moreRows == 0) && ($currentRow == $nRowDefs)} {
3479                     if {($parent == "TFOOT") ||
3480                         (($parent == "TBODY") && (!$haveTFoot))} {
3481                         set desiredCell(rowSep) 0
3482                     }
3483                 }
3484                 if {$desiredCell(colSep) == ""} {
3485                     set desiredCell(colSep) 1
3486                 }
3487                 if {$desiredCell(rowSep) == ""} {
3488                     set desiredCell(rowSep) 1
3489                 }
3490                 set found 0
3491                 foreach id [array names emptyCells] {
3492                     array set thisCell $emptyCells($id)
3493                     if {$thisCell(colSep) != $desiredCell(colSep)} {
3494                         continue
3495                     }
3496                     if {$thisCell(rowSep) != $desiredCell(rowSep)} {
3497                         continue
3498                     }
3499                     if {$currentRow > 1} {
3500                         if {[lindex $prevRow $colIndex] == $id} {
3501                             continue
3502                         }
3503                     }
3504                     if {$colIndex > 0} {
3505                         if {$lastCellId == $id} {
3506                             continue
3507                         }
3508                     }
3509                     set thisCellId $id
3510                     set found 1
3511                     break
3512                 }
3513                 if {!$found} {
3514                     if {$desiredCell(rowSep)} {
3515                         set ssi BORDER-BOTTOM
3516                     } else {
3517                         set ssi BORDER-NONE
3518                     }
3519                     set id [PushFormCell $ssi ""]
3520                     if {$desiredCell(colSep)} {
3521                         set ssi ENTRY-NONE-YES-NONE
3522                     } else {
3523                         set ssi ENTRY-NONE-NO-NONE
3524                     }
3525                     StartBlock CELL $ssi "" 1
3526                     PopForm
3527                     set emptyCells($id) [array get desiredCell]
3528                     set thisCellId $id
3529                 }
3530                 Replace thisRow $colIndex 1 $thisCellId
3531             }
3532         set lastCellId $thisCellId
3533         incr colIndex
3534         }
3535         incr moreRows -1
3536         incr currentRow 1
3537         upvar 0 thisRow prevRow
3538         upvar 0 rowDope(row$currentRow) thisRow
3539     }
3540
3541     # blow away the variables that get reset on each row
3542     unset rowDope(id)
3543     unset rowDope(rowSep)
3544     unset rowDope(vAlign)
3545 }
3546
3547
3548 # given a row list, an id and start and stop columns, replace the
3549 # entries in the list from start to stop with id - use "upvar" on
3550 # the row list so we actually update the caller's row
3551 proc Replace {callersRow start length id} {
3552     upvar $callersRow row
3553
3554     # length will be 0 if there was an error on the row
3555     if {$length <= 0} {
3556         return
3557     }
3558
3559     # make a list of ids long enough to fill the gap
3560     set i 1
3561     set ids $id; # we pad all the others with a starting space
3562     while {$i < $length} {
3563         append ids " " $id
3564         incr i
3565     }
3566
3567     # do the list replacement - need to "eval" because we want the
3568     # ids to be seen a individual args, not a list so we need to
3569     # evaluate the command twice
3570     set stop [expr "$start + $length - 1"]
3571     set command "set row \[lreplace \$row $start $stop $ids\]"
3572     eval $command
3573 }
3574
3575
3576 # process a table cell (Entry or EntryTbl); attributes are inherited
3577 # in the following fashion:
3578 #
3579 #       ColSpec
3580 #       SpanSpec
3581 #       Row
3582 #       Entry/EntryTbl
3583 #
3584 # with later values (going down the list) overriding earlier ones;
3585 # Table, TGroup, etc., values have already been propagated to the
3586 # ColSpecs
3587 proc StartCell {ancestor grandparent gi id align char colName cols
3588                     colSep moreRows nameEnd nameSt rowSep spanName
3589                         vAlign nColSpecs nTBodies} {
3590     global colNames tableGroupAttributes entryTableAttributes
3591     global numberOfColSpecs entryTableColSpecs nextId haveTFoot
3592     global needEntryTblTHeadForm entryTableSavedFRowVec
3593
3594     # get the appropriate SpanSpec list, if any; also get the row
3595     # row dope vector which also contains the current row number
3596     # and number of rows currently allocated (we might get ahead
3597     # of ourselves due to a vertical span via MOREROWS=)
3598     if {$ancestor == "TGROUP"} {
3599         upvar #0 tableGroupSpanSpecs spanSpecs
3600         upvar #0 tableGroupRowDope   rowDope
3601         set nCols $tableGroupAttributes(cols)
3602         set nRowDefs $tableGroupAttributes(rows)
3603     } else {
3604         upvar #0 entryTableSpanSpecs spanSpecs
3605         upvar #0 entryTableRowDope   rowDope
3606         set nCols $entryTableAttributes(cols)
3607         set nRowDefs $entryTableAttributes(rows)
3608     }
3609
3610     # get the proper list of ColSpecs for the current context
3611     switch  $grandparent {
3612         THEAD    { if {$ancestor == "ENTRYTBL"} {
3613                        upvar #0 entryTableHeadColSpecs colSpecs
3614                    } else {
3615                        upvar #0 tableGroupHeadColSpecs colSpecs
3616                    }
3617                  }
3618         TBODY    { if {$ancestor == "ENTRYTBL"} {
3619                        upvar #0 entryTableColSpecs colSpecs
3620                    } else {
3621                        upvar #0 tableGroupColSpecs colSpecs
3622                    }
3623                  }
3624         TFOOT    { upvar #0 tableFootColSpecs  colSpecs }
3625     }
3626
3627     # check for a span
3628     if {$spanName != ""} {
3629         if {[info exists spanSpecs($spanName)]} {
3630             array set thisSpan $spanSpecs($spanName)
3631             # SpanSpec column names win over explicit ones
3632             set nameSt  $thisSpan(nameSt)
3633             set nameEnd $thisSpan(nameEnd)
3634         } else {
3635             UserError "Attempt to use undefined SpanSpec \"$spanName\"" yes
3636         }
3637     }
3638
3639     # nameSt, whether explicit or from a span, wins over colName
3640     if {$nameSt != ""} {
3641         set colName $nameSt
3642     }
3643
3644     # get the row information - use upvar so we can update rowDope
3645     upvar 0 rowDope(currentRow)     currentRow
3646     upvar 0 rowDope(row$currentRow) thisRow
3647     upvar 0 rowDope(nRows)          nRows
3648
3649     # by now, if no colName we must have neither colName, nameSt nor
3650     # a horizontal span - find the next open spot in this row
3651     if {$colName != ""} {
3652         if {[info exists colNames($colName)]} {
3653             set startColNum $colNames($colName)
3654             if {$startColNum > $nCols} {
3655                 UserError "Attempt to address column outside of table" yes
3656                 set colName ""
3657             } else {
3658                 incr startColNum -1 ;# make the column number 0 based
3659             }
3660         } else {
3661             UserError "Attempt to use undefined column name \"$colName\"" yes
3662             set colName ""
3663         }
3664     }
3665     if {$colName == ""} {
3666         set index 0
3667         while {[lindex $thisRow $index] != ""} {
3668             incr index
3669         }
3670         if {$index == $nCols} {
3671             UserError "More entries defined than columns in this row" yes
3672             set index -1
3673         }
3674         set startColNum $index
3675     }
3676
3677     # if we have a nameEnd, it was either explicit or via a span -
3678     # get the stop column number; else set the stop column to the
3679     # start column, i.e., a span of 1
3680     if {$nameEnd == ""} {
3681         set stopColNum $startColNum
3682     } else {
3683         if {[info exists colNames($nameEnd)]} {
3684             set stopColNum $colNames($nameEnd)
3685             if {$stopColNum > $nCols} {
3686                 UserError "Attempt to address column outside of table" yes
3687                 set stopColNum $nCols
3688             }
3689             incr stopColNum -1 ;# make the column number 0 based
3690             if {$startColNum > $stopColNum} {
3691                 UserError "End of column span is before the start" yes
3692                 set stopColNum $startColNum
3693             }
3694         } else {
3695             UserError "Attempt to use undefined column name \"$nameEnd\"" yes
3696             set stopColNum $startColNum
3697         }
3698     }
3699
3700     # create an empty set of attributes for the cell - we'll fill
3701     # them in from the ColSpec, SpanSpec, Row and Entry or EntryTbl
3702     # defined values, if any, in that order
3703     set cellAlign  ""
3704     set cellColSep 1
3705     set cellRowSep 1
3706     set cellVAlign ""
3707
3708     # initialize the cell description with the ColSpec data
3709     # Table, TGroup and EntryTable attributes have already
3710     # percolated to the ColSpec
3711     if {$startColNum >= 0} {
3712         array set thisColSpec [lindex $colSpecs $startColNum]
3713         if {$thisColSpec(colSep) != ""} {
3714             set cellColSep $thisColSpec(colSep)
3715         }
3716         if {$thisColSpec(rowSep) != ""} {
3717             set cellRowSep $thisColSpec(rowSep)
3718         }
3719     }
3720
3721     # overlay any attributes defined on the span, that is, SpanSpec
3722     # attributes win over ColSpec ones
3723     if {[info exists thisSpan]} {
3724         if {$thisSpan(align) != ""} {
3725             set cellAlign $thisSpan(align)
3726         }
3727         if {$thisSpan(colSep) != ""} {
3728             set cellColSep $thisSpan(colSep)
3729         }
3730         if {$thisSpan(rowSep) != ""} {
3731             set cellRowSep $thisSpan(rowSep)
3732         }
3733     }
3734
3735     # overlay any attributes defined on the Row
3736     if {$rowDope(rowSep) != ""} {
3737         set cellRowSep $rowDope(rowSep)
3738     }
3739     if {$rowDope(vAlign) != ""} {
3740         set cellVAlign $rowDope(vAlign)
3741     }
3742
3743     # check for a char other than "" or "."; just a check, we don't
3744     # do anything with char
3745     set char [CheckChar $char]
3746
3747     # overlay any attributes defined on the Entry or EntryTbl - these
3748     # win over all
3749     if {$align != ""} {
3750         set cellAlign $align
3751     }
3752     if {$colSep != ""} {
3753         set cellColSep $colSep
3754     }
3755     if {$rowSep != ""} {
3756         set cellRowSep $rowSep
3757     }
3758     if {$vAlign != ""} {
3759         set cellVAlign $vAlign
3760     }
3761     
3762     # if this cell is the first on the row, feed it the (possible)
3763     # Row ID and set the Row ID to ""
3764     if {[set cellId $rowDope(id)] == ""} {
3765         set cellId SDL-RESERVED[incr nextId]
3766     } else {
3767         set rowDope(id) ""
3768     }
3769
3770     # now put the cell into the rowDope vector - if there's a
3771     # span, we'll put the cell in several slots; if there's a
3772     # vertical straddle, we may need to add more rows to rowDope
3773     if {$startColNum >= 0} {
3774         set stopRowNum [expr "$currentRow + $moreRows"]
3775         set spanLength [expr "($stopColNum - $startColNum) + 1"]
3776         set rowIndex $currentRow
3777         while {$rowIndex <= $stopRowNum} {
3778             if {![info exists rowDope(row$rowIndex)]} {
3779                 set rowDope(row$rowIndex) [MakeEmptyRow $nCols]
3780                 incr nRows
3781             }
3782             upvar 0 rowDope(row$rowIndex) thisRow
3783             set colIndex $startColNum
3784             while {$colIndex <= $stopColNum} {
3785                 if {[lindex $thisRow $colIndex] != ""} {
3786                     set badValMess1 "Multiple definitions for column"
3787                     set badValMess2 "of row $rowIndex"
3788                     UserError \
3789                         "$badValMess1 [expr $colIndex + 1] $badValMess2" yes
3790                     set stopColNum  0
3791                     set stopRowNum  0
3792                     set spanLength  0
3793                 }
3794                 incr colIndex
3795             }
3796             Replace thisRow $startColNum $spanLength $cellId
3797             incr rowIndex
3798         }
3799     }
3800
3801     # on the last column, the column separator should be 0; on the
3802     # last row, the row separator should be 0 - the table frame will
3803     # set the border on the right and bottom sides
3804     if {$stopColNum == $nCols} {
3805         set cellColSep 0
3806     }
3807     if {$currentRow == $nRowDefs} {
3808         if {($grandparent == "TFOOT") ||
3809             (($grandparent == "TBODY") && (!$haveTFoot))} {
3810             set cellRowSep 0
3811         }
3812     }
3813
3814     # push a form to hold the RowSep
3815     if {$cellRowSep == 1} {
3816         set ssi "BORDER-BOTTOM"
3817     } else {
3818         set ssi "BORDER-NONE"
3819     }
3820     PushFormCell $ssi $cellId
3821
3822     # build the SSI= for the cell and push a form to hold it
3823     if {$gi == "ENTRY"} {
3824         set ssi "ENTRY-"
3825     } else {
3826         set ssi "ENTRYTBL-"
3827     }
3828     switch $cellAlign {
3829         ""      { append ssi "NONE-" }
3830         LEFT    { append ssi "LEFT-" }
3831         RIGHT   { append ssi "RIGHT-" }
3832         CENTER  { append ssi "CENTER-" }
3833         JUSTIFY { append ssi "LEFT-" }
3834         CHAR    { append ssi "CHAR-" }
3835     }
3836     switch $cellColSep {
3837         0 { append ssi "NO-" }
3838         1 { append ssi "YES-" }
3839     }
3840     switch $cellVAlign {
3841         ""     -
3842         NONE   { append ssi "NONE" }
3843         TOP    { append ssi "TOP" }
3844         MIDDLE { append ssi "MIDDLE" }
3845         BOTTOM { append ssi "BOTTOM" }
3846     }
3847     PushForm CELL $ssi $id
3848
3849     # if we are in an Entry, open a paragraph in case all that's in
3850     # the Entry are inline objects - this may end up in an empty P
3851     # if the Entry contains paragraph level things, e.g., admonitions,
3852     # lists or paragraphs; if we are an EntryTbl, set up the defaults
3853     # for the recursive calls to, e.g., THead or TBody
3854     if {$gi == "ENTRY"} {
3855         StartParagraph "" "" ""
3856     } else {
3857         # the syntax would allow multiple TBODY in an ENTRYTBL but
3858         # we (and the rest of the SGML community, e.g., SGML/Open)
3859         # don't allow more than one - the transpec will keep us from
3860         # seeing the extras but we need to flag the error to the user
3861         if {$nTBodies != 1} {
3862             UserError "More than one TBODY in an ENTRYTBL" yes
3863         }
3864
3865         set entryTableAttributes(align) $align
3866         set entryTableAttributes(char)  [CheckChar $char]
3867
3868         # do a sanity check on the number of columns, there must be
3869         # at least 1
3870         if {$cols <= 0} {
3871             UserError "Unreasonable number of columns ($cols) in EntryTbl" yes
3872             set cols 1
3873         }
3874         set entryTableAttributes(cols)  $cols
3875
3876         if {$colSep == ""} {
3877             set entryTableAttributes(colSep) 1
3878         } else {
3879             set entryTableAttributes(colSep) $colSep
3880         }
3881         if {$rowSep == ""} {
3882             set entryTableAttributes(rowSep) 1
3883         } else {
3884             set entryTableAttributes(rowSep) $rowSep
3885         }
3886
3887         # check for more COLSPECs than COLS - error if so
3888         if {$nColSpecs > $cols} {
3889             UserError \
3890                 "More ColSpecs defined than columns in an EntryTbl" yes
3891         }
3892
3893         set numberOfColSpecs $nColSpecs
3894
3895         set entryTableColSpecs ""
3896
3897         # if no ColSpec definitions at this level, set them all to the
3898         # defaults - take advantage of the fact that the function ColSpec
3899         # will create default column specifications to fill out up to an
3900         # explicitly set ColNum
3901         if {$nColSpecs == 0} {
3902             ColSpec "" ENTRYTBL "" "" "" $cols "" "" ""
3903         }
3904
3905         # initialize a variable used to determine if we need a separate
3906         # FORM element for THead - if ColSpec elements are not given
3907         # at that level, it can go in the same FORM as the TBody and
3908         # we can guarantee that the columns will line up
3909         set needEntryTblTHeadForm 0
3910
3911         # and initialize a variable to hold saved FROWVEC elements
3912         # across THead into TBody in case we are merging them into
3913         # one FORM element rather than putting each in its own
3914         set entryTableSavedFRowVec ""
3915     }
3916 }
3917
3918
3919 # end a table Entry - pop the form holding the cell
3920 # attributes and the form holding the RowSep
3921 proc EndEntry {} {
3922     PopForm
3923     PopForm
3924 }
3925
3926
3927 # end a table EntryTbl - pop the form holding the cell
3928 # attributes and the form holding the RowSep and clean up the
3929 # global variables
3930 proc EndEntryTbl {} {
3931     global entryTableSpanSpecs numberOfColSpecs entryTableColSpecs
3932
3933     PopForm
3934     PopForm
3935
3936     if {[info exists entryTableSpanSpecs]} {
3937         unset entryTableSpanSpecs
3938     }
3939
3940     unset entryTableColSpecs
3941 }
3942
3943 ######################################################################
3944 ######################################################################
3945 #
3946 #                        RefEntry
3947 #
3948 ######################################################################
3949 ######################################################################
3950
3951 # change the OutputString routine into one that will save the content
3952 # of this element for use as the man-page title, e.g., the "cat"
3953 # in "cat(1)"; this name may be overridden by RefDescriptor in
3954 # RefNameDiv if the sort name is different (e.g., "memory" for
3955 # "malloc")
3956 proc DivertOutputToManTitle {} {
3957     rename OutputString SaveManTitleOutputString
3958     rename ManTitleOutputString OutputString
3959 }
3960
3961
3962 # change the output stream back to the OutputString in effect at the
3963 # time of the call to DivertOutputToManTitle
3964 proc RestoreOutputStreamFromManTitle {} {
3965     rename OutputString ManTitleOutputString
3966     rename SaveManTitleOutputString OutputString
3967 }
3968
3969
3970 # a routine to buffer the output into the string "manTitle" for later
3971 # use in the top corners of man-pages
3972 proc ManTitleOutputString {string} {
3973     global manTitle
3974
3975     append manTitle $string
3976 }
3977
3978
3979 # change the OutputString routine into one that will save the content
3980 # of this element for use as the man-page volume number, e.g., the "1"
3981 # in "cat(1)"
3982 proc DivertOutputToManVolNum {} {
3983     rename OutputString SaveManVolNumOutputString
3984     rename ManVolNumOutputString OutputString
3985 }
3986
3987
3988 # change the output stream back to the OutputString in effect at the
3989 # time of the call to DivertOutputToManVolNum
3990 proc RestoreOutputStreamFromManVolNum {} {
3991     rename OutputString ManVolNumOutputString
3992     rename SaveManVolNumOutputString OutputString
3993 }
3994
3995
3996 # a routine to buffer the output into the string "manVolNum" for later
3997 # use in the top corners of man-pages
3998 proc ManVolNumOutputString {string} {
3999     global manVolNum
4000
4001     append manVolNum $string
4002 }
4003
4004
4005 # start a reference name division; nothing to emit now, just save
4006 # the number of names defined in this division and initialize the
4007 # current name count to 1
4008 proc StartRefNameDiv {nNames} {
4009     global numManNames currentManName
4010
4011     set numManNames $nNames
4012     set currentManName 1
4013 }
4014
4015
4016 # end a reference name division; we can now emit the HEAD elements to
4017 # create the titles in the upper corners and the "NAME" section of the
4018 # man-page
4019 proc EndRefNameDiv {id} {
4020     global manTitle manVolNum manDescriptor manNames manPurpose
4021     global localizedAutoGeneratedStringArray
4022
4023     set manPageName $manTitle
4024     if {$manDescriptor != ""} {
4025         set manPageName $manDescriptor
4026     }
4027
4028     # emit the titles in the upper left and right corners
4029     Emit "<HEAD TYPE=\"LITERAL\" SSI=\"MAN-PAGE-TITLE-LEFT\">"
4030     Emit "${manPageName}($manVolNum)"
4031     Emit "</HEAD>\n"
4032     Emit "<HEAD TYPE=\"LITERAL\" SSI=\"MAN-PAGE-TITLE-RIGHT\">"
4033     Emit "${manPageName}($manVolNum)"
4034     Emit "</HEAD>\n"
4035
4036     # and the NAME section
4037     PushForm "" "" $id
4038     Emit "<HEAD TYPE=\"LITERAL\" SSI=\"MAN-PAGE-DIVISION-NAME\">"
4039     set message "NAME"
4040     Emit $localizedAutoGeneratedStringArray($message)
4041     Emit "</HEAD>\n"
4042     StartBlock "" "MAN-PAGE-DIVISION" "" 1
4043     StartParagraph "" "" ""
4044     Emit "$manNames - $manPurpose"
4045     PopForm
4046 }
4047
4048
4049 # change the OutputString routine into one that will save the content
4050 # of this element for use as the man-page descriptor, e.g., the
4051 # "string" in "string(3C)"
4052 proc DivertOutputToManDescriptor {} {
4053     rename OutputString SaveManDescriptorOutputString
4054     rename ManDescriptorOutputString OutputString
4055 }
4056
4057
4058 # change the output stream back to the OutputString in effect at the
4059 # time of the call to DivertOutputToManDescriptor
4060 proc RestoreOutputStreamFromManDescriptor {} {
4061     rename OutputString ManDescriptorOutputString
4062     rename SaveManDescriptorOutputString OutputString
4063 }
4064
4065
4066 # a routine to buffer the output into the string "manDescriptor" for
4067 # later use in the top corners of man-pages
4068 proc ManDescriptorOutputString {string} {
4069     global manDescriptor
4070
4071     append manDescriptor $string
4072 }
4073
4074
4075 # change the OutputString routine into one that will save the content
4076 # of this element for use as the man-page command or function name,
4077 # e.g., the "cat" in "cat(1)"
4078 proc DivertOutputToManNames {} {
4079     rename OutputString SaveManNamesOutputString
4080     rename ManNamesOutputString OutputString
4081 }
4082
4083
4084 # change the output stream back to the OutputString in effect at the
4085 # time of the call to DivertOutputToManNames
4086 proc RestoreOutputStreamFromManNames {} {
4087     rename OutputString ManNamesOutputString
4088     rename SaveManNamesOutputString OutputString
4089 }
4090
4091
4092 # a routine to buffer the output into the string "manNames" for
4093 # later use in the top corners of man-pages
4094 proc ManNamesOutputString {string} {
4095     global manNames
4096
4097     append manNames $string
4098 }
4099
4100
4101 # collect RefName elements into a single string; start diversion to
4102 # the string on the first man name
4103 proc StartAManName {} {
4104     global numManNames currentManName
4105
4106     if {$currentManName == 1} {
4107         DivertOutputToManNames
4108     }
4109 }
4110
4111
4112 # end diversion on the last man name; append "(), " to each name but
4113 # the last to which we only append "()"
4114 proc EndAManName {} {
4115     global numManNames currentManName manDescriptor manNames
4116
4117     if {($currentManName == 1) && ($manDescriptor == "")} {
4118         set manDescriptor $manNames
4119     }
4120
4121     if {$currentManName < $numManNames} {
4122         Emit ", "
4123     } elseif {$currentManName == $numManNames} {
4124         RestoreOutputStreamFromManNames
4125     }
4126
4127     incr currentManName
4128 }
4129
4130
4131 # change the OutputString routine into one that will save the content
4132 # of this element for use as the man-page purpose; this string will
4133 # follow the function or command name(s) separated by a "-"
4134 proc DivertOutputToManPurpose {} {
4135     rename OutputString SaveManPurposeOutputString
4136     rename ManPurposeOutputString OutputString
4137 }
4138
4139
4140 # change the output stream back to the OutputString in effect at the
4141 # time of the call to DivertOutputToManPurpose
4142 proc RestoreOutputStreamFromManPurpose {} {
4143     rename OutputString ManPurposeOutputString
4144     rename SaveManPurposeOutputString OutputString
4145 }
4146
4147
4148 # a routine to buffer the output into the string "manPurpose" for
4149 # later use in the NAME section of man-pages
4150 proc ManPurposeOutputString {string} {
4151     global manPurpose
4152
4153     append manPurpose $string
4154 }
4155
4156
4157 # start a reference synopsis division - create a FORM to hold the
4158 # division and, potentially, any RefSect2-3; if there is a Title on
4159 # RefSynopsisDiv, use it, else default to "SYNOPSIS"
4160 proc StartRefSynopsisDiv {id haveTitle nSynopses} {
4161     global remainingSynopses
4162     global localizedAutoGeneratedStringArray
4163
4164     set remainingSynopses $nSynopses
4165     PushForm "" "" $id
4166     if {!$haveTitle} {
4167         StartManPageDivisionTitle ""
4168         set message "SYNOPSIS"
4169         Emit $localizedAutoGeneratedStringArray($message)
4170         EndManPageDivisionTitle
4171     }
4172 }
4173
4174
4175 # the user provided a title for this section, use it
4176 proc StartManPageDivisionTitle {id} {
4177     if {$id != ""} {
4178         set id " ID=\"$id\""
4179     }
4180     Emit "<HEAD$id TYPE=\"LITERAL\" SSI=\"MAN-PAGE-DIVISION-NAME\">"
4181 }
4182
4183
4184 # the user provided a title for this section, we need to open a form
4185 # to hold the section now
4186 proc EndManPageDivisionTitle {} {
4187     Emit "</HEAD>\n"
4188     PushForm "" "MAN-PAGE-DIVISION" ""
4189 }
4190
4191 # begin a Synopsis - if this is the first of any of the synopses, emit
4192 # a FORM to hold them all
4193 proc StartSynopsis {id linespecific} {
4194     if {$linespecific == ""} {
4195         set type LINED
4196     } else {
4197         set type ""
4198     }
4199     StartParagraph id "" $type
4200 }
4201
4202
4203 # end any of Synopsis, CmdSynopsis or FuncSynopsis - close out the
4204 # form if it's the last one
4205 proc EndSynopses {parent} {
4206     global remainingSynopses
4207
4208     Emit "\n"
4209
4210     if {($parent == "REFSYNOPSISDIV") && ([incr remainingSynopses -1] == 0)} {
4211         PopForm
4212     }
4213 }
4214
4215
4216 # begin a CmdSynopsis
4217 proc StartCmdSynopsis {id} {
4218     StartParagraph id "" ""
4219 }
4220
4221
4222 # start a man-page argument - surround the arg in a KEY element
4223 proc StartArg {id choice separator} {
4224     # mark this spot if there's a user supplied ID
4225     Anchor $id
4226
4227     # emit nothing at start of list, v-bar inside of Group else space
4228     Emit $separator
4229
4230     Emit "<KEY CLASS=\"NAME\" SSI=\"MAN-PAGE-ARG\">"
4231     if {$choice == "OPT"} {
4232         Emit "\["
4233     } elseif {$choice == "REQ"} {
4234         Emit "\{"
4235     }
4236 }
4237
4238
4239 # end a man-page argument - if choice is not "plain", emit the proper
4240 # close character for the choice; if repeat is "repeat", emit an
4241 # ellipsis after the arg
4242 proc EndArg {choice repeat} {
4243     if {$choice == "OPT"} {
4244         Emit "\]"
4245     } elseif {$choice == "REQ"} {
4246         Emit "\}"
4247     }
4248     if {$repeat == "REPEAT"} {
4249         Emit "<SPC NAME=\"\[hellip\]\">"
4250     }
4251     Emit "</KEY>"
4252 }
4253
4254
4255 # start an argument, filename, etc., group in a man-page command
4256 # synopsis
4257 proc StartGroup {id choice separator} {
4258     # mark this spot if there's a user supplied ID
4259     Anchor $id
4260
4261     # emit nothing at start of list, v-bar inside of Group else space
4262     Emit $separator
4263
4264     # clean up optmult/reqmult since, for example, req+repeat == reqmult,
4265     # optmult and reqmult are redundant
4266     if {$choice == "OPTMULT"} {
4267         set choice OPT
4268     } elseif {$choice == "REQMULT"} {
4269         set choice REQ
4270     }
4271
4272     if {$choice == "OPT"} {
4273         Emit "\["
4274     } elseif {$choice == "REQ"} {
4275         Emit "\{"
4276     }
4277 }
4278
4279
4280 # end an argument, filename, etc., group in a man-page command
4281 # synopsis
4282 proc EndGroup {choice repeat} {
4283     # clean up optmult/reqmult since, for example, req+repeat == reqmult,
4284     # optmult and reqmult are redundant
4285     if {$choice == "OPTMULT"} {
4286         set choice OPT
4287         set repeat REPEAT
4288     } elseif {$choice == "REQMULT"} {
4289         set choice "REQ"
4290         set repeat REPEAT
4291     }
4292     if {$choice == "OPT"} {
4293         Emit "\]"
4294     } elseif {$choice == "REQ"} {
4295         Emit "\}"
4296     }
4297     if {$repeat == "REPEAT"} {
4298         Emit "<SPC NAME=\"\[hellip\]\">"
4299     }
4300 }
4301
4302
4303 # start a command name in a man-page command synopsis
4304 proc StartCommand {id separator} {
4305     # mark this spot if there's a user supplied ID
4306     Anchor $id
4307
4308     # emit nothing at start of synopsis else space
4309     Emit $separator
4310
4311     Emit "<KEY CLASS=\"NAME\" SSI=\"MAN-PAGE-COMMAND\">"
4312 }
4313
4314
4315 # begin a FuncSynopsis
4316 proc StartFuncSynopsis {id} {
4317 }
4318
4319
4320 # check that the GI of the element pointed to by a SynopFragmentRef
4321 # is really a SynopFragment
4322 proc CheckSynopFragmentRef {gi id} {
4323     if {$gi != "SYNOPFRAGMENT"} {
4324         set badValMess1 "SynopFragmentRef LinkEnd=$id"
4325         set badValMess2 "must refer to a SynopFragment"
4326         UserError "$badValMess1 $badValMess2" yes
4327     }
4328 }
4329
4330
4331 # begin a FuncSynopsisInfo - emit a P to hold it
4332 proc StartFuncSynopsisInfo {id linespecific} {
4333     if {$linespecific == "LINESPECIFIC"} {
4334         set type " TYPE=\"LINED\""
4335     } else {
4336         set type ""
4337     }
4338
4339     StartParagraph $id "FUNCSYNOPSISINFO" $type
4340 }
4341
4342
4343 # begin a FuncDef - emit a P to hold it
4344 proc StartFuncDef {id} {
4345     StartParagraph $id "FUNCDEF" ""
4346 }
4347
4348
4349 # end a FuncDef, emit the open paren in preparation for the args
4350 proc EndFuncDef {} {
4351     Emit "("
4352 }
4353
4354
4355 # handle Void or Varargs in a FuncSynopsis - wrap it in a KEY and
4356 # emit the string "VOID" or "VARARGS"
4357 proc DoVoidOrVarargs {gi id} {
4358     # mark this spot if there's a user supplied ID
4359     Anchor $id
4360
4361     Emit "<KEY CLASS=\"NAME\" SSI=\"FUNCDEF-ARGS\">"
4362     Emit $gi
4363     Emit "</KEY>"
4364     Emit ")"
4365 }
4366
4367
4368 # start a ParamDef - just emit an anchor, if needed, for now
4369 proc StartParamDef {id} {
4370     # mark this spot if there's a user supplied ID
4371     Anchor $id
4372 }
4373
4374
4375 # end of a ParamDef - emit either the ", " for the next one or, if the
4376 # last, emit the closing ")"
4377 proc EndParamDef {separator} {
4378     Emit $separator
4379 }
4380
4381
4382 # start a FuncParams - just emit an anchor, if needed, for now
4383 proc StartFuncParams {id} {
4384     # mark this spot if there's a user supplied ID
4385     Anchor $id
4386 }
4387
4388
4389 # end of a FuncParams - emit either the ", " for the next one or, if the
4390 # last, emit the closing ")"
4391 proc EndFuncParams {separator} {
4392     Emit $separator
4393 }
4394
4395
4396 ######################################################################
4397 ######################################################################
4398 #
4399 #                             links
4400 #
4401 ######################################################################
4402 ######################################################################
4403 # open an intradocument link
4404 proc StartLink {id linkend type} {
4405     StartParagraphMaybe "" "P" $id
4406
4407     Emit "<LINK RID=\"$linkend\""
4408     if {$type != ""} {
4409         set type [string toupper $type]
4410         switch $type {
4411             JUMPNEWVIEW {Emit " WINDOW=\"NEW\""}
4412             DEFINITION  {Emit " WINDOW=\"POPUP\""}
4413         }
4414     }
4415     Emit ">"
4416
4417     Anchor $id
4418 }
4419
4420
4421 # defer a Link at the start of a Para until we see if the following 
4422 # InlineGraphic has Role=graphic and we want it in a HEAD
4423 proc DeferLink {id linkend type} {
4424     global deferredLink
4425
4426     set deferredLink(gi)      LINK
4427     set deferredLink(id)      $id
4428     set deferredLink(linkend) $linkend
4429     set deferredLink(type)    $type
4430 }
4431
4432
4433 # open an interdocument link; this link will require an SNB entry
4434 proc StartOLink {id localInfo type} {
4435     StartParagraphMaybe "" "P" $id
4436
4437     set type [string toupper $type]
4438
4439     set linkType CURRENT
4440     switch $type {
4441         JUMP        {set linkType CURRENT}
4442         JUMPNEWVIEW {set linkType NEW}
4443         MAN         -
4444         DEFINITION  {set linkType POPUP}
4445     }
4446
4447     set snbType CROSSDOC
4448     switch $type {
4449         EXECUTE     {set snbType SYS-CMD}
4450         APP-DEFINED {set snbType CALLBACK}
4451         MAN         {set snbType MAN-PAGE}
4452     }
4453
4454     set snbId [AddToSNB $snbType $localInfo]
4455
4456     Emit "<LINK RID=\"$snbId\""
4457     if {$linkType != "CURRENT"} {
4458         Emit " WINDOW=\"$linkType\""
4459     }
4460     Emit ">"
4461 }
4462
4463
4464 # defer an OLink at the start of a Para until we see if the following 
4465 # InlineGraphic has Role=graphic and we want it in a HEAD
4466 proc DeferOLink {id localInfo type} {
4467     global deferredLink
4468
4469     set deferredLink(gi)        OLINK
4470     set deferredLink(id)        $id
4471     set deferredLink(localinfo) $localinfo
4472     set deferredLink(type)      $type
4473 }
4474
4475
4476 # defer a ULink at the start of a Para until we see if the following 
4477 # InlineGraphic has Role=graphic and we want it in a HEAD
4478 proc DeferULink {id} {
4479     global deferredLink
4480
4481     set deferredLink(gi)        ULINK
4482     set deferredLink(id)        $id
4483 }
4484
4485
4486 # close a link
4487 proc EndLink {} {
4488     Emit "</LINK>"
4489 }
4490
4491
4492 ######################################################################
4493 ######################################################################
4494 #
4495 #                       character formatting
4496 #
4497 ######################################################################
4498 ######################################################################
4499 # open a Quote; we'll emit two open single quotes wrapped in a
4500 # key with a style that will put them in a proportional font so they
4501 # fit together and look like an open double quote
4502 proc StartQuote {id} {
4503   Emit "<KEY CLASS=\"QUOTE\" SSI=\"PROPORTIONAL\">"
4504   Anchor $id
4505   Emit "``</KEY>"
4506 }
4507
4508 # close a Quote; we'll emit two close single quotes wrapped in a
4509 # key with a style that will put them in a proportional font so they
4510 # fit together and look like a close double quote
4511 proc EndQuote {} {
4512   Emit "<KEY CLASS=\"QUOTE\" SSI=\"PROPORTIONAL\">''</KEY>"
4513 }
4514
4515 ######################################################################
4516 ######################################################################
4517 #
4518 #                      end of document stuff
4519 #
4520 ######################################################################
4521 ######################################################################
4522
4523 # write out the .snb file - first update the file location for
4524 # insertion of the SNB by the second pass to reflect the addition
4525 # of the INDEX; also incorporate the INDEX and update the TOSS to
4526 # reflect any additions necessary to support tables
4527 proc WriteSNB {} {
4528     global savedSNB indexLocation tossLocation baseName
4529
4530     # get a handle for the index file and the existing .sdl file;
4531     # prepare to write the updated .sdl file and the .snb file by
4532     # blowing away the current names so the second open of the .sdl
4533     # file is creating a new file and we don't have leftover .snb
4534     # or .idx files laying around
4535     close stdout
4536     set sdlInFile [open "${baseName}.sdl" r]
4537     set sdlSize   [file size "${baseName}.sdl"]
4538     #
4539     set idxFile   [open "${baseName}.idx" r]
4540     set idxSize   [file size "${baseName}.idx"]
4541     #
4542     exec rm -f ${baseName}.sdl ${baseName}.idx ${baseName}.snb
4543     set sdlOutFile [open "${baseName}.sdl" w]
4544
4545     # create any additional TOSS entries made necessary by COLW and
4546     # COLJ settings for TGroup or EntryTbl elements.
4547     set toss [CreateTableTOSS]
4548     set tossSize [string length $toss]
4549
4550     # get a list of the byte offsets into the .sdl file for the
4551     # .snb entries
4552     set snbLocations [lsort -integer [array names savedSNB]]
4553
4554     # and write out the .snb file updating the locations as we go
4555     if {[llength $snbLocations] > 0} {
4556         set snbFile [open "${baseName}.snb" w]
4557         foreach location $snbLocations {
4558             puts $snbFile [expr "$location + $idxSize + $tossSize"]
4559             puts -nonewline $snbFile $savedSNB($location)
4560         }
4561         close $snbFile
4562     }
4563
4564     # now update the toss and include the index file into the sdl file
4565     # by copying the old .sdl file to the new up to the location of
4566     # the first FORMSTYLE in the TOSS and emitting the new TOSS
4567     # entries then continue copying the old .sdl file up to the index
4568     # location and copying the .idx file to the new .sdl file followed
4569     # by the rest of the old .sdl file (the old .sdl and .idx files
4570     # have already been deleted from the directory), finally, close
4571     # the output file
4572     #
4573     # 1: copy the sdl file up to the first FORMSTYLE element or, if
4574     #    none, to just after the open tag for the TOSS
4575     set location $tossLocation
4576     set readSize 1024
4577     while {$location > 0} {
4578         if {$location < $readSize} { set readSize $location }
4579         puts -nonewline $sdlOutFile [read $sdlInFile $readSize]
4580         incr location -$readSize
4581     }
4582     # 2: emit the TOSS updates, if any
4583     puts -nonewline $sdlOutFile $toss
4584     # 3: copy the sdl file up to the index location
4585     set location [expr "$indexLocation - $tossLocation"]
4586     set readSize 1024
4587     while {$location > 0} {
4588         if {$location < $readSize} { set readSize $location }
4589         puts -nonewline $sdlOutFile [read $sdlInFile $readSize]
4590         incr location -$readSize
4591     }
4592     # 4: copy over the index file
4593     set location $idxSize
4594     set readSize 1024
4595     while {$location > 0} {
4596         if {$location < $readSize} { set readSize $location }
4597         puts -nonewline $sdlOutFile [read $idxFile $readSize]
4598         incr location -$readSize
4599     }
4600     # 5: and copy over the rest of the sdl file
4601     set location [expr "$sdlSize - $indexLocation"]
4602     set readSize 1024
4603     while {$location > 0} {
4604         if {$location < $readSize} { set readSize $location }
4605         puts -nonewline $sdlOutFile [read $sdlInFile $readSize]
4606         incr location -$readSize
4607     }
4608     # 6: close the output
4609     close $sdlOutFile
4610 }
4611
4612
4613 # read the global variable newTOSS and use the information to create
4614 # TOSS entries for THead, TBody and TFoot; these entries will contain
4615 # the justification and width information for the table sub-components;
4616 # return the new TOSS elements
4617 proc CreateTableTOSS {} {
4618     global newTOSS
4619
4620     set returnValue ""
4621     foreach ssi [array names newTOSS] {
4622         array set thisTOSSdata $newTOSS($ssi)
4623         set vAlign $thisTOSSdata(vAlign)
4624         switch $vAlign {
4625             NONE -
4626             ""      { set vJust "" }
4627             TOP     { set vJust "TOP" }
4628             MIDDLE  { set vJust "CENTER" }
4629             BOTTOM  { set vJust "BOTTOM" }
4630         }
4631
4632         append returnValue "<FORMSTYLE\n"
4633         append returnValue "    CLASS=\"TABLE\"\n"
4634         append returnValue "    SSI=\"$ssi\"\n"
4635         append returnValue \
4636                     "    PHRASE=\"TGroup, THead or TBody specification\"\n"
4637         append returnValue "    COLW=\"$thisTOSSdata(colW)\"\n"
4638         append returnValue "    COLJ=\"$thisTOSSdata(colJ)\"\n"
4639         if {$vJust != ""} {
4640             append returnValue "    VJUST=\"${vJust}-VJUST\"\n"
4641         }
4642         append returnValue ">\n"
4643     }
4644
4645     return $returnValue
4646 }
4647
4648
4649 # try to open a file named docbook.tss either in our current
4650 # directory or on TOSS_PATH - if it exists, copy it to
4651 # the output file as the TOSS - when the first line containing
4652 # "<FORMSTYLE" is seen, save the location so we can include the
4653 # updates to the TOSS necessary due to needing FORMSTYLE entries for
4654 # tables with the appropriate COLJ and COLW values
4655 proc IncludeTOSS {} {
4656     global tossLocation TOSS_PATH
4657
4658     set tossLocation -1
4659     set foundToss     0
4660
4661     # look for docbook.tss in the current directory first, then on the path
4662     set path ". [split $TOSS_PATH :]"
4663     foreach dir $path {
4664         set tssFileName $dir/docbook.tss
4665         if {[file exists $tssFileName]} {
4666             set foundToss 1
4667             break;
4668         }
4669     }
4670
4671     if {$foundToss} {
4672         if {[file readable $tssFileName]} {
4673             set tssFile [open $tssFileName r]
4674             set eof [gets $tssFile line]
4675             while {$eof != -1} {
4676                 if {[string match "*<FORMSTYLE*" [string toupper $line]]} {
4677                     set tossLocation [tell stdout]
4678                 }
4679                 puts $line
4680                 set eof [gets $tssFile line]
4681             }
4682             close $tssFile
4683         } else {
4684             UserError "$tssFileName exists but is not readable" no
4685         }
4686     } else {
4687         UserWarning "Could not find docbook.tss - continuing with null TOSS" no
4688     }
4689
4690     if {$tossLocation == -1} {
4691         set tossLocation [tell stdout]
4692     }
4693 }
4694
4695 proc GetLocalizedAutoGeneratedStringArray {filename} {
4696     global localizedAutoGeneratedStringArray
4697
4698     set buffer [ReadLocaleStrings $filename]
4699
4700     set regExp {^(".*")[         ]*(".*")$} ;# look for 2 quoted strings
4701
4702     set stringList [split $buffer \n]
4703     set listLength [llength $stringList]
4704     set index 0
4705     while {$listLength > 0} {
4706         set line [lindex $stringList $index]
4707         set line [string trim $line]
4708         if {([string length $line] > 0) && ([string index $line 0] != "#")} {
4709             if {[regexp $regExp $line match match1 match2]} {
4710                 set match1 [string trim $match1 \"]
4711                 set match2 [string trim $match2 \"]
4712                 set localizedAutoGeneratedStringArray($match1) $match2
4713             } else {
4714                 UserError \
4715                     "Malformed line in $filename line [expr $index + 1]" no
4716             }
4717         }
4718         incr index
4719         incr listLength -1
4720     }
4721
4722     set message "Home Topic"
4723     if {![info exists localizedAutoGeneratedStringArray($message)]} {
4724         set localizedAutoGeneratedStringArray($message) $message
4725     }
4726     set message "No home topic (PartIntro) was specified by the author."
4727     if {![info exists localizedAutoGeneratedStringArray($message)]} {
4728         set localizedAutoGeneratedStringArray($message) $message
4729     }
4730     set message "See"
4731     if {![info exists localizedAutoGeneratedStringArray($message)]} {
4732         set localizedAutoGeneratedStringArray($message) $message
4733     }
4734     set message "See Also"
4735     if {![info exists localizedAutoGeneratedStringArray($message)]} {
4736         set localizedAutoGeneratedStringArray($message) $message
4737     }
4738     set message "NAME"
4739     if {![info exists localizedAutoGeneratedStringArray($message)]} {
4740         set localizedAutoGeneratedStringArray($message) $message
4741     }
4742     set message "SYNOPSIS"
4743     if {![info exists localizedAutoGeneratedStringArray($message)]} {
4744         set localizedAutoGeneratedStringArray($message) $message
4745     }
4746 }
4747
4748
4749 # start - initialize variables and write the preamble
4750 proc OpenDocument {host base date} {
4751     global docId baseName indexLocation snbLocation
4752     global validMarkArray partIntroId nextId
4753     global NO_UNIQUE_ID LOCALE_STRING_DIR
4754     global language charset
4755
4756     # NO_UNIQUE_ID will be set to YES for test purposes so we don't
4757     # get spurious mismatches from the timestamp of from the system on
4758     # which the document was processed.
4759     if {[string toupper $NO_UNIQUE_ID] == "YES"} {
4760         set docId TEST
4761         set timeStamp 0
4762     } else {
4763         set docId $host
4764         set timeStamp $date
4765     }
4766
4767     GetLocalizedAutoGeneratedStringArray ${LOCALE_STRING_DIR}/strings
4768
4769     # split out the language and charset info from LOCALE_STRING_DIR
4770     #   first, remove any directory information
4771     set languageAndCharset [lindex [split $LOCALE_STRING_DIR /] end]
4772     #   then split the language and charset at the dot
4773     set languageAndCharset [split $languageAndCharset .]
4774     #   and extract the values from the resulting list
4775     set language [lindex $languageAndCharset 0]
4776     set charset  [lindex $languageAndCharset 1]
4777
4778     set baseName $base
4779
4780     # set up the validMarkArray values
4781     ReInitPerMarkInfo
4782
4783     # if we have a PartIntro element, use its ID as the first-page
4784     # attribute - if no ID, assign one; if no PartIntro, assign an
4785     # ID and we'll dummy in a hometopic when we try to emit the first
4786     # level 1 virpage
4787     if {![info exists partIntroId]} {
4788         set partIntroId ""
4789     }
4790     if {$partIntroId == ""} {
4791         # set partIntroId SDL-RESERVED[incr nextId]
4792         set partIntroId SDL-RESERVED-HOMETOPIC
4793     }
4794     
4795     # open the document
4796     Emit "<SDLDOC PUB-ID=\"CDE 2.1\""
4797     Emit " DOC-ID=\"$docId\""
4798     Emit " LANGUAGE=\"$language\""
4799     Emit " CHARSET=\"$charset\""
4800     Emit " FIRST-PAGE=\"$partIntroId\""
4801     Emit " TIMESTMP=\"$timeStamp\""
4802     Emit " SDLDTD=\"1.1.1\">\n"
4803
4804     # and create the VSTRUCT - the INDEX goes in it, the SNB goes after
4805     # it; if there's a Title later, it'll reset the SNB location;
4806     # we also need to read in docbook.tss (if any) and to create an
4807     # empty TOSS to cause the second pass to replace docbook.tss with
4808     # <src file name>.tss (if any) in the new .sdl file
4809     Emit "<VSTRUCT DOC-ID=\"$docId\">\n"
4810     Emit "<LOIDS>\n</LOIDS>\n<TOSS>\n"
4811     IncludeTOSS
4812     Emit "</TOSS>\n"
4813     set indexLocation [tell stdout]
4814     Emit "</VSTRUCT>\n"
4815     set snbLocation [tell stdout]
4816 }
4817
4818
4819 # done - write the index and close the document
4820 proc CloseDocument {} {
4821     global inVirpage errorCount warningCount
4822     global snbLocation savedSNB currentSNB
4823
4824     # close any open block and the current VIRPAGE
4825     CloseBlock
4826     Emit $inVirpage; set inVirpage ""
4827
4828     # if the last VIRPAGE in the document had any system notation
4829     # block references, we need to add them to the saved snb array
4830     # before writing it out
4831     set names [array names currentSNB]
4832     if {[llength $names] != 0} {
4833         foreach name $names {
4834             # split the name into the GI and xid of the SNB entry
4835             set colonLoc [string first "::" $name]
4836             set type [string range $name 0 [incr colonLoc -1]]
4837             set data [string range $name [incr colonLoc 3] end]
4838
4839             # emit the entry
4840             append tempSNB "<$type ID=\"$currentSNB($name)\" "
4841             switch $type {
4842                 GRAPHIC   -
4843                 AUDIO     -
4844                 VIDEO     -
4845                 ANIMATE   -
4846                 CROSSDOC  -
4847                 MAN-PAGE  -
4848                 TEXTFILE  { set command "XID" }
4849                 SYS-CMD   { set command "COMMAND" }
4850                 CALLBACK  { set command "DATA" }
4851             }
4852             append tempSNB "$command=\"$data\">\n"
4853         }
4854         set savedSNB($snbLocation) $tempSNB
4855         unset currentSNB
4856     }
4857
4858     # close the document and write out the stored index and system
4859     # notation block
4860     Emit "</SDLDOC>\n"
4861     WriteIndex
4862     WriteSNB
4863
4864     if {$errorCount || $warningCount} {
4865         puts stderr "DtDocBook total user errors:   $errorCount"
4866         puts stderr "DtDocBook total user warnings: $warningCount"
4867     }
4868
4869     if {$errorCount > 0} {
4870         exit 1
4871     }
4872
4873     if {$warningCount > 0} {
4874         exit -1
4875     }
4876 }