source: branches/blt4/lang/tcl/scripts/library.tcl @ 2936

Last change on this file since 2936 was 2936, checked in by gah, 12 years ago

sync back with trunk

File size: 34.6 KB
Line 
1# ----------------------------------------------------------------------
2#  COMPONENT: library - provides access to the XML library
3#
4#  These routines make it easy to load the XML description for a
5#  series of tool parameters and browse through the results.
6# ======================================================================
7#  AUTHOR:  Michael McLennan, Purdue University
8#  Copyright (c) 2004-2005  Purdue Research Foundation
9#
10#  See the file "license.terms" for information on usage and
11#  redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES.
12# ======================================================================
13package require tdom
14package require Itcl
15
16namespace eval Rappture {
17    variable stdlib ""
18}
19
20# load the object system along with the XML library code
21Rappture::objects::init
22encoding system utf-8
23
24# ----------------------------------------------------------------------
25# USAGE: library <file>
26# USAGE: library standard
27# USAGE: library isvalid <object>
28#
29# Used to open a <file> containing an XML description of tool
30# parameters.  Loads the file and returns the name of the LibraryObj
31# file that represents it.
32#
33# If you use the word "standard" in place of the file name, this
34# function returns the standard Rappture library object, which
35# contains material definitions.
36#
37# The isvalid operation checks an <object> to see if it is a valid
38# library object.  Returns 1 if so, and 0 otherwise.
39# ----------------------------------------------------------------------
40proc Rappture::library {args} {
41    # handle the isvalid operation...
42    if {[llength $args] > 1 && [lindex $args 0] == "isvalid"} {
43        if {[llength $args] != 2} {
44            error "wrong # args: should be \"library isvalid object\""
45        }
46        set obj [lindex $args 1]
47        #
48        # BE CAREFUL with the object test:
49        # The command should look like a LibraryObj formed by #auto.
50        # We want to avoid things like "split" or "set", which are
51        # valid Tcl commands but won't respond well to isa.
52        #
53        if {[regexp {libraryObj[0-9]+$} $obj]
54              && [catch {$obj isa ::Rappture::LibraryObj} valid] == 0
55              && $valid} {
56            return 1
57        }
58        return 0
59    }
60
61    if {[llength $args] != 1} {
62        error "wrong # args: should be \"library file\" or \"library isvalid object\""
63    }
64    set fname [lindex $args 0]
65
66    if {$fname == "standard"} {
67        variable stdlib
68        if {$stdlib != ""} {
69            return $stdlib
70        }
71        set fname [file join $Rappture::installdir lib library.xml]
72
73        set fid [::open $fname r]
74        set info [read $fid]
75        close $fid
76
77        set stdlib [Rappture::LibraryObj ::#auto $info]
78        return $stdlib
79    }
80
81    if {[regexp {^<\?[Xx][Mm][Ll]} $fname]} {
82        set info $fname
83    } else {
84        # otherwise, try to open the file and create its LibraryObj
85        set fid [::open $fname r]
86        set info [read $fid]
87        close $fid
88    }
89
90    set obj [Rappture::LibraryObj ::#auto $info]
91    return $obj
92}
93
94# ----------------------------------------------------------------------
95# USAGE: entities ?-as <fval>? <object> <path>
96#
97# Used to sift through an XML <object> for "entities" within the
98# Rappture description.  Entities are things like strings, numbers,
99# etc., which show up in the GUI as controls.
100#
101# Returns a list of all entities found beneath <path>.
102#
103# By default, this method returns the component name "type(id)".
104# This is changed by setting the -as argument to "id" (for name
105# of the tail element), to "type" (for the type of the tail element),
106# to "object" (for an object representing the DOM node referenced by
107# the path.
108# ----------------------------------------------------------------------
109proc Rappture::entities {args} {
110    array set params {
111        -as component
112    }
113    while {[llength $args] > 1} {
114        set first [lindex $args 0]
115        if {[string index $first 0] == "-"} {
116            set choices [array names params]
117            if {[lsearch $choices $first] < 0} {
118                error "bad option \"$first\": should be [join [lsort $choices] {, }]"
119            }
120            set params($first) [lindex $args 1]
121            set args [lrange $args 2 end]
122        } else {
123            break
124        }
125    }
126    if {[llength $args] > 2} {
127        error "wrong # args: should be \"entities ?-as fval? obj ?path?\""
128    }
129    set xmlobj [lindex $args 0]
130    set path [lindex $args 1]
131
132    set rlist ""
133    lappend queue $path
134    while {[llength $queue] > 0} {
135        set path [lindex $queue 0]
136        set queue [lrange $queue 1 end]
137
138        foreach cpath [$xmlobj children -as path $path] {
139            set type [$xmlobj element -as type $cpath]
140            switch -- $type {
141                group - phase {
142                    lappend queue $cpath
143                }
144                structure {
145                    # add this to the return list with the right flavor
146                    if {$params(-as) == "component"} {
147                        lappend rlist $cpath
148                    } else {
149                        lappend rlist [$xmlobj element -as $params(-as) $cpath]
150                    }
151
152                    if {[$xmlobj element $cpath.current.parameters] != ""} {
153                        lappend queue $cpath.current.parameters
154                    }
155                }
156                drawing {
157                    # add this to the return list with the right flavor
158                    if {$params(-as) == "component"} {
159                        lappend rlist $cpath
160                    } else {
161                        lappend rlist [$xmlobj element -as $params(-as) $cpath]
162                    }
163
164                    foreach child [$xmlobj children $cpath.current] {
165                        if {[string match about* $child]} {
166                            continue
167                        }
168                        if {[$xmlobj element $cpath.current.$child.parameters] != ""} {
169                            lappend queue $cpath.current.$child.parameters
170                        }
171                    }
172                }
173                default {
174                    if {[catch {Rappture::objects::get $type}] == 0} {
175                        # add this to the return list with the right flavor
176                        if {$params(-as) == "component"} {
177                            lappend rlist $cpath
178                        } else {
179                            lappend rlist [$xmlobj element -as $params(-as) $cpath]
180                        }
181                    }
182
183                    # if this element has embedded groups, add them to the queue
184                    foreach ccpath [$xmlobj children -as path $cpath] {
185                        set cctype [$xmlobj element -as type $ccpath]
186                        if {$cctype == "group" || $cctype == "phase"} {
187                            lappend queue $ccpath
188                        }
189                    }
190                }
191            }
192        }
193    }
194    return $rlist
195}
196
197# ----------------------------------------------------------------------
198itcl::class Rappture::LibraryObj {
199    constructor {info} { # defined below }
200    destructor { # defined below }
201
202    public method element {args}
203    public method parent {args}
204    public method children {args}
205    public method get {args}
206    public method put {args}
207    public method copy {path from args}
208    public method remove {{path ""}}
209    public method xml {{path ""}}
210
211    public method diff {libobj}
212    public proc value {libobj path}
213
214    public proc path2list {path}
215    protected method find {path}
216    protected method node2name {node}
217    protected method node2comp {node}
218    protected method node2path {node}
219    protected method childnodes {node type}
220
221    private variable _root 0       ;# non-zero => this obj owns document
222    private variable _document ""  ;# XML DOM tree
223    private variable _node ""      ;# node within
224}
225
226# ----------------------------------------------------------------------
227# CONSTRUCTOR
228# ----------------------------------------------------------------------
229itcl::body Rappture::LibraryObj::constructor {info} {
230    if {[regexp {<?[Xx][Mm][Ll]} $info]} {
231        set _root 1
232        set _document [dom parse $info]
233        set _node [$_document documentElement]
234    } elseif {[regexp {^domNode} $info]} {
235        set _root 0
236        set _document [$info ownerDocument]
237        set _node $info
238    } else {
239        error "bad info: should be XML text or DOM node"
240    }
241}
242
243# ----------------------------------------------------------------------
244# DESTRUCTOR
245# ----------------------------------------------------------------------
246itcl::body Rappture::LibraryObj::destructor {} {
247    if {$_root && $_document != ""} {
248        $_document delete
249    }
250}
251
252# ----------------------------------------------------------------------
253# USAGE: element ?-as <fval>? ?<path>?
254#
255# Clients use this to query a particular element within the entire
256# data structure.  The path is a string of the form
257# "structure.box(source).corner".  This example represents the tag
258# <corner> within a tag <box id="source"> within a tag <structure>,
259# which must be found at the top level within this document.
260#
261# By default, this method returns the component name "type(id)".
262# This is changed by setting the -as argument to "id" (for name
263# of the tail element), to "type" (for the type of the tail element),
264# to "object" (for an object representing the DOM node referenced by
265# the path).
266# ----------------------------------------------------------------------
267itcl::body Rappture::LibraryObj::element {args} {
268    array set params {
269        -as component
270    }
271    while {[llength $args] > 1} {
272        set first [lindex $args 0]
273        if {[string index $first 0] == "-"} {
274            set choices [array names params]
275            if {[lsearch $choices $first] < 0} {
276                error "bad option \"$first\": should be [join [lsort $choices] {, }]"
277            }
278            set params($first) [lindex $args 1]
279            set args [lrange $args 2 end]
280        } else {
281            break
282        }
283    }
284    if {[llength $args] > 1} {
285        error "wrong # args: should be \"element ?-as fval? ?path?\""
286    }
287    set path [lindex $args 0]
288
289    set node [find $path]
290    if {$node == ""} {
291        return ""
292    }
293
294    switch -- $params(-as) {
295      object {
296          return [::Rappture::LibraryObj ::#auto $node]
297      }
298      component {
299          return [node2comp $node]
300      }
301      id {
302          return [node2name $node]
303      }
304      path {
305          return [node2path $node]
306      }
307      type {
308          return [$node nodeName]
309      }
310      default {
311          error "bad flavor \"$params(-as)\": should be component, id, object, path, type"
312      }
313    }
314}
315
316# ----------------------------------------------------------------------
317# USAGE: parent ?-as <fval>? ?<path>?
318#
319# Clients use this to query the parent of a particular element.
320# This is just like the "element" method, but it returns the parent
321# of the element instead of the element itself.
322#
323# By default, this method returns a list of component names "type(id)".
324# This is changed by setting the -as argument to "id" (for tail
325# names of all children), to "type" (for the types of all children),
326# to "object" (for a list of objects representing the DOM nodes for
327# all children).
328# ----------------------------------------------------------------------
329itcl::body Rappture::LibraryObj::parent {args} {
330    array set params {
331        -as component
332    }
333    while {[llength $args] > 1} {
334        set first [lindex $args 0]
335        if {[string index $first 0] == "-"} {
336            set choices [array names params]
337            if {[lsearch $choices $first] < 0} {
338                error "bad option \"$first\": should be [join [lsort $choices] {, }]"
339            }
340            set params($first) [lindex $args 1]
341            set args [lrange $args 2 end]
342        } else {
343            break
344        }
345    }
346    if {[llength $args] > 1} {
347        error "wrong # args: should be \"parent ?-as fval? ?path?\""
348    }
349    set path [lindex $args 0]
350
351    set node [find $path]
352    if {$node == ""} {
353        return ""
354    }
355    set node [$node parentNode]
356    if {$node == ""} {
357        return ""
358    }
359
360    switch -- $params(-as) {
361      object {
362          return [::Rappture::LibraryObj ::#auto $node]
363      }
364      component {
365          return [node2comp $node]
366      }
367      id {
368          return [node2name $node]
369      }
370      path {
371          return [node2path $node]
372      }
373      type {
374          return [$node nodeName]
375      }
376      default {
377          error "bad flavor \"$params(-as)\": should be component, id, object, path, type"
378      }
379    }
380}
381
382# ----------------------------------------------------------------------
383# USAGE: children ?-as <fval>? ?-type <name>? ?<path>?
384#
385# Clients use this to query the children of a particular element
386# within the entire data structure.  This is just like the "element"
387# method, but it returns the children of the element instead of the
388# element itself.  If the optional -type argument is specified, then
389# the return list is restricted to children of the specified type.
390#
391# By default, this method returns a list of component names "type(id)".
392# This is changed by setting the -as argument to "id" (for tail
393# names of all children), to "type" (for the types of all children),
394# to "object" (for a list of objects representing the DOM nodes for
395# all children).
396# ----------------------------------------------------------------------
397itcl::body Rappture::LibraryObj::children {args} {
398    array set params {
399        -as component
400        -type ""
401    }
402    while {[llength $args] > 1} {
403        set first [lindex $args 0]
404        if {[string index $first 0] == "-"} {
405            set choices [array names params]
406            if {[lsearch $choices $first] < 0} {
407                error "bad option \"$first\": should be [join [lsort $choices] {, }]"
408            }
409            set params($first) [lindex $args 1]
410            set args [lrange $args 2 end]
411        } else {
412            break
413        }
414    }
415    if {[llength $args] > 1} {
416        error "wrong # args: should be \"children ?-as fval? ?-type name? ?path?\""
417    }
418    set path [lindex $args 0]
419    set node [find $path]
420    if {$node == ""} {
421        return ""
422    }
423
424    set nlist ""
425    foreach n [$node childNodes] {
426        set type [$n nodeName]
427        if {[regexp {^#} $type]} {
428            continue
429        }
430        if {$params(-type) != "" && $params(-type) != $type} {
431            continue
432        }
433        lappend nlist $n
434    }
435
436    set rlist ""
437    switch -- $params(-as) {
438      object {
439          foreach n $nlist {
440              lappend rlist [::Rappture::LibraryObj ::#auto $n]
441          }
442      }
443      component {
444          foreach n $nlist {
445              lappend rlist [node2comp $n]
446          }
447      }
448      id {
449          foreach n $nlist {
450              lappend rlist [node2name $n]
451          }
452      }
453      path {
454          foreach n $nlist {
455              lappend rlist [node2path $n]
456          }
457      }
458      type {
459          foreach n $nlist {
460              lappend rlist [$n nodeName]
461          }
462      }
463      default {
464          error "bad flavor \"$params(-as)\": should be component, id, object, path, type"
465      }
466    }
467    return $rlist
468}
469
470# ----------------------------------------------------------------------
471# USAGE: get ?-decode yes? ?<path>?
472#
473# Clients use this to query the value of a node. Clients can specify
474# if they want the data to be automatically decoded or no using the
475# -decode flag. This is useful for situations where you want to keep
476# the data encoded to pass to another system, like dx data in fields
477# sending data to nanovis. If the path is not
478# specified, it returns the value associated with the root node.
479# Otherwise, it returns the value for the element specified by the
480# path.
481# ----------------------------------------------------------------------
482itcl::body Rappture::LibraryObj::get {args} {
483    array set params {
484        -decode yes
485    }
486    while {[llength $args] > 0} {
487        set first [lindex $args 0]
488        if {[string index $first 0] == "-"} {
489            set choices [array names params]
490            if {[lsearch $choices $first] < 0} {
491                error "bad option \"$first\": should be [join [lsort $choices] {, }]"
492            }
493            set params($first) [lindex $args 1]
494            set args [lrange $args 2 end]
495        } else {
496            break
497        }
498    }
499    if {[llength $args] > 1} {
500        error "wrong # args: should be \"get ?-decode yes? ?path?\""
501    }
502    if {[llength $args] == 1} {
503        set path [lindex $args 0]
504    } else {
505        set path ""
506    }
507
508    set node [find $path]
509    if {$node == ""} {
510        return ""
511    }
512    set string [$node text]
513    # Expat (via tDOM) will always produce utf-8 output, regardless of the
514    # encoding of the tool.xml.  We need to convert utf-8 to unicode for
515    # Tcl/Tk.  This shouldn't affect ASCII tool.xml files.
516    #puts stderr "before $path=$string"
517    #set string [encoding convertfrom utf-8 $string]
518    #puts stderr "after $path=$string [string length $string]"
519    #Rappture::encoding::debug $string
520    if {$params(-decode) == "yes"} {
521        set string [Rappture::encoding::decode -- $string]
522    }
523    puts stderr "returning $path=$string"
524    return [string trim $string]
525}
526
527# ----------------------------------------------------------------------
528# USAGE: put ?-append yes? ?-id num? ?-type string|file? ?-compress no? ?<path>? <string>
529#
530# Clients use this to set the value of a node.  If the path is not
531# specified, it sets the value for the root node.  Otherwise, it sets
532# the value for the element specified by the path.  If the value is a
533# string, then it is treated as the text within the tag at the tail
534# of the path.  If it is a DOM node or a library, then it is inserted
535# into the tree at the specified path.
536#
537# If the optional id is specified, then it sets the identifier for
538# the tag at the tail of the path.  If the optional append flag is
539# specified, then the value is appended to the current value.
540# Otherwise, the value replaces the current value.
541# ----------------------------------------------------------------------
542itcl::body Rappture::LibraryObj::put {args} {
543    array set params {
544        -id ""
545        -append no
546        -type string
547        -compress no
548    }
549    while {[llength $args] > 1} {
550        set first [lindex $args 0]
551        if {[string index $first 0] == "-"} {
552            set choices [array names params]
553            if {[lsearch $choices $first] < 0} {
554                error "bad option \"$first\": should be [join [lsort $choices] {, }]"
555            }
556            set params($first) [lindex $args 1]
557            set args [lrange $args 2 end]
558        } else {
559            break
560        }
561    }
562    if {[llength $args] < 1 || [llength $args] > 2} {
563        error "wrong # args: should be \"put ?-append bval? ?-id num? ?-type string|file? ?-compress bval? ?path? string\""
564    }
565    if {[llength $args] == 2} {
566        set path [lindex $args 0]
567        set str [lindex $args 1]
568    } else {
569        set path ""
570        set str [lindex $args 0]
571    }
572
573    if {$params(-type) == "file"} {
574        set fileName $str
575        set fid [open $fileName r]
576        fconfigure $fid -translation binary -encoding binary
577        set str [read $fid]
578        close $fid
579    }
580
581    if {$params(-compress) || [Rappture::encoding::is binary $str]} {
582        set str [Rappture::encoding::encode -- $str]
583    }
584
585    set node [find -create $path]
586
587    #
588    # Clean up any nodes that don't belong.  If we're appending
589    # the value, then clean up only child <tag> nodes.  Otherwise,
590    # clean up all nodes.
591    #
592    set nlist ""
593    if {$params(-append)} {
594        foreach n [$node childNodes] {
595            if {[$n nodeType] != "TEXT_NODE"} {
596                lappend nlist $n
597            }
598        }
599    } else {
600        set nlist [$node childNodes]
601    }
602    foreach n $nlist {
603        $n delete
604    }
605
606    if {[Rappture::library isvalid $str]} {
607        foreach n [[$str info variable _node -value] childNodes] {
608            if { [$n nodeType] == "COMMENT_NODE" } {
609                continue
610            }
611            $node appendXML [$n asXML]
612        }
613    } else {
614        set n [$_document createText $str]
615        $node appendChild $n
616        if {"" != $params(-id)} {
617            $node setAttribute id $params(-id)
618        }
619    }
620    return ""
621}
622
623# ----------------------------------------------------------------------
624# USAGE: copy <path> from ?<xmlobj>? <path>
625#
626# Clients use this to copy the value from one xmlobj/path to another.
627# If the <xmlobj> is not specified, it is assumed to be the same as
628# the current object.
629# ----------------------------------------------------------------------
630itcl::body Rappture::LibraryObj::copy {path from args} {
631    if {[llength $args] == 1} {
632        set xmlobj $this
633        set fpath [lindex $args 0]
634    } elseif {[llength $args] == 2} {
635        set xmlobj [lindex $args 0]
636        set fpath [lindex $args 1]
637    } else {
638        error "wrong # args: should be \"copy path from ?xmlobj? path\""
639    }
640    if {$from != "from"} {
641        error "bad syntax: should be \"copy path from ?xmlobj? path\""
642    }
643
644    if {[llength [$xmlobj children $fpath]] == 0} {
645        set val [$xmlobj get $fpath]
646        put $path $val
647    } else {
648        set obj [$xmlobj element -as object $fpath]
649        put $path $obj
650        itcl::delete object $obj
651    }
652}
653
654# ----------------------------------------------------------------------
655# USAGE: remove ?<path>?
656#
657# Clients use this to remove the specified node.  Removes the node
658# from the tree.
659# ----------------------------------------------------------------------
660itcl::body Rappture::LibraryObj::remove {{path ""}} {
661    set node [find $path]
662    if {$node != ""} {
663        $node delete
664    }
665}
666
667# ----------------------------------------------------------------------
668# USAGE: xml ?<path>?
669#
670# Returns a string representing the XML information for the information
671# in this library.
672# ----------------------------------------------------------------------
673itcl::body Rappture::LibraryObj::xml {{path ""}} {
674    if {"" != $path} {
675        set n [find $path]
676    } else {
677        set n $_node
678    }
679    return [$n asXML]
680}
681
682# ----------------------------------------------------------------------
683# USAGE: diff <libobj>
684#
685# Compares the entities in this object to those in another and
686# returns a list of differences.  The result is a list of the form:
687# {op1 path1 oldval1 newval1 ...} where each "op" is +/-/c for
688# added/subtracted/changed, "path" is the path within the library
689# that is different, and "oldval"/"newval" give the values for the
690# object at the path.
691# ----------------------------------------------------------------------
692itcl::body Rappture::LibraryObj::diff {libobj} {
693    set rlist ""
694
695    # query the values for all entities in both objects
696    set thisv [Rappture::entities $this input]
697    set otherv [Rappture::entities $libobj input]
698
699    # scan through values for this object, and compare against other one
700    foreach path $thisv {
701        set i [lsearch -exact $otherv $path]
702        if {$i < 0} {
703            foreach {raw norm} [value $this $path] break
704            lappend rlist - $path $raw ""
705        } else {
706            foreach {traw tnorm} [value $this $path] break
707            foreach {oraw onorm} [value $libobj $path] break
708            if {![string equal $tnorm $onorm]} {
709                lappend rlist c $path $traw $oraw
710            }
711            set otherv [lreplace $otherv $i $i]
712        }
713    }
714
715    # add any values left over in the other object
716    foreach path $otherv {
717        foreach {oraw onorm} [value $libobj $path] break
718        lappend rlist + $path "" $oraw
719    }
720    return $rlist
721}
722
723# ----------------------------------------------------------------------
724# USAGE: value <object> <path>
725#
726# Used to query the "value" associated with the <path> in an XML
727# <object>.  This is a little more complicated than the object's
728# "get" method.  It handles things like structures and values
729# with normalized units.
730#
731# Returns a list of two items:  {raw norm} where "raw" is the raw
732# value from the "get" method and "norm" is the normalized value
733# produced by this routine.  Example:  {300K 300}
734#
735# Right now, it is a handy little utility used by the "diff" method.
736# Eventually, it should be moved to a better object-oriented
737# implementation, where each Rappture type could overload the
738# various bits of processing below.  So we leave it as a "proc"
739# now instead of a method, since it should be deprecated soon.
740# ----------------------------------------------------------------------
741itcl::body Rappture::LibraryObj::value {libobj path} {
742    switch -- [$libobj element -as type $path] {
743        structure {
744            set raw $path
745            # try to find a label to represent the structure
746            set val [$libobj get $path.about.label]
747            if {"" == $val} {
748                set val [$libobj get $path.current.about.label]
749            }
750            if {"" == $val} {
751                if {[$libobj element $path.current] != ""} {
752                    set comps [$libobj children $path.current.components]
753                    set val "<structure> with [llength $comps] components"
754                } else {
755                    set val "<structure>"
756                }
757            }
758            return [list $raw $val]
759        }
760        number {
761            # get the usual value...
762            set raw ""
763            set val ""
764            if {"" != [$libobj element $path.current]} {
765                set raw [$libobj get $path.current]
766            } elseif {"" != [$libobj element $path.default]} {
767                set raw [$libobj get $path.default]
768            }
769            if {"" != $raw} {
770                set val $raw
771                # then normalize to default units
772                set units [$libobj get $path.units]
773                if {"" != $units} {
774                    set val [Rappture::Units::convert $val \
775                        -context $units -to $units -units off]
776                }
777            }
778            return [list $raw $val]
779        }
780    }
781
782    # for all other types, get the value (current, or maybe default)
783    set raw ""
784    if {"" != [$libobj element $path.current]} {
785        set raw [$libobj get $path.current]
786    } elseif {"" != [$libobj element $path.default]} {
787        set raw [$libobj get $path.default]
788    }
789    return [list $raw $raw]
790}
791
792# ----------------------------------------------------------------------
793# USAGE: find ?-create? <path>
794#
795# Used internally to find a particular element within the root node
796# according to the path, which is a string of the form
797# "typeNN(id).typeNN(id). ...", where each "type" is a tag <type>;
798# if the optional NN is specified, it indicates an index for the
799# <type> tag within its parent; if the optional (id) part is included,
800# it indicates a tag of the form <type id="id">.
801#
802# By default, it looks for an element along the path and returns None
803# if not found.  If the create flag is set, it creates various elements
804# along the path as it goes.  This is useful for "put" operations.
805#
806# If you include "#" instead of a specific number, a node will be
807# created automatically with a new number.  For example, the path
808# "foo.bar#" called the first time will create "foo.bar", the second
809# time "foo.bar1", the third time "foo.bar2" and so forth.
810#
811# Returns an object representing the element indicated by the path,
812# or "" if the path is not found.
813# ----------------------------------------------------------------------
814itcl::body Rappture::LibraryObj::find {args} {
815    set create 0
816    while {[llength $args] > 1} {
817        set first [lindex $args 0]
818        set args [lrange $args 1 end]
819        if {$first == "-create"} {
820            set create 1
821        } else {
822            error "bad option \"$first\": should be -create"
823        }
824    }
825    if {[llength $args] != 1} {
826        error "wrong # args: should be \"find ?-create? path\""
827    }
828    set path [lindex $args 0]
829
830    if {$path == ""} {
831        return $_node
832    }
833    set path [path2list $path]
834
835    #
836    # Follow the given path and look for all of the parts.
837    #
838    set lastnode $_node
839    set node $lastnode
840    foreach part $path {
841        if {![regexp {^(([a-zA-Z0-9_]*[a-zA-Z_]+#?)([0-9]*))?(\((.*)\))?$} $part \
842               match dummy type index dummy name]} {
843            error "bad path component \"$part\""
844        }
845        #
846        # If the name is like "type2", then look for elements with
847        # the type name and return the one with the given index.
848        # If the name is like "type", then assume the index is 0.
849        #
850        if {$name == ""} {
851            if {$index == ""} {
852                set index 0
853            }
854            set nlist [childnodes $node $type]
855            set node [lindex $nlist $index]
856        } else {
857            #
858            # If the name is like "type(id)", then look for elements
859            # that match the type and see if one has the requested name.
860            # if the name is like "(id)", then look for any elements
861            # with the requested name.
862            #
863            if {$type != ""} {
864                set nlist [childnodes $node $type]
865            } else {
866                set nlist [$node childNodes]
867            }
868            set found 0
869            foreach n $nlist {
870                if {[catch {$n getAttribute id} tag]} { set tag "" }
871                if {$tag == $name} {
872                    set found 1
873                    break
874                }
875            }
876            set node [expr {($found) ? $n : ""}]
877        }
878
879        if {$node == ""} {
880            if {!$create} {
881                return ""
882            }
883
884            #
885            # If the "create" flag is set, then create a node
886            # with the specified "type(id)" and continue on.
887            # If the type is "type#", then create a node with
888            # an automatic number.
889            #
890            if {![regexp {^([^\(]+)\(([^\)]+)\)$} $part match type name]} {
891                set type $part
892                set name ""
893            }
894
895            if {[string match *# $type]} {
896                set type [string trimright $type #]
897                set node [$_document createElement $type]
898
899                # find the last node of same type and append there
900                set pos ""
901                foreach n [$lastnode childNodes] {
902                    if {[$n nodeName] == $type} {
903                        set pos $n
904                    }
905                }
906                if {$pos != ""} {
907                    set pos [$pos nextSibling]
908                }
909                if {$pos != ""} {
910                    $lastnode insertBefore $node $pos
911                } else {
912                    $lastnode appendChild $node
913                }
914            } else {
915                set node [$_document createElement $type]
916                $lastnode appendChild $node
917            }
918            if {"" != $name} {
919                $node setAttribute id $name
920            }
921        }
922        set lastnode $node
923    }
924    return $node
925}
926
927# ----------------------------------------------------------------------
928# USAGE: path2list <path>
929#
930# Converts a path of the form "foo(a).bar.baz" into a list of the
931# form "foo(a) bar baz".  This is a little more complicated than
932# splitting on the .'s, since the stuff in ()'s might have embedded
933# .'s.  Returns a proper Tcl list for all elements of the path.
934# ----------------------------------------------------------------------
935itcl::body Rappture::LibraryObj::path2list {path} {
936    #
937    # Normally, we just split on .'s within the path.  But there
938    # might be some .'s embedded within ()'s in the path.  Change
939    # any embedded .'s to an out-of-band character, then split on
940    # the .'s, and change the embedded .'s back.
941    #
942    if {[regexp {(\([^\)]*)\.([^\)]*\))} $path]} {
943        while {[regsub -all {(\([^\)]*)\.([^\)]*\))} $path "\\1\007\\2" path]} {
944            # keep fixing...
945        }
946    }
947    set path [split $path .]
948    regsub -all {\007} $path {.} path
949
950    return $path
951}
952
953# ----------------------------------------------------------------------
954# USAGE: node2name <node>
955#
956# Used internally to create a name for the specified node.  If the
957# node doesn't have a specific name ("id" attribute) then a name of
958# the form "type123" is constructed.
959# ----------------------------------------------------------------------
960itcl::body Rappture::LibraryObj::node2name {node} {
961    if {[catch {$node getAttribute id} name]} { set name "" }
962    if {$name == ""} {
963        set pnode [$node parentNode]
964        if {$pnode == ""} {
965            return ""
966        }
967        set type [$node nodeName]
968        set siblings [childnodes $pnode $type]
969        set index [lsearch $siblings $node]
970        if {$index == 0} {
971            set name $type
972        } else {
973            set name "$type$index"
974        }
975    }
976    return $name
977}
978
979# ----------------------------------------------------------------------
980# USAGE: node2comp <node>
981#
982# Used internally to create a path component name for the specified
983# node.  A path component name has the form "type(id)" or just
984# "type##" if the node doesn't have a name.  This name can be used
985# in a path to uniquely address the component.
986# ----------------------------------------------------------------------
987itcl::body Rappture::LibraryObj::node2comp {node} {
988    set type [$node nodeName]
989    if {[catch {$node getAttribute id} name]} { set name "" }
990    if {$name == ""} {
991        set pnode [$node parentNode]
992        if {$pnode == ""} {
993            return ""
994        }
995        set siblings [childnodes $pnode $type]
996        set index [lsearch $siblings $node]
997        if {$index == 0} {
998            set name $type
999        } else {
1000            set name "$type$index"
1001        }
1002    } else {
1003        set name "${type}($name)"
1004    }
1005    return $name
1006}
1007
1008# ----------------------------------------------------------------------
1009# USAGE: node2path <node>
1010#
1011# Used internally to create a full path name for the specified node.
1012# The path is relative to the current object, so it stops when the
1013# parent is the root node for this object.
1014# ----------------------------------------------------------------------
1015itcl::body Rappture::LibraryObj::node2path {node} {
1016    set path [node2comp $node]
1017    set node [$node parentNode]
1018    while {$node != "" && $node != $_node} {
1019        set path "[node2comp $node].$path"
1020        set node [$node parentNode]
1021    }
1022    return $path
1023}
1024
1025# ----------------------------------------------------------------------
1026# USAGE: childnodes <node> <type>
1027#
1028# Used internally to return a list of children for the given <node>
1029# that match a specified <type>.  Similar to XML getElementsByTagName,
1030# but returns only direct children of the <node>.
1031# ----------------------------------------------------------------------
1032itcl::body Rappture::LibraryObj::childnodes {node type} {
1033    set rlist ""
1034    foreach cnode [$node childNodes] {
1035        if {[$cnode nodeName] == $type} {
1036            lappend rlist $cnode
1037        }
1038    }
1039    return $rlist
1040}
Note: See TracBrowser for help on using the repository browser.