Alex Caldwell
# XML DOM Tk Text Browser Editor # May 18, 2004 # Alex Caldwell M.D. # [email protected] # derived from xmlbrowser.tcl # by Richard Suchenwirth http://wiki.tcl.tk/3884 # Comparison: # Modified to allow real time editing of the XML DOM in memory via events in the Tk text # widget. # It seems like the text widget is faster than the BWidget Tree. I personally find # the table-like layout in the Tk text widget with the color coding is easier to read # and find your data than the Tree widget. # Purpose: # to get some XML data, parse it into an XML DOM tree in memory, and then # map the DOM structure into an editable display in a Tk text widget. A sort of "Map" is # generated in the text widget using marks and tags that corresponds to elements and # attributes in the DOM structure. Event bindings are used so that any changes to the data # in the text widget are updated in the DOM memory structure in parallel in real time. # The modified DOM can then be dumped as XML to a file, sent to another part of the # application, transmitted over the network, etc. It currently dumps the modified DOM # as XML to the console when any change is made to the data. So start it from a console. # This is similar to domtext widget in Swish and Waxml by Steve Ball http://waxml.sourceforge.net # I was using a similar technique with SAX and mapping the XML elements and attributes with # Tk text widget marks and tags. The problem with the SAX method is that it's sort of one-way # SAX works good for mapping the XML data to the text widget display, but when you want to # reverse that, and go back to XML from the Tk text widget display, you have to write a unique # Tcl script to collect your data and convert it back to XML. So the SAX method is not very # generalizable to different XML schema. Each change in the XML schema means you have to # re-write your script. # With the DOM model, it is more automatic in both directions and more generalizable to different # XML schema. # Limitations and Bugs: # You can only edit the text node data and the value of the attributes, you can't change the # XML schema itself. # Don't allow the value of an attribute to become NULL. You can change the value but leave at least # one character between the quote marks. # To do: # wrap it up in a namespace, turn it into a Tcl package and make it behave as a proper Tk widget with # options etc like the domtext widget. # Add a "Save" button and a better menubar across the top. # maybe add some whitespace so variable length text elements will always line up vertically in the # display which will make it faster to find stuff. # maybe remove or hide the XML element tags in the display and only show the text node data # and the attribute values with the element names only across a header at the top. #BWidget not required since we are going to use a Tk text widget #package require BWidget # we do need the tDOM package package require tdom proc recurseInsert {w node parent} { global colorlist color name tag_list foreach one $tag_list { global $one } set name [$node nodeName] set done 0 if {$name=="#text" || $name=="#cdata"} { set text [string map {\n " "} [$node nodeValue]] } else { set text <$name set lineno [lindex [split [$w index current] "."] 0] $w mark set begin$name$lineno [$w index current] $w mark gravity begin$name$lineno left $w insert end $text set text "" foreach att [getAttributes $node] { $w mark set begin$att$lineno [$w index current] $w mark gravity begin$att$lineno left catch {set text " $att=\"[$node getAttribute $att]\"" $w insert end $text } $w mark set end$att$lineno [$w index current] $w mark gravity end$att$lineno left $w tag add $att$lineno "begin$att$lineno + 1 char" end$att$lineno } # this tests if the option to insert a newline to format the display in the text # widget is turned on for this element if {[set $name] == "1"} { set text ">\n" } else { set text > } set children [$node childNodes] # I think this is a test to see if the child is a text node if {[llength $children]==1 && [$children nodeName]=="#text"} { # this tests if the option is turned on for that element to break the data display with # a newline on that element. # You can format the display in various ways depending on which elements you choose # to use newlines on in the Tk text widget. if {[set $name] == "1"} { append text "[$children nodeValue]\n</$name>\n" } else { append text "[$children nodeValue]</$name>" } set done 1 } } $w insert end "$text" $w mark set end$name$lineno [$w index current] $w mark gravity end$name$lineno left $w tag add $name$lineno begin$name$lineno end$name$lineno # update the DOM Tree and dump it as XML to the standard output. In application, you would save it # or use it somewhere else. Here it's just to monitor the changes to the DOM tree. $w tag bind $name$lineno <KeyRelease> " set new_text \[$w get begin$name$lineno end$name$lineno\] regsub \"<${name}(.*?)>\" \$new_text \{\} new_text regsub \"</$name>\" \$new_text \{\} new_text \[$node firstChild\] nodeValue \$new_text puts \"\[\$root asXML\]\" " $w tag configure $name$lineno -background $color($name) -relief raised -borderwidth 1 foreach att [getAttributes $node] { $w tag configure $att$lineno -relief sunken -background $color($att) -borderwidth 1 $w tag raise $att$lineno # Update DOM tree in memory and dump as XML to the standard output. In application, you would save it # or use it somewhere else. Here it's just to monitor the changes to the DOM tree. $w tag bind $att$lineno <KeyRelease> " set new_attribute \[$w get \"begin$att$lineno + 1 char\" end$att$lineno\] regsub -all \{\"\} \$new_attribute \{\} new_attribute set new_attribute \[split \$new_attribute \"=\"\] $node setAttribute \[lindex \$new_attribute 0\] \"\[lindex \$new_attribute 1\]\" puts \"\[\$root asXML\]\" " } if !$done { foreach child [$node childNodes] { recurseInsert $w $child $node } $w mark set startend[lindex [$node nodeName] 0]$lineno current $w mark gravity startend[lindex [$node nodeName] 0]$lineno left if {[set [lindex [$node nodeName] 0]] == "1"} { $w insert end "\n</[lindex [$node nodeName] 0]>\n" } else { $w insert end "</[lindex [$node nodeName] 0]>" } $w mark set end[lindex [$node nodeName] 0]$lineno [$w index current] $w mark gravity end[lindex [$node nodeName] 0]$lineno left $w tag add [lindex [$node nodeName] 0]$lineno startend[lindex [$node nodeName] 0]$lineno \ end[lindex [$node nodeName] 0]$lineno $w tag configure [lindex [$node nodeName] 0]$lineno -background $color([lindex [$node nodeName] 0]) \ -relief raised -borderwidth 1 } } proc getAttributes node { if {![catch {$node attributes} res]} {set res} } # this is for generating a list of the unique element names and attribute names # in your XML data. This will be used for mapping a unique color to the text # corresponding to that data in the Tk text widget. It makes an array called color # indexed by the element and attribute names, with a value of a color name for the display proc recurse_names {node} { global tag_list color colorlist foreach child [$node childNodes] { if {![regexp [$child nodeName] $tag_list] && [$child nodeName] != "#text" } { lappend tag_list [$child nodeName] set color([$child nodeName]) [lindex $colorlist [llength $tag_list]] if {[getAttributes $child] != ""} { set match "" if {![regexp [getAttributes $child] $tag_list match]} { lappend tag_list [getAttributes $child] set color([getAttributes $child]) [lindex $colorlist [llength $tag_list]] } if {$match != "" && ![string compare [getAttributes $child] $match]} { lappend tag_list [getAttributes $child] set color([getAttributes $child]) [lindex $colorlist [llength $tag_list]] } } recurse_names $child } } } # Check for an XML file from the command line. If none, present user with a # tk_getOpenFile dialog. if {[lindex $argv 0] == ""} { set fp [open [tk_getOpenFile]] } else { set fp [open [file join [lindex $argv 0]]] } fconfigure $fp -encoding utf-8 set xml [read $fp] close $fp dom parse $xml doc $doc documentElement root # BWidget Tree not needed as we are using the Tk text widget #Tree .t -yscrollcommand ".y set" -xscrollcommand ".x set" -padx 0 menubutton .m -text "Options... Add newline to choice of XML tags to format display" -menu .m.menu -indicatoron true grid .m -sticky news menu .m.menu text .t -yscrollcommand ".y set" -xscrollcommand ".x set" -wrap none scrollbar .x -ori hori -command ".t xview" scrollbar .y -ori vert -command ".t yview" grid .t .y -sticky news grid .x -sticky news grid rowconfig . 0 -weight 1 grid columnconfig . 0 -weight 1 # this is a map of the colors to use for the various attributes and elements # you want to display - needs to be at least as long as the no. of unique elements and attributes # in your XML data set colorlist [list white bisque red green lightblue yellow pink #E4D0EC orange #FF3F3F wheat\ peachpuff lightgrey olivedrab2 white ivory bisque pink yellow skyblue3] # set up a map of colors for each unique element or attribute set tag_list "" set color([$root nodeName]) [lindex $colorlist 0] lappend tag_list "[$root nodeName]" recurse_names $root # add menuitems representing the element names to the options menu foreach one $tag_list { .m.menu add checkbutton -label $one -variable $one -onvalue 1 -offvalue 0 } .m.menu add separator .m.menu add command -label "Save Options" -command { set f [open xmlbrowser.options w] foreach one $tag_list { puts $f "set $one [set $one]" } close $f } if {[catch {source ./xmlbrowser.options} res]} { toplevel .res label .res.label -text "No options file Available...\nSet and save options\nto format display." grid .res.label button .res.ok -text "OK" -command { destroy .res } grid .res.ok } after 5 recurseInsert .t $root root
Some XML Data to try with it: [1]
See also "browser" and "WAX" the successor to Swish XML editor.
A little XML parser | Arts and crafts of Tcl-Tk programming | Creates its display with the versatile text widget.
Using Snit to make Tk DOM Glue - has the same idea - attaches a widget to the XML DOM so changes in the widget update the DOM.