source: trunk/lang/tcl/scripts/library.tcl @ 3673

Last change on this file since 3673 was 3673, checked in by mmc, 11 years ago

Added "string trim" back into the "get" function on the Tcl part
of the Rappture::library reader. This should fix many problems
that we were having with raw strings (e.g., leading and trailing
spaces on <layout>wizard</layout> was causing tools to go back
into side-by-side mode, choice values with leading/trailing space
seemed like unknown values, etc.).

File size: 34.2 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-2012  HUBzero Foundation, LLC
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 [string trim [$node text]]
513    if {$params(-decode)} {
514        set string [Rappture::encoding::decode -- $string]
515    }
516    return $string
517}
518
519# ----------------------------------------------------------------------
520# USAGE: put ?-append yes? ?-id num? ?-type string|file? ?-compress no? ?<path>? <string>
521#
522# Clients use this to set the value of a node.  If the path is not
523# specified, it sets the value for the root node.  Otherwise, it sets
524# the value for the element specified by the path.  If the value is a
525# string, then it is treated as the text within the tag at the tail
526# of the path.  If it is a DOM node or a library, then it is inserted
527# into the tree at the specified path.
528#
529# If the optional id is specified, then it sets the identifier for
530# the tag at the tail of the path.  If the optional append flag is
531# specified, then the value is appended to the current value.
532# Otherwise, the value replaces the current value.
533# ----------------------------------------------------------------------
534itcl::body Rappture::LibraryObj::put {args} {
535    array set params {
536        -id ""
537        -append no
538        -type string
539        -compress no
540    }
541    while {[llength $args] > 1} {
542        set first [lindex $args 0]
543        if {[string index $first 0] == "-"} {
544            set choices [array names params]
545            if {[lsearch $choices $first] < 0} {
546                error "bad option \"$first\": should be [join [lsort $choices] {, }]"
547            }
548            set params($first) [lindex $args 1]
549            set args [lrange $args 2 end]
550        } else {
551            break
552        }
553    }
554    if {[llength $args] < 1 || [llength $args] > 2} {
555        error "wrong # args: should be \"put ?-append bval? ?-id num? ?-type string|file? ?-compress bval? ?path? string\""
556    }
557    if {[llength $args] == 2} {
558        set path [lindex $args 0]
559        set str [lindex $args 1]
560    } else {
561        set path ""
562        set str [lindex $args 0]
563    }
564
565    if {$params(-type) == "file"} {
566        set fileName $str
567        set fid [open $fileName r]
568        fconfigure $fid -translation binary -encoding binary
569        set str [read $fid]
570        close $fid
571    }
572
573    if {$params(-compress) || [Rappture::encoding::is binary $str]} {
574        set str [Rappture::encoding::encode -- $str]
575    }
576
577    set node [find -create $path]
578
579    #
580    # Clean up any nodes that don't belong.  If we're appending
581    # the value, then clean up only child <tag> nodes.  Otherwise,
582    # clean up all nodes.
583    #
584    set nlist ""
585    if {$params(-append)} {
586        foreach n [$node childNodes] {
587            if {[$n nodeType] != "TEXT_NODE"} {
588                lappend nlist $n
589            }
590        }
591    } else {
592        set nlist [$node childNodes]
593    }
594    foreach n $nlist {
595        $n delete
596    }
597
598    if {[Rappture::library isvalid $str]} {
599        foreach n [[$str info variable _node -value] childNodes] {
600            if { [$n nodeType] == "COMMENT_NODE" } {
601                continue
602            }
603            $node appendXML [$n asXML]
604        }
605    } else {
606        set n [$_document createText $str]
607        $node appendChild $n
608        if {"" != $params(-id)} {
609            $node setAttribute id $params(-id)
610        }
611    }
612    return ""
613}
614
615# ----------------------------------------------------------------------
616# USAGE: copy <path> from ?<xmlobj>? <path>
617#
618# Clients use this to copy the value from one xmlobj/path to another.
619# If the <xmlobj> is not specified, it is assumed to be the same as
620# the current object.
621# ----------------------------------------------------------------------
622itcl::body Rappture::LibraryObj::copy {path from args} {
623    if {[llength $args] == 1} {
624        set xmlobj $this
625        set fpath [lindex $args 0]
626    } elseif {[llength $args] == 2} {
627        set xmlobj [lindex $args 0]
628        set fpath [lindex $args 1]
629    } else {
630        error "wrong # args: should be \"copy path from ?xmlobj? path\""
631    }
632    if {$from != "from"} {
633        error "bad syntax: should be \"copy path from ?xmlobj? path\""
634    }
635
636    if {[llength [$xmlobj children $fpath]] == 0} {
637        set val [$xmlobj get $fpath]
638        put $path $val
639    } else {
640        set obj [$xmlobj element -as object $fpath]
641        put $path $obj
642        itcl::delete object $obj
643    }
644}
645
646# ----------------------------------------------------------------------
647# USAGE: remove ?<path>?
648#
649# Clients use this to remove the specified node.  Removes the node
650# from the tree.
651# ----------------------------------------------------------------------
652itcl::body Rappture::LibraryObj::remove {{path ""}} {
653    set node [find $path]
654    if {$node != ""} {
655        $node delete
656    }
657}
658
659# ----------------------------------------------------------------------
660# USAGE: xml ?<path>?
661#
662# Returns a string representing the XML information for the information
663# in this library.
664# ----------------------------------------------------------------------
665itcl::body Rappture::LibraryObj::xml {{path ""}} {
666    if {"" != $path} {
667        set n [find $path]
668    } else {
669        set n $_node
670    }
671    return [$n asXML]
672}
673
674# ----------------------------------------------------------------------
675# USAGE: diff <libobj>
676#
677# Compares the entities in this object to those in another and
678# returns a list of differences.  The result is a list of the form:
679# {op1 path1 oldval1 newval1 ...} where each "op" is +/-/c for
680# added/subtracted/changed, "path" is the path within the library
681# that is different, and "oldval"/"newval" give the values for the
682# object at the path.
683# ----------------------------------------------------------------------
684itcl::body Rappture::LibraryObj::diff {libobj} {
685    set rlist ""
686
687    # query the values for all entities in both objects
688    set thisv [Rappture::entities $this input]
689    set otherv [Rappture::entities $libobj input]
690
691    # scan through values for this object, and compare against other one
692    foreach path $thisv {
693        set i [lsearch -exact $otherv $path]
694        if {$i < 0} {
695            foreach {raw norm} [value $this $path] break
696            lappend rlist - $path $raw ""
697        } else {
698            foreach {traw tnorm} [value $this $path] break
699            foreach {oraw onorm} [value $libobj $path] break
700            if {![string equal $tnorm $onorm]} {
701                lappend rlist c $path $traw $oraw
702            }
703            set otherv [lreplace $otherv $i $i]
704        }
705    }
706
707    # add any values left over in the other object
708    foreach path $otherv {
709        foreach {oraw onorm} [value $libobj $path] break
710        lappend rlist + $path "" $oraw
711    }
712    return $rlist
713}
714
715# ----------------------------------------------------------------------
716# USAGE: value <object> <path>
717#
718# Used to query the "value" associated with the <path> in an XML
719# <object>.  This is a little more complicated than the object's
720# "get" method.  It handles things like structures and values
721# with normalized units.
722#
723# Returns a list of two items:  {raw norm} where "raw" is the raw
724# value from the "get" method and "norm" is the normalized value
725# produced by this routine.  Example:  {300K 300}
726#
727# Right now, it is a handy little utility used by the "diff" method.
728# Eventually, it should be moved to a better object-oriented
729# implementation, where each Rappture type could overload the
730# various bits of processing below.  So we leave it as a "proc"
731# now instead of a method, since it should be deprecated soon.
732# ----------------------------------------------------------------------
733itcl::body Rappture::LibraryObj::value {libobj path} {
734    switch -- [$libobj element -as type $path] {
735        structure {
736            set raw $path
737            # try to find a label to represent the structure
738            set val [$libobj get $path.about.label]
739            if {"" == $val} {
740                set val [$libobj get $path.current.about.label]
741            }
742            if {"" == $val} {
743                if {[$libobj element $path.current] != ""} {
744                    set comps [$libobj children $path.current.components]
745                    set val "<structure> with [llength $comps] components"
746                } else {
747                    set val "<structure>"
748                }
749            }
750            return [list $raw $val]
751        }
752        number {
753            # get the usual value...
754            set raw ""
755            set val ""
756            if {"" != [$libobj element $path.current]} {
757                set raw [$libobj get $path.current]
758            } elseif {"" != [$libobj element $path.default]} {
759                set raw [$libobj get $path.default]
760            }
761            if {"" != $raw} {
762                set val $raw
763                # then normalize to default units
764                set units [$libobj get $path.units]
765                if {"" != $units} {
766                    set val [Rappture::Units::convert $val \
767                        -context $units -to $units -units off]
768                }
769            }
770            return [list $raw $val]
771        }
772    }
773
774    # for all other types, get the value (current, or maybe default)
775    set raw ""
776    if {"" != [$libobj element $path.current]} {
777        set raw [$libobj get $path.current]
778    } elseif {"" != [$libobj element $path.default]} {
779        set raw [$libobj get $path.default]
780    }
781    return [list $raw $raw]
782}
783
784# ----------------------------------------------------------------------
785# USAGE: find ?-create? <path>
786#
787# Used internally to find a particular element within the root node
788# according to the path, which is a string of the form
789# "typeNN(id).typeNN(id). ...", where each "type" is a tag <type>;
790# if the optional NN is specified, it indicates an index for the
791# <type> tag within its parent; if the optional (id) part is included,
792# it indicates a tag of the form <type id="id">.
793#
794# By default, it looks for an element along the path and returns None
795# if not found.  If the create flag is set, it creates various elements
796# along the path as it goes.  This is useful for "put" operations.
797#
798# If you include "#" instead of a specific number, a node will be
799# created automatically with a new number.  For example, the path
800# "foo.bar#" called the first time will create "foo.bar", the second
801# time "foo.bar1", the third time "foo.bar2" and so forth.
802#
803# Returns an object representing the element indicated by the path,
804# or "" if the path is not found.
805# ----------------------------------------------------------------------
806itcl::body Rappture::LibraryObj::find {args} {
807    set create 0
808    while {[llength $args] > 1} {
809        set first [lindex $args 0]
810        set args [lrange $args 1 end]
811        if {$first == "-create"} {
812            set create 1
813        } else {
814            error "bad option \"$first\": should be -create"
815        }
816    }
817    if {[llength $args] != 1} {
818        error "wrong # args: should be \"find ?-create? path\""
819    }
820    set path [lindex $args 0]
821
822    if {$path == ""} {
823        return $_node
824    }
825    set path [path2list $path]
826
827    #
828    # Follow the given path and look for all of the parts.
829    #
830    set lastnode $_node
831    set node $lastnode
832    foreach part $path {
833        if {![regexp {^(([a-zA-Z0-9_]*[a-zA-Z_]+#?)([0-9]*))?(\((.*)\))?$} $part \
834               match dummy type index dummy name]} {
835            error "bad path component \"$part\""
836        }
837        #
838        # If the name is like "type2", then look for elements with
839        # the type name and return the one with the given index.
840        # If the name is like "type", then assume the index is 0.
841        #
842        if {$name == ""} {
843            if {$index == ""} {
844                set index 0
845            }
846            set nlist [childnodes $node $type]
847            set node [lindex $nlist $index]
848        } else {
849            #
850            # If the name is like "type(id)", then look for elements
851            # that match the type and see if one has the requested name.
852            # if the name is like "(id)", then look for any elements
853            # with the requested name.
854            #
855            if {$type != ""} {
856                set nlist [childnodes $node $type]
857            } else {
858                set nlist [$node childNodes]
859            }
860            set found 0
861            foreach n $nlist {
862                if {[catch {$n getAttribute id} tag]} { set tag "" }
863                if {$tag == $name} {
864                    set found 1
865                    break
866                }
867            }
868            set node [expr {($found) ? $n : ""}]
869        }
870
871        if {$node == ""} {
872            if {!$create} {
873                return ""
874            }
875
876            #
877            # If the "create" flag is set, then create a node
878            # with the specified "type(id)" and continue on.
879            # If the type is "type#", then create a node with
880            # an automatic number.
881            #
882            if {![regexp {^([^\(]+)\(([^\)]+)\)$} $part match type name]} {
883                set type $part
884                set name ""
885            }
886
887            if {[string match *# $type]} {
888                set type [string trimright $type #]
889                set node [$_document createElement $type]
890
891                # find the last node of same type and append there
892                set pos ""
893                foreach n [$lastnode childNodes] {
894                    if {[$n nodeName] == $type} {
895                        set pos $n
896                    }
897                }
898                if {$pos != ""} {
899                    set pos [$pos nextSibling]
900                }
901                if {$pos != ""} {
902                    $lastnode insertBefore $node $pos
903                } else {
904                    $lastnode appendChild $node
905                }
906            } else {
907                set node [$_document createElement $type]
908                $lastnode appendChild $node
909            }
910            if {"" != $name} {
911                $node setAttribute id $name
912            }
913        }
914        set lastnode $node
915    }
916    return $node
917}
918
919# ----------------------------------------------------------------------
920# USAGE: path2list <path>
921#
922# Converts a path of the form "foo(a).bar.baz" into a list of the
923# form "foo(a) bar baz".  This is a little more complicated than
924# splitting on the .'s, since the stuff in ()'s might have embedded
925# .'s.  Returns a proper Tcl list for all elements of the path.
926# ----------------------------------------------------------------------
927itcl::body Rappture::LibraryObj::path2list {path} {
928    #
929    # Normally, we just split on .'s within the path.  But there
930    # might be some .'s embedded within ()'s in the path.  Change
931    # any embedded .'s to an out-of-band character, then split on
932    # the .'s, and change the embedded .'s back.
933    #
934    if {[regexp {(\([^\)]*)\.([^\)]*\))} $path]} {
935        while {[regsub -all {(\([^\)]*)\.([^\)]*\))} $path "\\1\007\\2" path]} {
936            # keep fixing...
937        }
938    }
939    set path [split $path .]
940    regsub -all {\007} $path {.} path
941
942    return $path
943}
944
945# ----------------------------------------------------------------------
946# USAGE: node2name <node>
947#
948# Used internally to create a name for the specified node.  If the
949# node doesn't have a specific name ("id" attribute) then a name of
950# the form "type123" is constructed.
951# ----------------------------------------------------------------------
952itcl::body Rappture::LibraryObj::node2name {node} {
953    if {[catch {$node getAttribute id} name]} { set name "" }
954    if {$name == ""} {
955        set pnode [$node parentNode]
956        if {$pnode == ""} {
957            return ""
958        }
959        set type [$node nodeName]
960        set siblings [childnodes $pnode $type]
961        set index [lsearch $siblings $node]
962        if {$index == 0} {
963            set name $type
964        } else {
965            set name "$type$index"
966        }
967    }
968    return $name
969}
970
971# ----------------------------------------------------------------------
972# USAGE: node2comp <node>
973#
974# Used internally to create a path component name for the specified
975# node.  A path component name has the form "type(id)" or just
976# "type##" if the node doesn't have a name.  This name can be used
977# in a path to uniquely address the component.
978# ----------------------------------------------------------------------
979itcl::body Rappture::LibraryObj::node2comp {node} {
980    set type [$node nodeName]
981    if {[catch {$node getAttribute id} name]} { set name "" }
982    if {$name == ""} {
983        set pnode [$node parentNode]
984        if {$pnode == ""} {
985            return ""
986        }
987        set siblings [childnodes $pnode $type]
988        set index [lsearch $siblings $node]
989        if {$index == 0} {
990            set name $type
991        } else {
992            set name "$type$index"
993        }
994    } else {
995        set name "${type}($name)"
996    }
997    return $name
998}
999
1000# ----------------------------------------------------------------------
1001# USAGE: node2path <node>
1002#
1003# Used internally to create a full path name for the specified node.
1004# The path is relative to the current object, so it stops when the
1005# parent is the root node for this object.
1006# ----------------------------------------------------------------------
1007itcl::body Rappture::LibraryObj::node2path {node} {
1008    set path [node2comp $node]
1009    set node [$node parentNode]
1010    while {$node != "" && $node != $_node} {
1011        set path "[node2comp $node].$path"
1012        set node [$node parentNode]
1013    }
1014    return $path
1015}
1016
1017# ----------------------------------------------------------------------
1018# USAGE: childnodes <node> <type>
1019#
1020# Used internally to return a list of children for the given <node>
1021# that match a specified <type>.  Similar to XML getElementsByTagName,
1022# but returns only direct children of the <node>.
1023# ----------------------------------------------------------------------
1024itcl::body Rappture::LibraryObj::childnodes {node type} {
1025    set rlist ""
1026    foreach cnode [$node childNodes] {
1027        if {[$cnode nodeName] == $type} {
1028            lappend rlist $cnode
1029        }
1030    }
1031    return $rlist
1032}
Note: See TracBrowser for help on using the repository browser.