source: trunk/tcl/scripts/library.tcl @ 954

Last change on this file since 954 was 954, checked in by mmc, 16 years ago

Fixed the test for "library isvalid" to avoid an error that Joe Ringgenberg
found: If a choice value had a word like "split", it was executed as a Tcl
command during the "library isvalid" test to see if it responded as a
LibraryObj? object. If so, then it would be grafted on as a tree; otherwise,
it is put as a single text element. With any other text, this test would
fail. But "split" is a command and does not fail, but produces a non-boolean
result and causes another error. It's fixed now to look at the command
name and make sure that it at least looks like libraryObj##.

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