Another version of the starDOM XML editor

Arjen Markus I took the script at the starDOM page and added menus and a little dialogue for editing existing attributes - I wanted this because it reduces the chance for errors.

It is not perfect yet:

  • Some work on the generated dialogue is needed (if you edit "DATA", you may want to have a proper text widget, not an entry widget at your disposal)
  • Checks are required to avoid inadvertent loss of work
  • Add the Gtk look for use on Linux/UNIX

Code

package require Tk
package require BWidget
package require tdom

namespace eval ComboBox {#needed to extend BWidget functionality}
proc ComboBox::enable {w what args} {
    switch -- $what {
        history {
            $w configure -values {{}} ;# always keep a blank on top
            foreach evt {<Return> <FocusOut>} {
                $w bind $evt {+ ComboBox::_add %W [%W cget -text]}
            }
        }
        chooser {
            set values [$w cget -values]
            set width 0
            foreach i $values {
                set sl [string length $i]
                if {$sl > $width} {set width $sl}
            }
            set bg [[label .dummy] cget -bg]
            destroy .dummy
            $w setvalue first
            $w configure -width [expr {$width+1}]
            $w configure -editable 0 -relief flat -entrybg $bg
        }
    }
    if {$args != ""} {eval [list $w configure] $args}
}
proc ComboBox::_add {w item} {
    set w [winfo parent $w] ;# binding comes from entry
    set values [$w cget -values]
    if {[lsearch -exact $values $item] < 0} {
        $w configure -values [linsert $values 1 $item]
    }
}
namespace eval starDOM {
    set version 0.43
    set about "<about xmlns:foo=\"http://foo.bar/grill\">
          <!-- demo, self-test, rudimentary documentation -->
          <?Tcl toplevel .greeting; button .greeting.b -text \"Isn't it nice\" \
                  -command {destroy .greeting}; pack .greeting.b ?>
          <name       >starDOM</name>
          <version    >$version</version>
          <description lang=\"en\">A little XML browser - now equipped with a rather long description string to try out the popup feature</description>
          <uses>
            <pkg>Tk [package require Tk]</pkg>
            <pkg>BWidget [package require BWidget]</pkg>
            <pkg>tdom \[package require tdom\]</pkg>
          </uses>
          <authors foo:test=\"ok?\">
            <author>Rolf Ade</author>
            <author>Arjen Markus</author>
            <author>Richard Suchenwirth</author>
          </authors>
        </about>"

     namespace eval vars {
         # Private variables, when editing attributes
         variable save_node ""
     }
}
#------------------------------------------ PROCEDURE DIVISION.
proc starDOM::attName att {
    if {[llength $att] != 3} {return $att}
    if {[lindex $att 2] == {}} {
       set attName "xmlns"
       if {[lindex $att 1] != {}} {
           append attName : [lindex $att 1]
       }
       return $attName
    } else {
       return [lindex $att 1]:[lindex $att 0]
    }
}
proc starDOM::Eval {query} {
    variable info
    catch {uplevel #0 $query} res ;# execute any Tcl command
    puts "% $query\n$res"
    if {[string length $res]>70} {set res [string range $res 0 69]...}
    set info $res
}
proc starDOM::formatNodeText {node} {
    switch [$node nodeType] {
        "ELEMENT_NODE" {
            set text "<[$node nodeName]"
            foreach att [$node attributes] {
                if {[llength $att] == 3} { #(1)..
                    if {[lindex $att 2] == {}} {
                        set attName "xmlns"
                        if {[lindex $att 1] != {}} {
                            append attName ":[lindex $att 1]"
                        }
                    } else {
                        set attName "[lindex $att 1]:[lindex $att 0]"
                    }
                } else {
                    set attName $att
                } ;#..(1)
                append text " $attName=\"[$node getAttribute $attName]\""
            }
            append text ">"
            if {[$node hasChildNodes]} {
                set children [$node childNodes]
                if {[llength $children]==1 && [$children nodeName]=="#text"} {
                    append text [string map {\n " "} [$children nodeValue]]
                } else {
                    set drawcross "allways" ;# bad English, wanted by BWidget
                }
            }
        }
        "TEXT_NODE" {
            set text [string map {\n " "} [$node nodeValue]]
        }
        "COMMENT_NODE" {
            set text "<!--[string map {\n " "} [$node nodeValue]]-->"
            set fill "grey50"
        }
        "PROCESSING_INSTRUCTION_NODE" {
            set text "<?[$node target] [string map {\n "" } [$node data]]?>"
            set fill "grey50"
        }
    }
    return $text
}
proc starDOM::insertNode {w parent node} {
    set drawcross "auto"
    set fill "black"
    set text [formatNodeText $node]
    switch [$node nodeType] {
        "ELEMENT_NODE" {
            set children [$node childNodes]
            if {[llength $children]!=1 || [$children nodeName]=="#text"} {
                set drawcross "allways" ;# bad English, wanted by BWidget
            }
        }
        "COMMENT_NODE" -
        "PROCESSING_INSTRUCTION_NODE" {
            set fill "grey50"
        }
        default {
            set fill "black"
        }
    }
    $w insert end $parent $node -text $text -fill $fill -drawcross $drawcross
}
proc starDOM::nodeInfo {w node {prefix ""}} {
    variable info
    if {[info command $node]==""} return
    set info "$prefix$node: [$node toXPath]"
    append info " - [llength [$node childNodes]] child(ren)"
    catch {append info " - [string length [$node text]] text chars"}
}
proc starDOM::nodeText {w node} {
    set text [$w itemcget n:$node -text]
    set w2 .[clock clicks]
    toplevel $w2
    wm title $w2 $node
    pack [text $w2.0 -width 50 -height 20 -wrap word -bg lightyellow]
    $w2.0 insert end $text
}

proc starDOM::nodeText {w node} {
     if { $vars::save_node != "" } {
         raise .tnode
         return
     }

     set vars::save_node    $node

     $w itemconfigure $node -fill red

     set tag      [$node nodeName]

     #
     # Now create a toplevel window to edit the attribute values
     #
     toplevel .tnode
     frame    .tnode.f
     wm title .tnode "Attributes for: $tag"

     foreach att [$node attributes] {
         if {[llength $att] == 3} { #(1)..
             if {[lindex $att 2] == {}} {
                 set attrib "xmlns"
                 if {[lindex $att 1] != {}} {
                     append attrib ":[lindex $att 1]"
                 }
             } else {
                 set attrib "[lindex $att 1]:[lindex $att 0]"
             }
         } else {
             set attrib $att
         } ;#..(1)

         set vars::$attrib [$node getAttribute $attrib]
         label .tnode.f.l$attrib -text $attrib
         entry .tnode.f.e$attrib -textvariable ::starDOM::vars::$attrib
         grid  .tnode.f.l$attrib .tnode.f.e$attrib \
             -sticky nw -padx 3 -pady 2
     }

     set children [$node childNodes]
     if {[llength $children]==1 && [$children nodeName]=="#text"} {
        label .tnode.f.l_data -text "DATA:"
        entry .tnode.f.e_data -textvariable ::starDOM::vars::DATA
        grid  .tnode.f.l_data .tnode.f.e_data \
            -sticky nw -padx 3 -pady 2
        set vars::DATA [$children nodeValue]
     }

     button .tnode.ok     -text OK     \
         -command [list starDOM::closeNodeEdit $w 1] -width 6
     button .tnode.cancel -text Cancel \
         -command [list starDOM::closeNodeEdit $w 0] -width 6

     grid   .tnode.f -
     grid   .tnode.ok .tnode.cancel -padx 3 -pady 4
}
proc starDOM::closeNodeEdit {w save} {
    if { $save } {
        foreach att [$vars::save_node attributes] {
            if {[llength $att] == 3} { #(1)..
                if {[lindex $att 2] == {}} {
                    set attrib "xmlns"
                    if {[lindex $att 1] != {}} {
                        append attrib ":[lindex $att 1]"
                    }
                } else {
                    set attrib "[lindex $att 1]:[lindex $att 0]"
                }
            } else {
                set attrib $att
            } ;#..(1)
            $vars::save_node setAttribute $attrib [set vars::$attrib]
        }
        set children [$vars::save_node childNodes]
        if {[llength $children]==1 && [$children nodeName]=="#text"} {
           $children nodeValue [set vars::DATA]
        }
        # .t needed directly - $w is the canvas!
        .t itemconfigure $vars::save_node -text \
            [formatNodeText $vars::save_node]
    }

    destroy .tnode
    $w itemconfigure $vars::save_node -fill black
    set vars::save_node ""
}
proc starDOM::openCross {w {node ""}} {
    if {$node == ""} {set node [$w selection get]}
    if {[$w itemcget $node -drawcross] == "allways"} {
        foreach child [$node childNodes] {
            insertNode $w $node $child
        }
        $w itemconfigure $node -drawcross "auto"
    }
}
proc starDOM::openFile {w {filename ""}} {
    variable info
    if {$filename == ""} {
        set filename [tk_getOpenFile -filetypes {
          {{XML file} *.xml} {{HTML file} *.html} {{All files} *.*}}]
        }
    if {$filename != ""} {
        cd [file dir $filename] ;# so later opens will start here
        wm title . "$filename - starDOM"
        starDOM::showTree $w $filename
        set info "Loaded $filename - [file size $filename] bytes"
    }
}
proc starDOM::save {{filename ""}} {
    variable root; variable info
    if {$filename == ""} {set filename [lindex [wm title .] 0]}
    set filename [tk_getSaveFile -filetypes {
        {{XML file} *.xml} {{HTML file} *.html} {{All files} *.*}
        } -initialfile $filename -defaultextension .xml]
    if {$filename != ""} {
        set fp [open $filename w]
        $root asXML -channel $fp
        close $fp
        wm title . "$filename - starDOM"
        set info "Saved $filename - [file size $filename] bytes"
    }
}
proc starDOM::search {w} {
    variable mode; variable query; variable info;
    variable changed; variable next; variable root
    variable nodes
    if {$changed} {
        switch -- $mode {
            case   - case/all -
            XPath - XPath/all {
                set q [expr {$mode=="case" || $mode == "case/all" ?
                    "descendant-or-self::text()\[contains(.,'$query')\]"
                    : $query}]
                set t [time {set nodes [$root selectNodes $q]}]
            }
            nocase - nocase/all -
            regexp - regexp/all {
                set nodes {}
                if {$mode == "nocase" || $mode == "nocase/all"} {
                    set s [string tolower $query]
                    set cond {[string first $s [string tolo [$n nodeValue]]]>=0}
                } else {
                    set cond {[regexp $query [$n nodeValue]]}
                }
                foreach n [$root selectNodes //text()] {
                    if $cond {lappend nodes $n}
                }
            }
            eval {return [Eval $query]}
        }
        set changed 0
        set next [expr {[string first /all $mode] >= 0 ? -1: 0}]
    }
    if {[llength $nodes]} {
        showNode $w
    } else {set info "Not found."}
}
proc starDOM::showNode w {
    variable next; variable hilited; variable info; variable nodes
    foreach hinode $hilited {$w itemconfigure $hinode -fill black}
    set hilited {}
    set nrOfNodes [llength $nodes]
    if {$next == -1} {
        set nr 0; set nrmax [expr {$nrOfNodes - 1}]
    } else {
        set nr $next; set nrmax $next
        nodeInfo $w [lindex $nodes $nr] "[expr {$nr+1}]/$nrOfNodes - "
        if {($nr + 1) == $nrOfNodes} {
            set next 0
        } else {
            incr next
        }
    }
    while {$nr <= $nrmax} {
        set node [lindex $nodes $nr]
        if {$node==""} break
        foreach ancestor [$node selectNodes ancestor::*] {
            openCross $w $ancestor
        $w itemconfigure $ancestor -open 1
        }
        set parent [$node parentNode]
        set sibs [$parent childNodes]
            if {[llength $sibs]==1 && [$sibs nodeName]=="#text"} {
            set node $parent
        }
        $w itemconfigure $node -fill blue
        if {$next > -1} {$w see $node}
        lappend hilited $node
        incr nr
    }
}
proc starDOM::showTree {w string {isText 0}} {
    variable hilited {} root
    variable style
    raise [winfo toplevel $w]
    if {$root != ""} {
        [$root ownerDocument] delete
        set root "" ;# in case later parsing fails
    }
    $w delete [$w nodes root]
    $w selection clear
    if {!$isText && $style == ""} {
        set fd  [tDOM::xmlOpenFile $string]
        set doc [eval dom parse $style -channel $fd]
        close $fd
    } else {
        if {!$isText} {
            set fd [open $string]
            set string [read $fd]
            close $fd
        }
        set doc [eval dom parse $style [list $string]]
    }
    set root [$doc documentElement]
    insertNode $w root $root
    openCross $w $root   ;# Show children of root right after startup
    $w itemconfigure $root -open 1
}
proc starDOM::viewAbout {} {
    tk_messageBox -icon info -title starDOM -type ok -message \
tarDOM:
simple XML file viewer/editor
Rolf Ade, Arjen Markus, and
chard Suchenwirth}
    destroy .vs
}
proc starDOM::closeWindow {} {
    # TODO: check if the contents have changed
    destroy .vs
}
proc starDOM::exitGUI {} {
    # TODO: check if this is really what the user wants
    destroy .
}
proc starDOM::viewSource {{fn ""}} {
    variable root
    if {$fn == ""} {set fn [lindex [wm title .] 0]}
    catch {destroy .vs}
    toplevel .vs
    wm title .vs "$fn - source"
    bind .vs <Control-space> {starDOM::showTree .t [.vs.t get 1.0 end] 1}
    text .vs.t -wrap word -yscrollcommand ".vs.y set"
    scrollbar .vs.y -ori vert -command ".vs.t yview"

    #
    # Set up the (simple) menu bar
    set  mw    .vs.menu
    menu       $mw
    menu       $mw.window  -tearoff false

    $mw add cascade -label Window -menu $mw.window

    .vs configure -menu $mw

    #
    # Set up the "Window" menu
    #
    $mw.window add command -label Save -underline 0 \
       -command {starDOM::showTree .t [.vs.t get 1.0 end] 1}
    $mw.window add separator
    $mw.window add command -label Close -underline 0 \
       -command {starDOM::closeWindow}

    pack .vs.y -side right -fill y
    pack .vs.t -fill both -expand 1
    if {[file exists $fn]} {
        set fp [open $fn]
        .vs.t insert 1.0 [read $fp]
        close $fp
    } elseif {$fn != "Untitled"} {.vs.t insert 1.0 [$root asXML]}
    if {0} {
        if {[.t selection get] != ""} {
            set node [.t selection get]
            set toPath [$node toXPath]
            dom setStoreLineColumn 1
            set tmpdoc [dom parse [.vs.t get 1.0 end]]
            dom setStoreLineColumn 0
            $tmpdoc documentElement tmproot
            set tmpnode [$tmproot selectNodes $toPath]
            set line [$tmpnode getLine]
            set col  [$tmpnode getColumn]
            $tmpdoc delete
            focus .vs.t
            .vs.t mark set insert $line.$col
            .vs.t see $line.$col
        }
    }
}
proc starDOM::UI {} {
    variable changed 0 mode "case" query "" info "" root "" style ""
    interp alias {} help {} DynamicHelp::register
    foreach i {file new open save} {
      set im($i) [image create photo \
        -file [file join $::BWIDGET::LIBRARY images $i.gif]]
    }

    #
    # Set up the (simple) menu bar
    set  mw    .menu
    menu       $mw
    menu       $mw.file  -tearoff false
    menu       $mw.edit  -tearoff false
    menu       $mw.help  -tearoff false

    $mw add cascade -label File   -menu $mw.file
    $mw add cascade -label Edit   -menu $mw.edit
    $mw add cascade -label Help   -menu $mw.help

    . configure -menu $mw

    #
    # Set up the "File" menu
    #
    $mw.file add command -label New -underline 0 \
        -command {starDOM::viewSource Untitled}
    $mw.file add command -label Open -underline 0 \
       -command {starDOM::openFile .t}
    $mw.file add separator
   # $mw.file add command -label Save -underline 0 \
   #    -command {starDOM::save}
     $mw.file add command -label "Save as ..." -underline 1 \
        -command {starDOM::save}
    $mw.file add separator
    $mw.file add command -label Exit -underline 1 \
       -command {starDOM::exitGUI}

    $mw.edit add command -label "Edit source" -underline 1 \
        -command {starDOM::viewSource}

    $mw.help add command -label "About ..." -underline 0 \
        -command {starDOM::viewAbout}

    frame  .f
    Button .f.new -image $im(new) -command {starDOM::viewSource Untitled} \
        -width 16
    help .f.new balloon "Create new XML document
        <Control-space> to parse"
    Button .f.open -image $im(open) -command {starDOM::openFile .t}
    help .f.open balloon "Open existing XML file"
    Button .f.view -image $im(file) -width 16 -command starDOM::viewSource
    help .f.view balloon "View document source
        <Control-space> to reparse after editing"
    Button .f.save -image $im(save) -command starDOM::save
    help .f.save balloon "Save current document to file"
    ComboBox .f.e -width 25 -textvariable starDOM::query
    .f.e enable history
    .f.e bind <Key>    {set starDOM::changed 1}
    .f.e bind <Return> {+ starDOM::search .t}
    help .f.e balloon "Enter search text/expression here.
        Hit <Return> to search (or eval).
        History: see pop-up, or use <Up>/<Down>"
    ComboBox .f.m -values {
        case case/all nocase nocase/all regexp regexp/all XPath XPath/all eval
        } -textvariable starDOM::mode
    .f.m enable chooser -relief ridge
    help .f.m balloon "Search mode (full text, except XPath)
        case:\tcase-sensitive
        nocase:\tcase-insensitive (A=a)
        regexp:\tregular expression
        XPath:\tDon't know? Don't bother!
        */all:\tthe same, all at once
        eval:\texecute Tcl command (to stdout)"
    ComboBox .f.style -values {{} -html -simple} \
        -textvariable starDOM::style
    .f.style enable chooser -relief ridge
    help .f.style balloon "Parsing style:
        (blank): regular = strict
        -html: tolerant for bad HTML
        -simple: fast, 7-bit only"
    eval pack [winfo children .f] -side left -fill y
    pack  .f.e -fill x -expand 1

    Tree .t -yscrollcommand ".y set" -xscrollcommand ".x set" -padx 0 \
            -opencmd "starDOM::openCross .t" -height 20
    bind .t <KeyPress-Right> "starDOM::openCross .t;Tree::_keynav right .t"
    .t bindText <1> {.t selection set}
    .t bindText <1> {+ starDOM::nodeInfo %W}
    .t bindText <Double-1> {starDOM::nodeText %W}
    scrollbar .x -ori hori -command ".t xview"
    scrollbar .y -ori vert -command ".t yview"
    Label .info -textvariable starDOM::info -anchor w -pady 0
    help .info balloon "Short info display
        3/5: 3rd of 5 instances highlighted
        Click on a node for its XPath and #children"
    grid .f   -  -sticky ew
    grid .t   .y -sticky news
    grid .x      -sticky news
    grid .info - -sticky ew
    grid rowconfig    . 1 -weight 1
    grid columnconfig . 0 -weight 1
    if {$::tcl_platform(platform)=="windows"} {
        catch {bind .t.c <MouseWheel> {
            %W yview scroll [expr {int(pow(%D/-120,3))}] units
        }}
        catch {focus .t.c}
    }
}
#---------------------------------------------------- "main"
starDOM::UI
set starDOM::info "Welcome to starDOM $starDOM::version!"
if {[llength $argv] && [file exists [lindex $argv 0]]} {
    starDOM::showTree .t [lindex $argv 0]
} else {
    starDOM::showTree .t $starDOM::about 1
}
bind . <Shift-Escape> {console show}
bind . <Escape>       {exec wish $argv0 &; exit}
trace variable starDOM::mode w {set starDOM::changed 1 ;#}

jmn 2004-09-30 :

I found the presence of the #text data to the right of the nodes a little confusing seeing as it's duplicated once the node is opened. Apply the following patch to make this text only appear to the right of a node when it's closed.

escargo 2005-07-26: I tried to apply this patch (on Cygwin running on Windows XP Pro Service Pack 2), but I got a "malformed patch at line 39: @@ -254,6 +261,11 @@" error message. Could you be more explicit about how the patch was created and how it is supposed to be applied?

After manually patching the source, I discovered that running this on Windows where the document name contains a blank, the viewSource proc errors out trying to open the file.

--- stardom1.tcl        Thu Sep 30 06:27:31 2004
+++ stardom2.tcl        Thu Sep 30 07:19:28 2004
@@ -79,7 +79,7 @@
        if {[string length $res]>70} {set res [string range $res 0 69]...}
        set info $res
     }
-    proc starDOM::formatNodeText {node} {
+    proc starDOM::formatNodeText {node {isopen 0}} {
        switch [$node nodeType] {
            "ELEMENT_NODE" {
                set text "<[$node nodeName]"
@@ -102,7 +102,10 @@
                if {[$node hasChildNodes]} {
                    set children [$node childNodes]
                    if {[llength $children]==1 && [$children nodeName]=="#text"} {
-                       append text [string map {\n " "} [$children nodeValue]]
+                       #show #text to right of closed node only
+                       if {!$isopen} {
+                           append text [string map {\n " "} [$children nodeValue]]
+                       }
                    } else {
                        set drawcross "allways" ;# bad English, wanted by BWidget
                    }
@@ -236,10 +239,14 @@
            set children [$vars::save_node childNodes]
            if {[llength $children]==1 && [$children nodeName]=="#text"} {
               $children nodeValue [set vars::DATA]
+                  if {[llength [.t nodes $vars::save_node]]} {
+                  .t itemconfigure [.t nodes $vars::save_node 0] -text [set vars::DATA]
+              }
            }
           # .t needed directly - $w is the canvas!
+             set isopen [.t itemcget $vars::save_node -open]
            .t itemconfigure $vars::save_node -text \
-               [formatNodeText $vars::save_node]
+               [formatNodeText $vars::save_node $isopen]
        }
        destroy .tnode
@@ -254,6 +261,11 @@
            }
            $w itemconfigure $node -drawcross "auto"
        }
+      .t itemconfigure $node -text [formatNodeText $node 1]
+    }
+    proc starDOM::closeCross {w {node ""}} {
+       if {$node == ""} {set node [$w selection get]}
+       .t itemconfigure $node -text [formatNodeText $node]
     }
     proc starDOM::openFile {w {filename ""}} {
        variable info
@@ -533,7 +545,7 @@
        pack  .f.e -fill x -expand 1
        Tree .t -yscrollcommand ".y set" -xscrollcommand ".x set" -padx 0 \
-               -opencmd "starDOM::openCross .t" -height 20
+               -opencmd "starDOM::openCross .t" -closecmd "starDOM::closeCross .t" -height 20
        bind .t <KeyPress-Right> "starDOM::openCross .t;Tree::_keynav right .t"
        .t bindText <1> {.t selection set}
        .t bindText <1> {+ starDOM::nodeInfo %W}

JET This looks very good. Is it time yet to make it a starkit and add it to the list of downloadable starkits?