home *** CD-ROM | disk | FTP | other *** search
/ PC World 2000 December / PCWorld_2000-12_cd.bin / Komunikace / Comanche / xuibuilder / TclXML-1.1.1 / sgml.tcl < prev    next >
Text File  |  2000-11-02  |  46KB  |  1,654 lines

  1. # sgml.tcl --
  2. #
  3. #    This file provides generic parsing services for SGML-based
  4. #    languages, namely HTML and XML.
  5. #
  6. #    NB.  It is a misnomer.  There is no support for parsing
  7. #    arbitrary SGML as such.
  8. #
  9. # Copyright (c) 1998,1999 Zveno Pty Ltd
  10. # http://www.zveno.com/
  11. #
  12. # Zveno makes this software available free of charge for any purpose.
  13. # Copies may be made of this software but all of this notice must be included
  14. # on any copy.
  15. #
  16. # The software was developed for research purposes only and Zveno does not
  17. # warrant that it is error free or fit for any purpose.  Zveno disclaims any
  18. # liability for all claims, expenses, losses, damages and costs any user may
  19. # incur as a result of using, copying or modifying this software.
  20. #
  21. # Copyright (c) 1997 ANU and CSIRO on behalf of the
  22. # participants in the CRC for Advanced Computational Systems ('ACSys').
  23. # ACSys makes this software and all associated data and documentation 
  24. # ('Software') available free of charge for any purpose.  You may make copies 
  25. # of the Software but you must include all of this notice on any copy.
  26. # The Software was developed for research purposes and ACSys does not warrant
  27. # that it is error free or fit for any purpose.  ACSys disclaims any
  28. # liability for all claims, expenses, losses, damages and costs any user may
  29. # incur as a result of using, copying or modifying the Software.
  30. #
  31. # $Id: sgml.tcl,v 1.1.1.1 1996/02/22 06:06:14 daniel Exp $
  32.  
  33. package provide sgml 1.6
  34.  
  35. namespace eval sgml {
  36.     namespace export tokenise parseEvent
  37.  
  38.     namespace export parseDTD
  39.  
  40.     # Convenience routine
  41.     proc cl x {
  42.     return "\[$x\]"
  43.     }
  44.  
  45.     # Define various regular expressions
  46.     # white space
  47.     variable Wsp " \t\r\n"
  48.     variable noWsp [cl ^$Wsp]
  49.  
  50.     # Various XML names
  51.     variable nmtoken [cl -a-zA-Z0-9._]+
  52.     variable name [cl a-zA-Z_][cl -a-zA-Z0-9._]*
  53.  
  54.     # Other
  55.     variable ParseEventNum
  56.     if {![info exists ParseEventNum]} {
  57.     set ParseEventNum 0
  58.     }
  59.     variable ParseDTDnum
  60.     if {![info exists ParseDTDNum]} {
  61.     set ParseDTDNum 0
  62.     }
  63.  
  64.     # table of predefined entities for XML
  65.  
  66.     variable EntityPredef
  67.     array set EntityPredef {
  68.     lt <   gt >   amp &   quot \"   apos '
  69.     }
  70.  
  71. }
  72.  
  73. # sgml::tokenise --
  74. #
  75. #    Transform the given HTML/XML text into a Tcl list.
  76. #
  77. # Arguments:
  78. #    sgml        text to tokenize
  79. #    elemExpr    RE to recognise tags
  80. #    elemSub        transform for matched tags
  81. #    args        options
  82. #
  83. # Valid Options:
  84. #    -final        boolean        True if no more data is to be supplied
  85. #    -statevariable    varName        Name of a variable used to store info
  86. #
  87. # Results:
  88. #    Returns a Tcl list representing the document.
  89.  
  90. proc sgml::tokenise {sgml elemExpr elemSub args} {
  91.     array set options {-final 1}
  92.     catch {array set options $args}
  93.     set options(-final) [Boolean $options(-final)]
  94.  
  95.     # If the data is not final then there must be a variable to store
  96.     # unused data.
  97.     if {!$options(-final) && ![info exists options(-statevariable)]} {
  98.     return -code error {option "-statevariable" required if not final}
  99.     }
  100.  
  101.     # Pre-process stage
  102.     #
  103.     # Extract the internal DTD subset, if any
  104.  
  105.     catch {upvar #0 $options(-internaldtdvariable) dtd}
  106.     if {[regexp {<!DOCTYPE[^[<]+\[([^]]+)\]} $sgml discard dtd]} {
  107.     regsub {(<!DOCTYPE[^[<]+)(\[[^]]+\])} $sgml {\1\&xml:intdtd;} sgml
  108.     }
  109.  
  110.     # Protect Tcl special characters
  111.     regsub -all {([{}\\])} $sgml {\\\1} sgml
  112.  
  113.     # Do the translation
  114.  
  115.     if {[info exists options(-statevariable)]} {
  116.     upvar #0 $opts(-statevariable) unused
  117.     if {[info exists unused]} {
  118.         regsub -all $elemExpr $unused$sgml $elemSub sgml
  119.         unset unused
  120.     } else {
  121.         regsub -all $elemExpr $sgml $elemSub sgml
  122.     }
  123.     set sgml "{} {} {} {} \{$sgml\}"
  124.  
  125.     # Performance note (Tcl 8.0):
  126.     #    Use of lindex, lreplace will cause parsing to list object
  127.  
  128.     if {[regexp {^([^<]*)(<[^>]*$)} [lindex $sgml end] x text unused]} {
  129.         set sgml [lreplace $sgml end end $text]
  130.     }
  131.  
  132.     } else {
  133.  
  134.     # Performance note (Tcl 8.0):
  135.     #    In this case, no conversion to list object is performed
  136.  
  137.     regsub -all $elemExpr $sgml $elemSub sgml
  138.     set sgml "{} {} {} {} \{$sgml\}"
  139.     }
  140.  
  141.     return $sgml
  142.  
  143. }
  144.  
  145. # sgml::parseEvent --
  146. #
  147. #    Produces an event stream for a XML/HTML document,
  148. #    given the Tcl list format returned by tokenise.
  149. #
  150. #    This procedure checks that the document is well-formed,
  151. #    and throws an error if the document is found to be not
  152. #    well formed.  Warnings are passed via the -warningcommand script.
  153. #
  154. #    The procedure only check for well-formedness,
  155. #    no DTD is required.  However, facilities are provided for entity expansion.
  156. #
  157. # Arguments:
  158. #    sgml        Instance data, as a Tcl list.
  159. #    args        option/value pairs
  160. #
  161. # Valid Options:
  162. #    -final            Indicates end of document data
  163. #    -elementstartcommand    Called when an element starts
  164. #    -elementendcommand    Called when an element ends
  165. #    -characterdatacommand    Called when character data occurs
  166. #    -entityreferencecommand    Called when an entity reference occurs
  167. #    -processinginstructioncommand    Called when a PI occurs
  168. #    -externalentityrefcommand    Called for an external entity reference
  169. #
  170. #    (Not compatible with expat)
  171. #    -xmldeclcommand        Called when the XML declaration occurs
  172. #    -doctypecommand        Called when the document type declaration occurs
  173. #    -commentcommand        Called when a comment occurs
  174. #
  175. #    -errorcommand        Script to evaluate for a fatal error
  176. #    -warningcommand        Script to evaluate for a reportable warning
  177. #    -statevariable        global state variable
  178. #    -normalize        whether to normalize names
  179. #    -reportempty        whether to include an indication of empty elements
  180. #
  181. # Results:
  182. #    The various callback scripts are invoked.
  183. #    Returns empty string.
  184. #
  185. # BUGS:
  186. #    If command options are set to empty string then they should not be invoked.
  187.  
  188. proc sgml::parseEvent {sgml args} {
  189.     variable Wsp
  190.     variable noWsp
  191.     variable nmtoken
  192.     variable name
  193.     variable ParseEventNum
  194.  
  195.     array set options [list \
  196.     -elementstartcommand        [namespace current]::noop    \
  197.     -elementendcommand        [namespace current]::noop    \
  198.     -characterdatacommand        [namespace current]::noop    \
  199.     -processinginstructioncommand    [namespace current]::noop    \
  200.     -externalentityrefcommand    [namespace current]::noop    \
  201.     -xmldeclcommand            [namespace current]::noop    \
  202.     -doctypecommand            [namespace current]::noop    \
  203.     -commentcommand            [namespace current]::noop    \
  204.     -entityreferencecommand        {}                \
  205.     -warningcommand            [namespace current]::noop    \
  206.     -errorcommand            [namespace current]::Error    \
  207.     -final                1                \
  208.     -emptyelement            [namespace current]::EmptyElement    \
  209.     -parseattributelistcommand    [namespace current]::noop    \
  210.     -normalize            1                \
  211.     -internaldtd            {}                \
  212.     -reportempty            0                \
  213.     -entityvariable            [namespace current]::EntityPredef    \
  214.     ]
  215.     catch {array set options $args}
  216.  
  217.     if {![info exists options(-statevariable)]} {
  218.     set options(-statevariable) [namespace current]::ParseEvent[incr ParseEventNum]
  219.     }
  220.  
  221.     upvar #0 $options(-statevariable) state
  222.     upvar #0 $options(-entityvariable) entities
  223.  
  224.     if {![info exists state]} {
  225.     # Initialise the state variable
  226.     array set state {
  227.         mode normal
  228.         haveXMLDecl 0
  229.         haveDocElement 0
  230.         context {}
  231.         stack {}
  232.         line 0
  233.     }
  234.     }
  235.  
  236.     foreach {tag close empty param text} $sgml {
  237.  
  238.     # Keep track of lines in the input
  239.     incr state(line) [regsub -all \n $param {} discard]
  240.     incr state(line) [regsub -all \n $text {} discard]
  241.  
  242.     # If the current mode is cdata or comment then we must undo what the
  243.     # regsub has done to reconstitute the data
  244.  
  245.     switch $state(mode) {
  246.         comment {
  247.         # This had "[string length $param] && " as a guard -
  248.         # can't remember why :-(
  249.         if {[regexp ([cl ^-]*)--\$ $tag discard comm1]} {
  250.             # end of comment (in tag)
  251.             set tag {}
  252.             set close {}
  253.             set empty {}
  254.             set state(mode) normal
  255.             uplevel #0 $options(-commentcommand) [list $state(commentdata)<$comm1]
  256.             unset state(commentdata)
  257.         } elseif {[regexp ([cl ^-]*)--\$ $param discard comm1]} {
  258.             # end of comment (in attributes)
  259.             uplevel #0 $options(-commentcommand) [list $state(commentdata)<$close$tag$empty>$comm1]
  260.             unset state(commentdata)
  261.             set tag {}
  262.             set param {}
  263.             set close {}
  264.             set empty {}
  265.             set state(mode) normal
  266.         } elseif {[regexp ([cl ^-]*)-->(.*) $text discard comm1 text]} {
  267.             # end of comment (in text)
  268.             uplevel #0 $options(-commentcommand) [list $state(commentdata)<$close$tag$param$empty>$comm1]
  269.             unset state(commentdata)
  270.             set tag {}
  271.             set param {}
  272.             set close {}
  273.             set empty {}
  274.             set state(mode) normal
  275.         } else {
  276.             # comment continues
  277.             append state(commentdata) <$close$tag$param$empty>$text
  278.             continue
  279.         }
  280.         }
  281.         cdata {
  282.         if {[string length $param] && [regexp ([cl ^\]]*)\]\][cl $Wsp]*\$ $tag discard cdata1]} {
  283.             # end of CDATA (in tag)
  284.             uplevel #0 $options(-characterdatacommand) [list $state(cdata)<$close$cdata1$text]
  285.             set text {}
  286.             set tag {}
  287.             unset state(cdata)
  288.             set state(mode) normal
  289.         } elseif {[regexp ([cl ^\]]*)\]\][cl $Wsp]*\$ $param discard cdata1]} {
  290.             # end of CDATA (in attributes)
  291.             uplevel #0 $options(-characterdatacommand) [list $state(cdata)<$close$tag$cdata1$text]
  292.             set text {}
  293.             set tag {}
  294.             set param {}
  295.             unset state(cdata)
  296.             set state(mode) normal
  297.         } elseif {[regexp ([cl ^\]]*)\]\][cl $Wsp]*>(.*) $text discard cdata1 text]} {
  298.             # end of CDATA (in text)
  299.             uplevel #0 $options(-characterdatacommand) [list $state(cdata)<$close$tag$param$empty>$cdata1$text]
  300.             set text {}
  301.             set tag {}
  302.             set param {}
  303.             set close {}
  304.             set empty {}
  305.             unset state(cdata)
  306.             set state(mode) normal
  307.         } else {
  308.             # CDATA continues
  309.             append state(cdata) <$close$tag$param$empty>$text
  310.             continue
  311.         }
  312.         }
  313.     }
  314.  
  315.     # default: normal mode
  316.  
  317.     # Bug: if the attribute list has a right angle bracket then the empty
  318.     # element marker will not be seen
  319.  
  320.     set isEmpty [uplevel #0 $options(-emptyelement) [list $tag $param $empty]]
  321.     if {[llength $isEmpty]} {
  322.         foreach {empty tag param} $isEmpty break
  323.     }
  324.  
  325.     switch -glob -- [string length $tag],[regexp {^\?|!.*} $tag],$close,$empty {
  326.  
  327.         0,0,, {
  328.         # Ignore empty tag - dealt with non-normal mode above
  329.         }
  330.         *,0,, {
  331.  
  332.         # Start tag for an element.
  333.  
  334.         # Check for a right angle bracket in an attribute value
  335.         # This manifests itself by terminating the value before
  336.         # the delimiter is seen, and the delimiter appearing
  337.         # in the text
  338.  
  339.         # BUG: If two or more attribute values have right angle
  340.         # brackets then this will fail on the second one.
  341.  
  342.         if {[regexp [format {=[%s]*"[^"]*$} $Wsp] $param] && \
  343.             [regexp {([^"]*"[^>]*)>(.*)} $text discard attrListRemainder text]} {
  344.             append param >$attrListRemainder
  345.         } elseif {[regexp [format {=[%s]*'[^']*$} $Wsp] $param] && \
  346.             [regexp {([^']*'[^>]*)>(.*)} $text discard attrListRemainder text]} {
  347.             append param >$attrListRemainder
  348.         }
  349.  
  350.         # Check if the internal DTD entity is in an attribute
  351.         # value
  352.         regsub -all &xml:intdtd\; $param \[$options(-internaldtd)\] param
  353.  
  354.         ParseEvent:ElementOpen $tag $param options
  355.         set state(haveDocElement) 1
  356.  
  357.         }
  358.  
  359.         *,0,/, {
  360.  
  361.         # End tag for an element.
  362.  
  363.         ParseEvent:ElementClose $tag options
  364.  
  365.         }
  366.  
  367.         *,0,,/ {
  368.  
  369.         # Empty element
  370.  
  371.         ParseEvent:ElementOpen $tag $param options -empty 1
  372.         ParseEvent:ElementClose $tag options -empty 1
  373.  
  374.         }
  375.  
  376.         *,1,* {
  377.         # Processing instructions or XML declaration
  378.         switch -glob -- $tag {
  379.  
  380.             {\?xml} {
  381.             # XML Declaration
  382.             if {$state(haveXMLDecl)} {
  383.                 uplevel #0 $options(-errorcommand) "unexpected characters \"<$tag\" around line $state(line)"
  384.             } elseif {![regexp {\?$} $param]} {
  385.                 uplevel #0 $options(-errorcommand) "XML Declaration missing characters \"?>\" around line $state(line)"
  386.             } else {
  387.  
  388.                 # Get the version number
  389.                 if {[regexp {[     ]*version="(-+|[a-zA-Z0-9_.:]+)"[     ]*} $param discard version] || [regexp {[     ]*version='(-+|[a-zA-Z0-9_.:]+)'[     ]*} $param discard version]} {
  390.                 if {[string compare $version "1.0"]} {
  391.                     # Should we support future versions?
  392.                     # At least 1.X?
  393.                     uplevel #0 $options(-errorcommand) "document XML version \"$version\" is incompatible with XML version 1.0"
  394.                 }
  395.                 } else {
  396.                 uplevel #0 $options(-errorcommand) "XML Declaration missing version information around line $state(line)"
  397.                 }
  398.  
  399.                 # Get the encoding declaration
  400.                 set encoding {}
  401.                 regexp {[     ]*encoding="([A-Za-z]([A-Za-z0-9._]|-)*)"[     ]*} $param discard encoding
  402.                 regexp {[     ]*encoding='([A-Za-z]([A-Za-z0-9._]|-)*)'[     ]*} $param discard encoding
  403.  
  404.                 # Get the standalone declaration
  405.                 set standalone {}
  406.                 regexp {[     ]*standalone="(yes|no)"[     ]*} $param discard standalone
  407.                 regexp {[     ]*standalone='(yes|no)'[     ]*} $param discard standalone
  408.  
  409.                 # Invoke the callback
  410.                 uplevel #0 $options(-xmldeclcommand) [list $version $encoding $standalone]
  411.  
  412.             }
  413.  
  414.             }
  415.  
  416.             {\?*} {
  417.             # Processing instruction
  418.             if {![regsub {\?$} $param {} param]} {
  419.                 uplevel #0 $options(-errorcommand) "PI: expected '?' character around line $state(line)"
  420.             } else {
  421.                 uplevel #0 $options(-processinginstructioncommand) [list [string range $tag 1 end] [string trimleft $param]]
  422.             }
  423.             }
  424.  
  425.             !DOCTYPE {
  426.             # External entity reference
  427.             # This should move into xml.tcl
  428.             # Parse the params supplied.  Looking for Name, ExternalID and MarkupDecl
  429.             regexp ^[cl $Wsp]*($name)(.*) $param x state(doc_name) param
  430.             set state(doc_name) [Normalize $state(doc_name) $options(-normalize)]
  431.             set externalID {}
  432.             set pubidlit {}
  433.             set systemlit {}
  434.             set externalID {}
  435.             if {[regexp -nocase ^[cl $Wsp]*(SYSTEM|PUBLIC)(.*) $param x id param]} {
  436.                 switch [string toupper $id] {
  437.                 SYSTEM {
  438.                     if {[regexp ^[cl $Wsp]+"([cl ^"]*)"(.*) $param x systemlit param] || [regexp ^[cl $Wsp]+'([cl ^']*)'(.*) $param x systemlit param]} {
  439.                     set externalID [list SYSTEM $systemlit] ;# "
  440.                     } else {
  441.                     uplevel #0 $options(-errorcommand) {{syntax error: SYSTEM identifier not followed by literal}}
  442.                     }
  443.                 }
  444.                 PUBLIC {
  445.                     if {[regexp ^[cl $Wsp]+"([cl ^"]*)"(.*) $param x pubidlit param] || [regexp ^[cl $Wsp]+'([cl ^']*)'(.*) $param x pubidlit param]} {
  446.                     if {[regexp ^[cl $Wsp]+"([cl ^"]*)"(.*) $param x systemlit param] || [regexp ^[cl $Wsp]+'([cl ^']*)'(.*) $param x systemlit param]} {
  447.                         set externalID [list PUBLIC $pubidlit $systemlit]
  448.                     } else {
  449.                         uplevel #0 $options(-errorcommand) "syntax error: PUBLIC identifier not followed by system literal around line $state(line)"
  450.                     }
  451.                     } else {
  452.                     uplevel #0 $options(-errorcommand) "syntax error: PUBLIC identifier not followed by literal around line $state(line)"
  453.                     }
  454.                 }
  455.                 }
  456.                 if {[regexp -nocase ^[cl $Wsp]+NDATA[cl $Wsp]+($name)(.*) $param x notation param]} {
  457.                 lappend externalID $notation
  458.                 }
  459.             }
  460.  
  461.             uplevel #0 $options(-doctypecommand) $state(doc_name) [list $pubidlit $systemlit $options(-internaldtd)]
  462.  
  463.             }
  464.  
  465.             !--* {
  466.  
  467.             # Start of a comment
  468.             # See if it ends in the same tag, otherwise change the
  469.             # parsing mode
  470.  
  471.             regexp {!--(.*)} $tag discard comm1
  472.             if {[regexp ([cl ^-]*)--[cl $Wsp]*\$ $comm1 discard comm1_1]} {
  473.                 # processed comment (end in tag)
  474.                 uplevel #0 $options(-commentcommand) [list $comm1_1]
  475.             } elseif {[regexp ([cl ^-]*)--[cl $Wsp]*\$ $param discard comm2]} {
  476.                 # processed comment (end in attributes)
  477.                 uplevel #0 $options(-commentcommand) [list $comm1$comm2]
  478.             } elseif {[regexp ([cl ^-]*)-->(.*) $text discard comm2 text]} {
  479.                 # processed comment (end in text)
  480.                 uplevel #0 $options(-commentcommand) [list $comm1$param>$comm2]
  481.             } else {
  482.                 # start of comment
  483.                 set state(mode) comment
  484.                 set state(commentdata) "$comm1$param>$text"
  485.                 continue
  486.             }
  487.             }
  488.  
  489.             {!\[CDATA\[*} {
  490.  
  491.             regexp {!\[CDATA\[(.*)} $tag discard cdata1
  492.             if {[regexp {(.*)]]$} $param discard cdata2]} {
  493.                 # processed CDATA (end in attribute)
  494.                 uplevel #0 $options(-characterdatacommand) [list $cdata1$cdata2$text]
  495.                 set text {}
  496.             } elseif {[regexp {(.*)]]>(.*)} $text discard cdata2 text]} {
  497.                 # processed CDATA (end in text)
  498.                 uplevel #0 $options(-characterdatacommand) [list $cdata1$param$empty>$cdata2$text]
  499.                 set text {}
  500.             } else {
  501.                 # start CDATA
  502.                 set state(cdata) "$cdata1$param>$text"
  503.                 set state(mode) cdata
  504.                 continue
  505.             }
  506.  
  507.             }
  508.  
  509.             !ELEMENT {
  510.             # Internal DTD declaration
  511.             }
  512.             !ATTLIST {
  513.             }
  514.             !ENTITY {
  515.             }
  516.             !NOTATION {
  517.             }
  518.  
  519.  
  520.             !* {
  521.             uplevel #0 $options(-processinginstructioncommand) [list $tag $param]
  522.             }
  523.             default {
  524.             uplevel #0 $options(-errorcommand) [list "unknown processing instruction \"<$tag>\" around line $state(line)"]
  525.             }
  526.         }
  527.         }
  528.         *,1,* -
  529.         *,0,/,/ {
  530.         # Syntax error
  531.             uplevel #0 $options(-errorcommand) [list [list syntax error: closed/empty tag: tag $tag param $param empty $empty close $close around line $state(line)]]
  532.         }
  533.     }
  534.  
  535.     # Process character data
  536.  
  537.     if {$state(haveDocElement) && [llength $state(stack)]} {
  538.  
  539.         # Check if the internal DTD entity is in the text
  540.         regsub -all &xml:intdtd\; $text \[$options(-internaldtd)\] text
  541.  
  542.         # Look for entity references
  543.         if {([array size entities] || [string length $options(-entityreferencecommand)]) && \
  544.         [regexp {&[^;]+;} $text]} {
  545.  
  546.         # protect Tcl specials
  547.         regsub -all {([][$\\])} $text {\\\1} text
  548.         # Mark entity references
  549.         regsub -all {&([^;]+);} $text [format {%s; %s {\1} ; %s %s} \}\} [namespace code [list Entity options $options(-entityreferencecommand) $options(-characterdatacommand) $options(-entityvariable)]] [list uplevel #0 $options(-characterdatacommand)] \{\{] text
  550.         set text "uplevel #0 $options(-characterdatacommand) {{$text}}"
  551.         eval $text
  552.         } else {
  553.         # Restore protected special characters
  554.         regsub -all {\\([{}\\])} $text {\1} text
  555.         uplevel #0 $options(-characterdatacommand) [list $text]
  556.         }
  557.     } elseif {[string length [string trim $text]]} {
  558.         uplevel #0 $options(-errorcommand) "unexpected text \"$text\" in document prolog around line $state(line)"
  559.     }
  560.  
  561.     }
  562.  
  563.     # If this is the end of the document, close all open containers
  564.     if {$options(-final) && [llength $state(stack)]} {
  565.     eval $options(-errorcommand) [list [list element [lindex $state(stack) end] remains unclosed around line $state(line)]]
  566.     }
  567.  
  568.     return {}
  569. }
  570.  
  571. # sgml::ParseEvent:ElementOpen --
  572. #
  573. #    Start of an element.
  574. #
  575. # Arguments:
  576. #    tag    Element name
  577. #    attr    Attribute list
  578. #    opts    Option variable in caller
  579. #    args    further configuration options
  580. #
  581. # Options:
  582. #    -empty boolean
  583. #        indicates whether the element was an empty element
  584. #
  585. # Results:
  586. #    Modify state and invoke callback
  587.  
  588. proc sgml::ParseEvent:ElementOpen {tag attr opts args} {
  589.     upvar $opts options
  590.     upvar #0 $options(-statevariable) state
  591.     array set cfg {-empty 0}
  592.     array set cfg $args
  593.  
  594.     if {$options(-normalize)} {
  595.     set tag [string toupper $tag]
  596.     }
  597.  
  598.     # Update state
  599.     lappend state(stack) $tag
  600.  
  601.     # Parse attribute list into a key-value representation
  602.     if {[string compare $options(-parseattributelistcommand) {}]} {
  603.     if {[catch {uplevel #0 $options(-parseattributelistcommand) [list $attr]} attr]} {
  604.         uplevel #0 $options(-errorcommand) [list $attr around line $state(line)]
  605.         set attr {}
  606.     }
  607.     }
  608.  
  609.     set empty {}
  610.     if {$cfg(-empty) && $options(-reportempty)} {
  611.     set empty {-empty 1}
  612.     }
  613.  
  614.     # Invoke callback
  615.     uplevel #0 $options(-elementstartcommand) [list $tag $attr] $empty
  616.  
  617.     return {}
  618. }
  619.  
  620. # sgml::ParseEvent:ElementClose --
  621. #
  622. #    End of an element.
  623. #
  624. # Arguments:
  625. #    tag    Element name
  626. #    opts    Option variable in caller
  627. #    args    further configuration options
  628. #
  629. # Options:
  630. #    -empty boolean
  631. #        indicates whether the element as an empty element
  632. #
  633. # Results:
  634. #    Modify state and invoke callback
  635.  
  636. proc sgml::ParseEvent:ElementClose {tag opts args} {
  637.     upvar $opts options
  638.     upvar #0 $options(-statevariable) state
  639.     array set cfg {-empty 0}
  640.     array set cfg $args
  641.  
  642.     # WF check
  643.     if {[string compare $tag [lindex $state(stack) end]]} {
  644.     uplevel #0 $options(-errorcommand) [list "end tag \"$tag\" does not match open element \"[lindex $state(stack) end]\" around line $state(line)"]
  645.     return
  646.     }
  647.  
  648.     # Update state
  649.     set state(stack) [lreplace $state(stack) end end]
  650.  
  651.     set empty {}
  652.     if {$cfg(-empty) && $options(-reportempty)} {
  653.     set empty {-empty 1}
  654.     }
  655.  
  656.     # Invoke callback
  657.     uplevel #0 $options(-elementendcommand) [list $tag] $empty
  658.  
  659.     return {}
  660. }
  661.  
  662. # sgml::Normalize --
  663. #
  664. #    Perform name normalization if required
  665. #
  666. # Arguments:
  667. #    name    name to normalize
  668. #    req    normalization required
  669. #
  670. # Results:
  671. #    Name returned as upper-case if normalization required
  672.  
  673. proc sgml::Normalize {name req} {
  674.     if {$req} {
  675.     return [string toupper $name]
  676.     } else {
  677.     return $name
  678.     }
  679. }
  680.  
  681. # sgml::Entity --
  682. #
  683. #    Resolve XML entity references (syntax: &xxx;).
  684. #
  685. # Arguments:
  686. #    opts        options array variable in caller
  687. #    entityrefcmd    application callback for entity references
  688. #    pcdatacmd    application callback for character data
  689. #    entities    name of array containing entity definitions.
  690. #    ref        entity reference (the "xxx" bit)
  691. #
  692. # Results:
  693. #    Returns substitution text for given entity.
  694.  
  695. proc sgml::Entity {opts entityrefcmd pcdatacmd entities ref} {
  696.     upvar 2 $opts options
  697.     upvar #0 $options(-statevariable) state
  698.  
  699.     if {![string length $entities]} {
  700.     set entities [namespace current EntityPredef]
  701.     }
  702.  
  703.     switch -glob -- $ref {
  704.     %* {
  705.         # Parameter entity - not recognised outside of a DTD
  706.     }
  707.     #x* {
  708.         # Character entity - hex
  709.         if {[catch {format %c [scan [string range $ref 2 end] %x tmp; set tmp]} char]} {
  710.         return -code error "malformed character entity \"$ref\""
  711.         }
  712.         uplevel #0 $pcdatacmd [list $char]
  713.  
  714.         return {}
  715.  
  716.     }
  717.     #* {
  718.         # Character entity - decimal
  719.         if {[catch {format %c [scan [string range $ref 1 end] %d tmp; set tmp]} char]} {
  720.         return -code error "malformed character entity \"$ref\""
  721.         }
  722.         uplevel #0 $pcdatacmd [list $char]
  723.  
  724.         return {}
  725.  
  726.     }
  727.     default {
  728.         # General entity
  729.         upvar #0 $entities map
  730.         if {[info exists map($ref)]} {
  731.  
  732.         if {![regexp {<|&} $map($ref)]} {
  733.  
  734.             # Simple text replacement - optimise
  735.  
  736.             uplevel #0 $pcdatacmd [list $map($ref)]
  737.  
  738.             return {}
  739.  
  740.         }
  741.  
  742.         # Otherwise an additional round of parsing is required.
  743.         # This only applies to XML, since HTML doesn't have general entities
  744.  
  745.         # Must parse the replacement text for start & end tags, etc
  746.         # This text must be self-contained: balanced closing tags, and so on
  747.  
  748.         set tokenised [tokenise $map($ref) $::xml::tokExpr $::xml::substExpr]
  749.         set final $options(-final)
  750.         unset options(-final)
  751.         eval parseEvent [list $tokenised] [array get options] -final 0
  752.         set options(-final) $final
  753.  
  754.         return {}
  755.  
  756.         } elseif {[string length $entityrefcmd]} {
  757.  
  758.         uplevel #0 $entityrefcmd [list $ref]
  759.  
  760.         return {}
  761.  
  762.         }
  763.     }
  764.     }
  765.  
  766.     # If all else fails leave the entity reference untouched
  767.     uplevel #0 $pcdatacmd [list &$ref\;]
  768.  
  769.     return {}
  770. }
  771.  
  772. ####################################
  773. #
  774. # DTD parser for SGML (XML).
  775. #
  776. # This DTD actually only handles XML DTDs.  Other language's
  777. # DTD's, such as HTML, must be written in terms of a XML DTD.
  778. #
  779. # A DTD is represented as a three element Tcl list.
  780. # The first element contains the content models for elements,
  781. # the second contains the attribute lists for elements and
  782. # the last element contains the entities for the document.
  783. #
  784. ####################################
  785.  
  786. # sgml::parseDTD --
  787. #
  788. #    Entry point to the SGML DTD parser.
  789. #
  790. # Arguments:
  791. #    dtd    data defining the DTD to be parsed
  792. #    args    configuration options
  793. #
  794. # Results:
  795. #    Returns a three element list, first element is the content model
  796. #    for each element, second element are the attribute lists of the
  797. #    elements and the third element is the entity map.
  798.  
  799. proc sgml::parseDTD {dtd args} {
  800.     variable Wsp
  801.     variable ParseDTDnum
  802.  
  803.     array set opts [list \
  804.     -errorcommand        [namespace current]::noop \
  805.     state            [namespace current]::parseDTD[incr ParseDTDnum]
  806.     ]
  807.     array set opts $args
  808.  
  809.     set exp <!([cl ^$Wsp>]+)[cl $Wsp]*([cl ^$Wsp]+)[cl $Wsp]*([cl ^>]*)>
  810.     set sub {{\1} {\2} {\3} }
  811.     regsub -all $exp $dtd $sub dtd
  812.  
  813.     foreach {decl id value} $dtd {
  814.     catch {DTD:[string toupper $decl] $id $value} err
  815.     }
  816.  
  817.     return [list [array get contentmodel] [array get attributes] [array get entities]]
  818. }
  819.  
  820. # Procedures for handling the various declarative elements in a DTD.
  821. # New elements may be added by creating a procedure of the form
  822. # parse:DTD:_element_
  823.  
  824. # For each of these procedures, the various regular expressions they use
  825. # are created outside of the proc to avoid overhead at runtime
  826.  
  827. # sgml::DTD:ELEMENT --
  828. #
  829. #    <!ELEMENT ...> defines an element.
  830. #
  831. #    The content model for the element is stored in the contentmodel array,
  832. #    indexed by the element name.  The content model is parsed into the
  833. #    following list form:
  834. #
  835. #        {}    Content model is EMPTY.
  836. #            Indicated by an empty list.
  837. #        *    Content model is ANY.
  838. #            Indicated by an asterix.
  839. #        {ELEMENT ...}
  840. #            Content model is element-only.
  841. #        {MIXED {element1 element2 ...}}
  842. #            Content model is mixed (PCDATA and elements).
  843. #            The second element of the list contains the 
  844. #            elements that may occur.  #PCDATA is assumed 
  845. #            (ie. the list is normalised).
  846. #
  847. # Arguments:
  848. #    id    identifier for the element.
  849. #    value    other information in the PI
  850.  
  851. proc sgml::DTD:ELEMENT {id value} {
  852.     dbgputs DTD_parse [list DTD:ELEMENT $id $value]
  853.     variable Wsp
  854.     upvar opts state
  855.     upvar contentmodel cm
  856.  
  857.     if {[info exists cm($id)]} {
  858.     eval $state(-errorcommand) element [list "element \"$id\" already declared"]
  859.     } else {
  860.     switch -- $value {
  861.         EMPTY {
  862.             set cm($id) {}
  863.         }
  864.         ANY {
  865.             set cm($id) *
  866.         }
  867.         default {
  868.         if {[regexp [format {^\([%s]*#PCDATA[%s]*(\|([^)]+))?[%s]*\)*[%s]*$} $Wsp $Wsp $Wsp $Wsp] discard discard mtoks]} {
  869.             set cm($id) [list MIXED [split $mtoks |]]
  870.         } else {
  871.             if {[catch {CModelParse $state(state) $value} result]} {
  872.             eval $state(-errorcommand) element [list $result]
  873.             } else {
  874.             set cm($id) [list ELEMENT $result]
  875.             }
  876.         }
  877.         }
  878.     }
  879.     }
  880. }
  881.  
  882. # sgml::CModelParse --
  883. #
  884. #    Parse an element content model (non-mixed).
  885. #    A syntax tree is constructed.
  886. #    A transition table is built next.
  887. #
  888. #    This is going to need alot of work!
  889. #
  890. # Arguments:
  891. #    state    state array variable
  892. #    value    the content model data
  893. #
  894. # Results:
  895. #    A Tcl list representing the content model.
  896.  
  897. proc sgml::CModelParse {state value} {
  898.     upvar #0 $state var
  899.  
  900.     # First build syntax tree
  901.     set syntaxTree [CModelMakeSyntaxTree $state $value]
  902.  
  903.     # Build transition table
  904.     set transitionTable [CModelMakeTransitionTable $state $syntaxTree]
  905.  
  906.     return [list $syntaxTree $transitionTable]
  907. }
  908.  
  909. # sgml::CModelMakeSyntaxTree --
  910. #
  911. #    Construct a syntax tree for the regular expression.
  912. #
  913. #    Syntax tree is represented as a Tcl list:
  914. #    rep {:choice|:seq {{rep list1} {rep list2} ...}}
  915. #    where:    rep is repetition character, *, + or ?. {} for no repetition
  916. #        listN is nested expression or Name
  917. #
  918. # Arguments:
  919. #    spec    Element specification
  920. #
  921. # Results:
  922. #    Syntax tree for element spec as nested Tcl list.
  923. #
  924. #    Examples:
  925. #    (memo)
  926. #        {} {:seq {{} memo}}
  927. #    (front, body, back?)
  928. #        {} {:seq {{} front} {{} body} {? back}}
  929. #    (head, (p | list | note)*, div2*)
  930. #        {} {:seq {{} head} {* {:choice {{} p} {{} list} {{} note}}} {* div2}}
  931. #    (p | a | ul)+
  932. #        + {:choice {{} p} {{} a} {{} ul}}
  933.  
  934. proc sgml::CModelMakeSyntaxTree {state spec} {
  935.     upvar #0 $state var
  936.     variable Wsp
  937.     variable name
  938.  
  939.     # Translate the spec into a Tcl list.
  940.  
  941.     # None of the Tcl special characters are allowed in a content model spec.
  942.     if {[regexp {\$|\[|\]|\{|\}} $spec]} {
  943.     return -code error "illegal characters in specification"
  944.     }
  945.  
  946.     regsub -all [format {(%s)[%s]*(\?|\*|\+)?[%s]*(,|\|)?} $name $Wsp $Wsp] $spec [format {%sCModelSTname %s {\1} {\2} {\3}} \n $state] spec
  947.     regsub -all {\(} $spec "\nCModelSTopenParen $state " spec
  948.     regsub -all [format {\)[%s]*(\?|\*|\+)?[%s]*(,|\|)?} $Wsp $Wsp] $spec [format {%sCModelSTcloseParen %s {\1} {\2}} \n $state] spec
  949.  
  950.     array set var {stack {} state start}
  951.     eval $spec
  952.  
  953.     # Peel off the outer seq, its redundant
  954.     return [lindex [lindex $var(stack) 1] 0]
  955. }
  956.  
  957. # sgml::CModelSTname --
  958. #
  959. #    Processes a name in a content model spec.
  960. #
  961. # Arguments:
  962. #    state    state array variable
  963. #    name    name specified
  964. #    rep    repetition operator
  965. #    cs    choice or sequence delimiter
  966. #
  967. # Results:
  968. #    See CModelSTcp.
  969.  
  970. proc sgml::CModelSTname {state name rep cs args} {
  971.     if {[llength $args]} {
  972.     return -code error "syntax error in specification: \"$args\""
  973.     }
  974.  
  975.     CModelSTcp $state $name $rep $cs
  976. }
  977.  
  978. # sgml::CModelSTcp --
  979. #
  980. #    Process a content particle.
  981. #
  982. # Arguments:
  983. #    state    state array variable
  984. #    name    name specified
  985. #    rep    repetition operator
  986. #    cs    choice or sequence delimiter
  987. #
  988. # Results:
  989. #    The content particle is added to the current group.
  990.  
  991. proc sgml::CModelSTcp {state cp rep cs} {
  992.     upvar #0 $state var
  993.  
  994.     switch -glob -- [lindex $var(state) end]=$cs {
  995.     start= {
  996.         set var(state) [lreplace $var(state) end end end]
  997.         # Add (dummy) grouping, either choice or sequence will do
  998.         CModelSTcsSet $state ,
  999.         CModelSTcpAdd $state $cp $rep
  1000.     }
  1001.     :choice= -
  1002.     :seq= {
  1003.         set var(state) [lreplace $var(state) end end end]
  1004.         CModelSTcpAdd $state $cp $rep
  1005.     }
  1006.     start=| -
  1007.     start=, {
  1008.         set var(state) [lreplace $var(state) end end [expr {$cs == "," ? ":seq" : ":choice"}]]
  1009.         CModelSTcsSet $state $cs
  1010.         CModelSTcpAdd $state $cp $rep
  1011.     }
  1012.     :choice=| -
  1013.     :seq=, {
  1014.         CModelSTcpAdd $state $cp $rep
  1015.     }
  1016.     :choice=, -
  1017.     :seq=| {
  1018.         return -code error "syntax error in specification: incorrect delimiter after \"$cp\", should be \"[expr {$cs == "," ? "|" : ","}]\""
  1019.     }
  1020.     end=* {
  1021.         return -code error "syntax error in specification: no delimiter before \"$cp\""
  1022.     }
  1023.     default {
  1024.         return -code error "syntax error"
  1025.     }
  1026.     }
  1027.     
  1028. }
  1029.  
  1030. # sgml::CModelSTcsSet --
  1031. #
  1032. #    Start a choice or sequence on the stack.
  1033. #
  1034. # Arguments:
  1035. #    state    state array
  1036. #    cs    choice oir sequence
  1037. #
  1038. # Results:
  1039. #    state is modified: end element of state is appended.
  1040.  
  1041. proc sgml::CModelSTcsSet {state cs} {
  1042.     upvar #0 $state var
  1043.  
  1044.     set cs [expr {$cs == "," ? ":seq" : ":choice"}]
  1045.  
  1046.     if {[llength $var(stack)]} {
  1047.     set var(stack) [lreplace $var(stack) end end $cs]
  1048.     } else {
  1049.     set var(stack) [list $cs {}]
  1050.     }
  1051. }
  1052.  
  1053. # sgml::CModelSTcpAdd --
  1054. #
  1055. #    Append a content particle to the top of the stack.
  1056. #
  1057. # Arguments:
  1058. #    state    state array
  1059. #    cp    content particle
  1060. #    rep    repetition
  1061. #
  1062. # Results:
  1063. #    state is modified: end element of state is appended.
  1064.  
  1065. proc sgml::CModelSTcpAdd {state cp rep} {
  1066.     upvar #0 $state var
  1067.  
  1068.     if {[llength $var(stack)]} {
  1069.     set top [lindex $var(stack) end]
  1070.         lappend top [list $rep $cp]
  1071.     set var(stack) [lreplace $var(stack) end end $top]
  1072.     } else {
  1073.     set var(stack) [list $rep $cp]
  1074.     }
  1075. }
  1076.  
  1077. # sgml::CModelSTopenParen --
  1078. #
  1079. #    Processes a '(' in a content model spec.
  1080. #
  1081. # Arguments:
  1082. #    state    state array
  1083. #
  1084. # Results:
  1085. #    Pushes stack in state array.
  1086.  
  1087. proc sgml::CModelSTopenParen {state args} {
  1088.     upvar #0 $state var
  1089.  
  1090.     if {[llength $args]} {
  1091.     return -code error "syntax error in specification: \"$args\""
  1092.     }
  1093.  
  1094.     lappend var(state) start
  1095.     lappend var(stack) [list {} {}]
  1096. }
  1097.  
  1098. # sgml::CModelSTcloseParen --
  1099. #
  1100. #    Processes a ')' in a content model spec.
  1101. #
  1102. # Arguments:
  1103. #    state    state array
  1104. #    rep    repetition
  1105. #    cs    choice or sequence delimiter
  1106. #
  1107. # Results:
  1108. #    Stack is popped, and former top of stack is appended to previous element.
  1109.  
  1110. proc sgml::CModelSTcloseParen {state rep cs args} {
  1111.     upvar #0 $state var
  1112.  
  1113.     if {[llength $args]} {
  1114.     return -code error "syntax error in specification: \"$args\""
  1115.     }
  1116.  
  1117.     set cp [lindex $var(stack) end]
  1118.     set var(stack) [lreplace $var(stack) end end]
  1119.     set var(state) [lreplace $var(state) end end]
  1120.     CModelSTcp $state $cp $rep $cs
  1121. }
  1122.  
  1123. # sgml::CModelMakeTransitionTable --
  1124. #
  1125. #    Given a content model's syntax tree, constructs
  1126. #    the transition table for the regular expression.
  1127. #
  1128. #    See "Compilers, Principles, Techniques, and Tools",
  1129. #    Aho, Sethi and Ullman.  Section 3.9, algorithm 3.5.
  1130. #
  1131. # Arguments:
  1132. #    state    state array variable
  1133. #    st    syntax tree
  1134. #
  1135. # Results:
  1136. #    The transition table is returned, as a key/value Tcl list.
  1137.  
  1138. proc sgml::CModelMakeTransitionTable {state st} {
  1139.     upvar #0 $state var
  1140.  
  1141.     # Construct nullable, firstpos and lastpos functions
  1142.     array set var {number 0}
  1143.     foreach {nullable firstpos lastpos} [    \
  1144.     TraverseDepth1st $state $st {
  1145.         # Evaluated for leaf nodes
  1146.         # Compute nullable(n)
  1147.         # Compute firstpos(n)
  1148.         # Compute lastpos(n)
  1149.         set nullable [nullable leaf $rep $name]
  1150.         set firstpos [list {} $var(number)]
  1151.         set lastpos [list {} $var(number)]
  1152.         set var(pos:$var(number)) $name
  1153.     } {
  1154.         # Evaluated for nonterminal nodes
  1155.         # Compute nullable, firstpos, lastpos
  1156.         set firstpos [firstpos $cs $firstpos $nullable]
  1157.         set lastpos  [lastpos  $cs $lastpos  $nullable]
  1158.         set nullable [nullable nonterm $rep $cs $nullable]
  1159.     }    \
  1160.     ] break
  1161.  
  1162.     set accepting [incr var(number)]
  1163.     set var(pos:$accepting) #
  1164.  
  1165.     # var(pos:N) maps from position to symbol.
  1166.     # Construct reverse map for convenience.
  1167.     # NB. A symbol may appear in more than one position.
  1168.     # var is about to be reset, so use different arrays.
  1169.  
  1170.     foreach {pos symbol} [array get var pos:*] {
  1171.     set pos [lindex [split $pos :] 1]
  1172.     set pos2symbol($pos) $symbol
  1173.     lappend sym2pos($symbol) $pos
  1174.     }
  1175.  
  1176.     # Construct the followpos functions
  1177.     catch {unset var}
  1178.     followpos $state $st $firstpos $lastpos
  1179.  
  1180.     # Construct transition table
  1181.     # Dstates is [union $marked $unmarked]
  1182.     set unmarked [list [lindex $firstpos 1]]
  1183.     while {[llength $unmarked]} {
  1184.     set T [lindex $unmarked 0]
  1185.     lappend marked $T
  1186.     set unmarked [lrange $unmarked 1 end]
  1187.  
  1188.     # Find which input symbols occur in T
  1189.     set symbols {}
  1190.     foreach pos $T {
  1191.         if {$pos != $accepting && [lsearch $symbols $pos2symbol($pos)] < 0} {
  1192.         lappend symbols $pos2symbol($pos)
  1193.         }
  1194.     }
  1195.     foreach a $symbols {
  1196.         set U {}
  1197.         foreach pos $sym2pos($a) {
  1198.         if {[lsearch $T $pos] >= 0} {
  1199.             # add followpos($pos)
  1200.                 if {$var($pos) == {}} {
  1201.                     lappend U $accepting
  1202.                 } else {
  1203.                     eval lappend U $var($pos)
  1204.                 }
  1205.         }
  1206.         }
  1207.         set U [makeSet $U]
  1208.         if {[llength $U] && [lsearch $marked $U] < 0 && [lsearch $unmarked $U] < 0} {
  1209.         lappend unmarked $U
  1210.         }
  1211.         set Dtran($T,$a) $U
  1212.     }
  1213.     
  1214.     }
  1215.  
  1216.     return [list [array get Dtran] [array get sym2pos] $accepting]
  1217. }
  1218.  
  1219. # sgml::followpos --
  1220. #
  1221. #    Compute the followpos function, using the already computed
  1222. #    firstpos and lastpos.
  1223. #
  1224. # Arguments:
  1225. #    state        array variable to store followpos functions
  1226. #    st        syntax tree
  1227. #    firstpos    firstpos functions for the syntax tree
  1228. #    lastpos        lastpos functions
  1229. #
  1230. # Results:
  1231. #    followpos functions for each leaf node, in name/value format
  1232.  
  1233. proc sgml::followpos {state st firstpos lastpos} {
  1234.     upvar #0 $state var
  1235.  
  1236.     switch -- [lindex [lindex $st 1] 0] {
  1237.     :seq {
  1238.         for {set i 1} {$i < [llength [lindex $st 1]]} {incr i} {
  1239.             followpos $state [lindex [lindex $st 1] $i]            \
  1240.             [lindex [lindex $firstpos 0] [expr $i - 1]]    \
  1241.             [lindex [lindex $lastpos 0] [expr $i - 1]]
  1242.             foreach pos [lindex [lindex [lindex $lastpos 0] [expr $i - 1]] 1] {
  1243.             eval lappend var($pos) [lindex [lindex [lindex $firstpos 0] $i] 1]
  1244.             set var($pos) [makeSet $var($pos)]
  1245.             }
  1246.         }
  1247.     }
  1248.     :choice {
  1249.         for {set i 1} {$i < [llength [lindex $st 1]]} {incr i} {
  1250.         followpos $state [lindex [lindex $st 1] $i]            \
  1251.             [lindex [lindex $firstpos 0] [expr $i - 1]]    \
  1252.             [lindex [lindex $lastpos 0] [expr $i - 1]]
  1253.         }
  1254.     }
  1255.     default {
  1256.         # No action at leaf nodes
  1257.     }
  1258.     }
  1259.  
  1260.     switch -- [lindex $st 0] {
  1261.     ? {
  1262.         # We having nothing to do here ! Doing the same as
  1263.         # for * effectively converts this qualifier into the other.
  1264.     }
  1265.     * {
  1266.         foreach pos [lindex $lastpos 1] {
  1267.         eval lappend var($pos) [lindex $firstpos 1]
  1268.         set var($pos) [makeSet $var($pos)]
  1269.         }
  1270.     }
  1271.     }
  1272.  
  1273. }
  1274.  
  1275. # sgml::TraverseDepth1st --
  1276. #
  1277. #    Perform depth-first traversal of a tree.
  1278. #    A new tree is constructed, with each node computed by f.
  1279. #
  1280. # Arguments:
  1281. #    state    state array variable
  1282. #    t    The tree to traverse, a Tcl list
  1283. #    leaf    Evaluated at a leaf node
  1284. #    nonTerm    Evaluated at a nonterminal node
  1285. #
  1286. # Results:
  1287. #    A new tree is returned.
  1288.  
  1289. proc sgml::TraverseDepth1st {state t leaf nonTerm} {
  1290.     upvar #0 $state var
  1291.  
  1292.     set nullable {}
  1293.     set firstpos {}
  1294.     set lastpos {}
  1295.  
  1296.     switch -- [lindex [lindex $t 1] 0] {
  1297.     :seq -
  1298.     :choice {
  1299.         set rep [lindex $t 0]
  1300.         set cs [lindex [lindex $t 1] 0]
  1301.  
  1302.         foreach child [lrange [lindex $t 1] 1 end] {
  1303.         foreach {childNullable childFirstpos childLastpos} \
  1304.             [TraverseDepth1st $state $child $leaf $nonTerm] break
  1305.         lappend nullable $childNullable
  1306.         lappend firstpos $childFirstpos
  1307.         lappend lastpos  $childLastpos
  1308.         }
  1309.  
  1310.         eval $nonTerm
  1311.     }
  1312.     default {
  1313.         incr var(number)
  1314.         set rep [lindex [lindex $t 0] 0]
  1315.         set name [lindex [lindex $t 1] 0]
  1316.         eval $leaf
  1317.     }
  1318.     }
  1319.  
  1320.     return [list $nullable $firstpos $lastpos]
  1321. }
  1322.  
  1323. # sgml::firstpos --
  1324. #
  1325. #    Computes the firstpos function for a nonterminal node.
  1326. #
  1327. # Arguments:
  1328. #    cs        node type, choice or sequence
  1329. #    firstpos    firstpos functions for the subtree
  1330. #    nullable    nullable functions for the subtree
  1331. #
  1332. # Results:
  1333. #    firstpos function for this node is returned.
  1334.  
  1335. proc sgml::firstpos {cs firstpos nullable} {
  1336.     switch -- $cs {
  1337.     :seq {
  1338.         set result [lindex [lindex $firstpos 0] 1]
  1339.         for {set i 0} {$i < [llength $nullable]} {incr i} {
  1340.             if {[lindex [lindex $nullable $i] 1]} {
  1341.                 eval lappend result [lindex [lindex $firstpos [expr $i + 1]] 1]
  1342.         } else {
  1343.             break
  1344.         }
  1345.         }
  1346.     }
  1347.     :choice {
  1348.         foreach child $firstpos {
  1349.         eval lappend result $child
  1350.         }
  1351.     }
  1352.     }
  1353.  
  1354.     return [list $firstpos [makeSet $result]]
  1355. }
  1356.  
  1357. # sgml::lastpos --
  1358. #
  1359. #    Computes the lastpos function for a nonterminal node.
  1360. #    Same as firstpos, only logic is reversed
  1361. #
  1362. # Arguments:
  1363. #    cs        node type, choice or sequence
  1364. #    lastpos        lastpos functions for the subtree
  1365. #    nullable    nullable functions forthe subtree
  1366. #
  1367. # Results:
  1368. #    lastpos function for this node is returned.
  1369.  
  1370. proc sgml::lastpos {cs lastpos nullable} {
  1371.     switch -- $cs {
  1372.     :seq {
  1373.         set result [lindex [lindex $lastpos end] 1]
  1374.         for {set i [expr [llength $nullable] - 1]} {$i >= 0} {incr i -1} {
  1375.         if {[lindex [lindex $nullable $i] 1]} {
  1376.             eval lappend result [lindex [lindex $lastpos $i] 1]
  1377.         } else {
  1378.             break
  1379.         }
  1380.         }
  1381.     }
  1382.     :choice {
  1383.         foreach child $lastpos {
  1384.         eval lappend result $child
  1385.         }
  1386.     }
  1387.     }
  1388.  
  1389.     return [list $lastpos [makeSet $result]]
  1390. }
  1391.  
  1392. # sgml::makeSet --
  1393. #
  1394. #    Turn a list into a set, ie. remove duplicates.
  1395. #
  1396. # Arguments:
  1397. #    s    a list
  1398. #
  1399. # Results:
  1400. #    A set is returned, which is a list with duplicates removed.
  1401.  
  1402. proc sgml::makeSet s {
  1403.     foreach r $s {
  1404.     if {[llength $r]} {
  1405.         set unique($r) {}
  1406.     }
  1407.     }
  1408.     return [array names unique]
  1409. }
  1410.  
  1411. # sgml::nullable --
  1412. #
  1413. #    Compute the nullable function for a node.
  1414. #
  1415. # Arguments:
  1416. #    nodeType    leaf or nonterminal
  1417. #    rep        repetition applying to this node
  1418. #    name        leaf node: symbol for this node, nonterm node: choice or seq node
  1419. #    subtree        nonterm node: nullable functions for the subtree
  1420. #
  1421. # Results:
  1422. #    Returns nullable function for this branch of the tree.
  1423.  
  1424. proc sgml::nullable {nodeType rep name {subtree {}}} {
  1425.     switch -glob -- $rep:$nodeType {
  1426.     :leaf -
  1427.     +:leaf {
  1428.         return [list {} 0]
  1429.     }
  1430.     \\*:leaf -
  1431.     \\?:leaf {
  1432.         return [list {} 1]
  1433.     }
  1434.     \\*:nonterm -
  1435.     \\?:nonterm {
  1436.         return [list $subtree 1]
  1437.     }
  1438.     :nonterm -
  1439.     +:nonterm {
  1440.         switch -- $name {
  1441.         :choice {
  1442.             set result 0
  1443.             foreach child $subtree {
  1444.             set result [expr $result || [lindex $child 1]]
  1445.             }
  1446.         }
  1447.         :seq {
  1448.             set result 1
  1449.             foreach child $subtree {
  1450.             set result [expr $result && [lindex $child 1]]
  1451.             }
  1452.         }
  1453.         }
  1454.         return [list $subtree $result]
  1455.     }
  1456.     }
  1457. }
  1458.  
  1459. # These regular expressions are defined here once for better performance
  1460.  
  1461. namespace eval sgml {
  1462.     variable Wsp
  1463.  
  1464.     # Watch out for case-sensitivity
  1465.  
  1466.     set attlist_exp [cl $Wsp]*([cl ^$Wsp]+)[cl $Wsp]*([cl ^$Wsp]+)[cl $Wsp]*(#REQUIRED|#IMPLIED)
  1467.     set attlist_enum_exp [cl $Wsp]*([cl ^$Wsp]+)[cl $Wsp]*\\(([cl ^)]*)\\)[cl $Wsp]*("([cl ^")])")? ;# "
  1468.     set attlist_fixed_exp [cl $Wsp]*([cl ^$Wsp]+)[cl $Wsp]*([cl ^$Wsp]+)[cl $Wsp]*(#FIXED)[cl $Wsp]*([cl ^$Wsp]+)
  1469.  
  1470.     set param_entity_exp [cl $Wsp]*([cl ^$Wsp]+)[cl $Wsp]*([cl ^"$Wsp]*)[cl $Wsp]*"([cl ^"]*)"
  1471.  
  1472.     set notation_exp [cl $Wsp]*([cl ^$Wsp]+)[cl $Wsp]*(.*)
  1473.  
  1474. }
  1475.  
  1476. # sgml::DTD:ATTLIST --
  1477. #
  1478. #    <!ATTLIST ...> defines an attribute list.
  1479. #
  1480. # Arguments:
  1481. #    id    Element an attribute list is being defined for.
  1482. #    value    data from the PI.
  1483. #
  1484. # Results:
  1485. #    Attribute list variables are modified.
  1486.  
  1487. proc sgml::DTD:ATTLIST {id value} {
  1488.     variable attlist_exp
  1489.     variable attlist_enum_exp
  1490.     variable attlist_fixed_exp
  1491.     dbgputs DTD_parse [list DTD:ATTLIST $id $value]
  1492.     upvar opts state
  1493.     upvar attributes am
  1494.  
  1495.     if {[info exists am($id)]} {
  1496.     eval $state(-errorcommand) attlist [list "attribute list for element \"$id\" already declared"]
  1497.     } else {
  1498.     # Parse the attribute list.  If it were regular, could just use foreach,
  1499.     # but some attributes may have values.
  1500.     regsub -all {([][$\\])} $value {\\\1} value
  1501.     regsub -all $attlist_exp $value {[DTDAttribute {\1} {\2} {\3}]} value
  1502.     regsub -all $attlist_enum_exp $value {[DTDAttribute {\1} {\2} {\3}]} value
  1503.     regsub -all $attlist_fixed_exp $value {[DTDAttribute {\1} {\2} {\3} {\4}]} value
  1504.     subst $value
  1505.     set am($id) [array get attlist]
  1506.     }
  1507. }
  1508.  
  1509. # sgml::DTDAttribute --
  1510. #
  1511. #    Parse definition of a single attribute.
  1512. #
  1513. # Arguments:
  1514. #    name    attribute name
  1515. #    type    type of this attribute
  1516. #    default    default value of the attribute
  1517. #    value    other information
  1518.  
  1519. proc sgml::DTDAttribute {name type default {value {}}} {
  1520.     upvar attlist al
  1521.     # This needs further work
  1522.     set al($name) [list $default $value]
  1523. }
  1524.  
  1525. # sgml::DTD:ENTITY --
  1526. #
  1527. #    <!ENTITY ...> PI
  1528. #
  1529. # Arguments:
  1530. #    id    identifier for the entity
  1531. #    value    data
  1532. #
  1533. # Results:
  1534. #    Modifies the caller's entities array variable
  1535.  
  1536. proc sgml::DTD:ENTITY {id value} {
  1537.     variable param_entity_exp
  1538.     dbgputs DTD_parse [list DTD:ENTITY $id $value]
  1539.     upvar opts state
  1540.     upvar entities ents
  1541.  
  1542.     if {[string compare % $id]} {
  1543.     # Entity declaration
  1544.     if {[info exists ents($id)]} {
  1545.         eval $state(-errorcommand) entity [list "entity \"$id\" already declared"]
  1546.     } else {
  1547.         if {![regexp {"([^"]*)"} $value x entvalue] && ![regexp {'([^']*)'} $value x entvalue]} {
  1548.         eval $state(-errorcommand) entityvalue [list "entity value \"$value\" not correctly specified"]
  1549.         } ;# "
  1550.         set ents($id) $entvalue
  1551.     }
  1552.     } else {
  1553.     # Parameter entity declaration
  1554.     switch -glob [regexp $param_entity_exp $value x name scheme data],[string compare {} $scheme] {
  1555.         0,* {
  1556.         eval $state(-errorcommand) entityvalue [list "parameter entity \"$value\" not correctly specified"]
  1557.         }
  1558.         *,0 {
  1559.             # SYSTEM or PUBLIC declaration
  1560.         }
  1561.         default {
  1562.             set ents($id) $data
  1563.         }
  1564.     }
  1565.     }
  1566. }
  1567.  
  1568. # sgml::DTD:NOTATION --
  1569.  
  1570. proc sgml::DTD:NOTATION {id value} {
  1571.     variable notation_exp
  1572.     upvar opts state
  1573.  
  1574.     if {[regexp $notation_exp $value x scheme data] == 2} {
  1575.     } else {
  1576.     eval $state(-errorcommand) notationvalue [list "notation value \"$value\" incorrectly specified"]
  1577.     }
  1578. }
  1579.  
  1580. ### Utility procedures
  1581.  
  1582. # sgml::noop --
  1583. #
  1584. #    A do-nothing proc
  1585. #
  1586. # Arguments:
  1587. #    args    arguments
  1588. #
  1589. # Results:
  1590. #    Nothing.
  1591.  
  1592. proc sgml::noop args {
  1593.     return 0
  1594. }
  1595.  
  1596. # sgml::identity --
  1597. #
  1598. #    Identity function.
  1599. #
  1600. # Arguments:
  1601. #    a    arbitrary argument
  1602. #
  1603. # Results:
  1604. #    $a
  1605.  
  1606. proc sgml::identity a {
  1607.     return $a
  1608. }
  1609.  
  1610. # sgml::Error --
  1611. #
  1612. #    Throw an error
  1613. #
  1614. # Arguments:
  1615. #    args    arguments
  1616. #
  1617. # Results:
  1618. #    Error return condition.
  1619.  
  1620. proc sgml::Error args {
  1621.     uplevel return -code error [list $args]
  1622. }
  1623.  
  1624. ### Following procedures are based on html_library
  1625.  
  1626. # sgml::zapWhite --
  1627. #
  1628. #    Convert multiple white space into a single space.
  1629. #
  1630. # Arguments:
  1631. #    data    plain text
  1632. #
  1633. # Results:
  1634. #    As above
  1635.  
  1636. proc sgml::zapWhite data {
  1637.     regsub -all "\[ \t\r\n\]+" $data { } data
  1638.     return $data
  1639. }
  1640.  
  1641. proc sgml::Boolean value {
  1642.     regsub {1|true|yes|on} $value 1 value
  1643.     regsub {0|false|no|off} $value 0 value
  1644.     return $value
  1645. }
  1646.  
  1647. proc sgml::dbgputs {where text} {
  1648.     variable dbg
  1649.  
  1650.     catch {if {$dbg} {puts stdout "DBG: $where ($text)"}}
  1651. }
  1652.