- xmlstruct::create xml - Returns an extended domNode object command
- $node set xpathQuery ?value? - retrieves or modifies portions of the xml document that match the given xpathQuery
- $node unset xpathQuery - deletes the portions of the xml document that match the given xpathQuery
- $node lappend xpathQuery value ?value? ?value? ... - appends the given values to the node(s) that match the given xpathQuery (See examples below--this description is not very good)
set y [xmlstruct::create { <employees> <employee> <name>Joe</name> <age>28</age> <hobbies> <favorite> <name>Sailing</name> </favorite> <others> <name>Reading</name> <name>Running</name> </others> </hobbies> <department> <name>Engineering</name> </department> </employee> </employees> }] # Reading values % $y set /employees/employee/name Joe % $y set /employees/employee/age 28 % $y set //name Joe Sailing Reading Running Engineering % $y set //hobbies//name Sailing Reading Running % $y set {/employees/employee[1]//favorite} <name>Sailing</name> % $y set / ;# Returns whole document - output not included here # Modifying values % $y set {/employees/employee[name='Joe']/age} 27 ;# Braces needed to escape XPath square bracket % $y set {/employees/employee[name='Joe']/age} 27 % $y set {//employee[1]/name} Bill % $y set {//employee/name} Bill # Deleting % $y unset //department % $y unset //favorite % $y unset //hobbies % $y set / <employees> <employee> <name>Bill</name> <age>27</age> </employee> </employees> # Building a document incrementally % set y [xmlstruct::create <employees/>] % $y lappend employee Joe Bill Bob % $y set / <employees> <employee>Joe</employee> <employee>Bill</employee> <employee>Bob</employee> </employees> % set y [xmlstruct::create <employees/>] % $y lappend employee <name>Joe</name> <name>Bill</name> <name>Bob</name> <employees> <employee> <name>Joe</name> </employee> <employee> <name>Bill</name> </employee> <employee> <name>Bob</name> </employee> </employees>}
# # TODO: return the value when setting a value and return a list of values when multiple values are set # package require tdom # By placing these procs in the ::dom::domNode namespace, they automatically # become add-on domNode methods proc ::dom::domNode::unset {node query} { ::set resultNodes [$node selectNodes $query type] switch $type { attrnodes {xmlstruct::unsetattrs $node $query} nodes {xmlstruct::unsetnodes $resultNodes} empty {error "No results found for '$query'"} default {error "$type is an unsupported query result type"} } } proc ::dom::domNode::set {node query args} { switch [llength $args] { 0 {return [xmlstruct::getvalue $node $query]} 1 {return [xmlstruct::setvalue $node $query [lindex $args 0]]} default {error "wrong # args: should be \"set xpathQuery ?newValue?\""} } } proc ::dom::domNode::lappend {node query args} { foreach arg $args { xmlstruct::setnew $node $query $arg } } namespace eval xmlstruct {} # Convenience function for creating an xml doc and returning the root proc xmlstruct::create {xml} { ::set doc [dom parse $xml] return [$doc documentElement] } # For '$node set query' calls proc xmlstruct::getvalue {node query} { ::set resultNodes [$node selectNodes $query type] switch $type { attrnodes { ::set retVal {} foreach attrVal $resultNodes { lappend retVal [lindex $attrVal 1] } return $retVal } nodes { ::set retVal {} foreach node $resultNodes { ::set xml "" foreach child [$node childNodes] { append xml [$child asXML] } lappend retVal $xml } # This is so the curly braces are not there due to the above lappend if {[llength $resultNodes] == 1} {::set retVal [lindex $retVal 0]} return $retVal } empty {return ""} default {error "$type is an unsupported query result type"} } } # For '$node set query value' calls proc xmlstruct::setvalue {node query value} { ::set targetNodes [$node selectNodes $query type] switch $type { nodes {xmlstruct::setnodes $targetNodes $query $value} attrnodes {xmlstruct::setattrs $node $query $value} empty {xmlstruct::setnew $node $query $value} default {error "$type is an unsupported query result type"} } } # Creates a new attribute/element for an xpath query in which all # the elements of the query up to the last exist proc xmlstruct::setnew {node query value} { set possibleMatch [split $query /] set unmatched [lindex $possibleMatch end] set possibleMatch [lreplace $possibleMatch end end] if {[llength $possibleMatch] == 0} { set possibleMatch . } set nodes [$node selectNodes [join $possibleMatch /] type] switch $type { nodes { if {[string index $unmatched 0] == "@"} { foreach node $nodes { $node setAttribute [string range $unmatched 1 end] $value } } else { foreach node $nodes { $node appendXML "<$unmatched/>" set newNode [$node lastChild] $newNode set . $value } } } attrnodes {error "Can't add children to attributes ($possibleMatch)"} empty {error "Create elements matching $possibleMatch first"} } } # For i.e. '$node unset {/employees/employee[1]/@age}' calls proc xmlstruct::unsetattrs {node query} { ::set nodeQuery [join [lrange [split $query /] 0 end-1] /] ::set attribute [string range [lindex [split $query /] end] 1 end] foreach matchingNode [$node selectNodes $nodeQuery] { $matchingNode removeAttribute $attribute } } # For i.e. '$node set {/employees/employee[1]/@age} 25' calls proc xmlstruct::setattrs {node query value} { ::set nodeQuery [join [lrange [split $query /] 0 end-1] /] ::set attribute [string range [lindex [split $query /] end] 1 end] foreach matchingNode [$node selectNodes $nodeQuery] { $matchingNode setAttribute $attribute $value } return $value } # For i.e. '$node unset {/employees/employee[1]}' calls proc xmlstruct::unsetnodes {nodes} { # This probably breaks if some nodes are descendents of each other and # they don't get deleted in the right order foreach node $nodes { $node delete } } # Determines if the given string is intended to be valid xml proc xmlstruct::isXml {string} { ::set string [string trim $string] if {([string index $string 0] == "<") && [string index $string end] == ">"} { return 1 } else { return 0 } } # For i.e. '$node set {/employees/employee[1]} value' calls proc xmlstruct::setnodes {targetNodes query value} { if {[xmlstruct::isXml $value]} { foreach target $targetNodes {xmlstruct::setxml $target $value} } else { foreach target $targetNodes {xmlstruct::settext $target $value} } } # TODO: don't allow this to be called for the documentElement node # (i.e. $obj set / "some text" should not be allowed) # For i.e. '$node set {/employees/employee/name} Bill' calls proc xmlstruct::settext {node text} { ::set doc [$node ownerDocument] foreach child [$node childNodes] {$child delete} if {[string length $text] > 0} { ::set textNode [$doc createTextNode $text] $node appendChild $textNode } return $text } # For i.e. '$node set {/employees/employee} <name>Bill</name>' calls proc xmlstruct::setxml {node xml} { foreach child [$node childNodes] {$child delete} $node appendXML $xml return $xml }
27sep2002 Jochen Loewer Excellent Brian! I think this XPath based updating of XML/DOM structures makes it even easier to generate/manipulate nested structures. I also read about a similar approach somewhere else. Do you mind if I add this to the next standard tDOM distribution? May be also coded in C?27Sep2002 Brian Theado - I think it would be great to have this functionality as part of the standard distribution. Use the above code however you like. Note the following limitations that I didn't previously document:
- Creating brand new elements/attributes isn't supported yet (when it is supported it should somehow be restricted to simple xpath i.e. without filters and without compound queries joined by '|'--really anything that can return multiple nodes/attributes) -- 29sep2002 - This is now supported (see xmlstruct::setnew)
- The functions xmlstruct::setattrs and xmlstruct::unsetattrs above parse the xpath query using split and is not bulletproof
- When passing an xml string to the set operation, the xml gets added as a child of the node specified. In the article above, the specified node gets replaced with the xml which is a better behavior IMO. -- 29sep2002 - Now that new elements and attributes and lappend is supported, I like the way it is better
% set x [xmlstruct::create <employees/>] % $x lappend employee Bill % $x set / <employees> <employee>Bill</employee> </employees> % $x lappend employee Joe % $x set / <employees> <employee>Bill</employee> <employee>Joe</employee> </employees>The lindex command would be unnecessary because XPath has built-in index operations (i.e. $x set employee[1]). The lreplace command is similarly unnecessary. Linsert would be useful though.Another idea is to see how functionality similar to trace could be adapted to this model.27sep02 jcw - Would it be an option to use ()'s for indexing - e.g. employee(1)? How about other variations, such as "." as path separator instead of "/"? Another thought - could this approach be used as foundation for different forms of structured data? Things like tdom and tcldom and metakit? Not to over-generalize or even make all notations and features work everywhere, but simply to introduce a way which helps people pick up conventions more quickly. There are so many conventions for nesting ('/', '.', '::', ',', ' ')...27sep02 Brian Theado - XPath (which is pretty much the core of XSL) has a specific syntax that already makes use of (off the top of my head) the symbols '/', '//', '.', '..', '::', '(', ')', '[', ']'. The parsing for XPath is already handled internally by tDOM. One of the nice things about the above code is that you get the full power of XPath (which I find very powerful). I suppose a simplified syntax would be possible that could unify the various forms of structured data, but XPath in its simplest form (using just the first 4 symbols listed above) is very much like unix filesystem access which a large number of people are already familiar with. Now, the article that got me started on this code [2] does use a '.' separator, so something like you suggest must be possible.27sep02 jcw - Thanks, also for the pointers. Hrm, if only the world were simpler: another example is 1-based vs. 0-based indexing. Oh well, still I find it most illuminating to see such simplifying approaches as the one you created on this page.27sep02 Jacob Levy: Would an XPath binding for e4Graph be something people would consider useful?
See also tclxml, tcldom, tdom