File Coverage

blib/lib/XML/GXML.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             # GXML module: generic, template-based XML transformation tool
2             # Copyright (C) 1999-2001 Josh Carter
3             # All rights reserved.
4             #
5             # This program is free software; you can redistribute it and/or modify
6             # it under the same terms as Perl itself.
7              
8             package XML::GXML;
9              
10             # 'strict' turned off for release, but stays on during development.
11             # use strict;
12 1     1   961 use Cwd;
  1         2  
  1         82  
13 1     1   2154 use XML::Parser;
  0            
  0            
14              
15             # Most of these vars are used as locals during parsing.
16             use vars ('$VERSION', '@attrStack', '$output',
17             '$baseTag', '$rPreserve', '$self');
18             $VERSION = 2.4;
19              
20             my $debugMode = 0;
21              
22             #######################################################################
23             # new, destroy, other initialization and attributes
24             #######################################################################
25              
26             sub new
27             {
28             my ($pkg, $rParams) = @_;
29              
30             my $templateDir = ($rParams->{'templateDir'} || 'templates');
31             my $varMarker = ($rParams->{'variableMarker'} || '%%%');
32             my $templateMgr = new XML::GXML::TemplateManager($templateDir,
33             $rParams->{'addlTemplates'},
34             $rParams->{'addlTemplate'},
35             $rParams->{'addlTempExists'},
36             $varMarker);
37              
38             $debugMode = $rParams->{'debugMode'} unless ($debugMode);
39            
40             # Create the new beast
41             my $self = bless
42             {
43             _templateMgr => $templateMgr,
44             _varMarker => $varMarker,
45             _remappings => ($rParams->{'remappings'} || { }),
46             _htmlMode => ($rParams->{'html'} || 0),
47             _dashConvert => ($rParams->{'dashConvert'} || 0),
48             _addlAttrs => ($rParams->{'addlAttrs'} || undef),
49             }, $pkg;
50              
51             $self->AddCallbacks($rParams->{'callbacks'});
52              
53             return $self;
54             }
55              
56             sub DESTROY
57             {
58             # nothing needed for now
59             }
60              
61             #
62             # AddCallbacks
63             #
64             # Callbacks allow you to be notified at the start or end of a given
65             # tag. Pass in a hash of tag names to subroutine refs. Tag names
66             # should be prefixed with "start:" or "end:" to specify where the
67             # callback should take place. See docs for more info on using
68             # callbacks.
69             #
70             sub AddCallbacks
71             {
72             my ($self, $rCallbacks) = @_;
73             my (%start, %end);
74              
75             # add our default commands
76             %start = ('gxml:foreach' => \&ForEachStart);
77             %end = ('gxml:ifexists' => \&ExistsCommand,
78             'gxml:ifequals' => \&EqualsCommand,
79             'gxml:ifequal' => \&EqualsCommand,
80             'gxml:ifnotequal' => \&NotEqualsCommand,
81             'gxml:include' => \&IncludeCommand,
82             'gxml:foreach' => \&ForEachEnd,);
83              
84             # and add the stuff passed in, if anything
85             foreach my $callback (keys %{$rCallbacks})
86             {
87             if ($callback =~ /^start:(.*)/)
88             {
89             $start{$1} = $rCallbacks->{$callback};
90             XML::GXML::Util::Log("adding start callback $1");
91             }
92             elsif ($callback =~ /^end:(.*)/)
93             {
94             $end{$1} = $rCallbacks->{$callback};
95             XML::GXML::Util::Log("adding end callback $1");
96             }
97             else
98             {
99             XML::GXML::Util::Log("unknown callback type $callback");
100             }
101             }
102              
103             $self->{'_cb-start'} = \%start;
104             $self->{'_cb-end'} = \%end;
105             }
106              
107             #######################################################################
108             # Process, ProcessFile
109             #######################################################################
110              
111             #
112             # Process
113             #
114             # Processes a given XML string. Returns the output as a scalar.
115             #
116             sub Process()
117             {
118             my ($selfParam, $stuff) = @_;
119            
120             # Set up these pseudo-global vars
121             local (@attrStack, $output, $baseTag, $rPreserve);
122              
123             # Also create this so XML::Parser handlers can see it
124             local $self = $selfParam;
125              
126             # See note in LoadTemplate about this
127             $stuff =~ s/$self->{_varMarker}/::VAR::/g;
128              
129             # Process the beastie
130             my $xp = new XML::Parser(ErrorContext => 2);
131             $xp->setHandlers(Char => \&HandleChar,
132             Start => \&HandleStart,
133             End => \&HandleEnd,
134             Comment => \&HandleComment,
135             Default => \&HandleDefault);
136              
137             $xp->parse($stuff);
138              
139             return $output;
140             }
141              
142             #
143             # ProcessFile
144             #
145             # Processes a given XML file. If an output file name is provided, the
146             # result will be dumped into there. Otherwise it will return the
147             # output as a scalar.
148             #
149             sub ProcessFile()
150             {
151             my ($selfParam, $source, $dest) = @_;
152             my $fileName;
153             my $baseDir = cwd();
154            
155             # Set up these pseudo-global vars
156             local (@attrStack, $output, $baseTag, $rPreserve);
157              
158             # Also create this so XML::Parser handlers can see it
159             local $self = $selfParam;
160              
161             #
162             # Open and parse the input file.
163             #
164             $fileName = XML::GXML::Util::ChangeToDirectory($source);
165            
166             open(IN, $fileName) || die "open input $fileName: $!";
167              
168             # Slurp everything
169             local $/;
170             undef $/; # turn on slurp mode
171             my $file = ;
172            
173             close(IN);
174             chdir($baseDir);
175              
176             # See note in LoadTemplate about this
177             $file =~ s/$self->{_varMarker}/::VAR::/g;
178              
179             # Process the beastie
180             my $xp = new XML::Parser(ErrorContext => 2);
181             $xp->setHandlers(Char => \&HandleChar,
182             Start => \&HandleStart,
183             End => \&HandleEnd,
184             Comment => \&HandleComment,
185             Default => \&HandleDefault);
186              
187             $xp->parse($file);
188              
189             return $output unless ($dest);
190              
191             #
192             # Find and open the output file.
193             #
194             chdir($baseDir);
195             $fileName = XML::GXML::Util::ChangeToDirectory($dest);
196              
197             open(OUT, ">$fileName") || die "open output $fileName: $!";
198            
199             # Ensure the permissions are correct on the output file.
200             my $cnt = chmod 0745, $fileName;
201             warn "chmod failed on $fileName: $!" unless $cnt;
202              
203             # Print the results
204             print OUT $output;
205              
206             close(OUT);
207             chdir($baseDir);
208             }
209              
210             #######################################################################
211             # XML parser callbacks
212             #######################################################################
213              
214             #
215             # HandleStart
216             #
217             # Create a new attribute frame for this element and fill it with the
218             # element's attributes, if any. Nothing is printed to $output just yet;
219             # that comes in HandleEnd.
220             #
221             sub HandleStart()
222             {
223             my ($xp, $element, %attrs) = @_;
224             my ($key, %cbParams);
225              
226             # First element in the document is always the base
227             $baseTag = $element unless defined($baseTag);
228            
229             XML::GXML::Util::Log("start: $element");
230              
231             foreach $key (keys %attrs)
232             {
233             my $val = $attrs{$key};
234              
235             # Compact whitespace, strip leading and trailing ws.
236             $val =~ s/\s+/ /g;
237             $val =~ s/^\s*(.*?)\s*$/$1/;
238              
239             # Make variable substitutions in each key.
240             $val = SubstituteAttributes($val);
241              
242             # Stick newly molested $val back into $attrs if we've still
243             # got something left, or delete the attribute if it's empty
244             # (could have been made empty after substitition).
245             if (length($val))
246             { $attrs{$key} = $val; }
247             else
248             { delete $attrs{$key}; }
249              
250             XML::GXML::Util::Log("\t$key: $val");
251             }
252              
253             # Save our tag name in the attrs, too.
254             $attrs{_TAG_} = $element;
255              
256             # Add these attributes to the master tree.
257             AddAttributeNode($element, \%attrs);
258              
259             # Call registered callback, if one exists
260             if ($self->{'_cb-start'}->{$element})
261             {
262             &{$self->{'_cb-start'}->{$element}}(\%cbParams);
263             }
264             }
265              
266             #
267             # HandleEnd
268             #
269             # By now we should have stuff in the current attribute frame's _BODY_
270             # special attribute, assuming there was char data. We need to either
271             # run a substitution for the tag, or just echo the _BODY_ framed by
272             # opening and closing tags. If there was no char data and this isn't a
273             # templated element, just echo (HTML syntax).
274             #
275             sub HandleEnd()
276             {
277             my ($xp, $element) = @_;
278             my $orig = $element;
279             my $html = ($self->{_htmlMode} ne 0);
280             my ($rActions, $discard, $repeat, $strip);
281             my %cbParams;
282              
283             XML::GXML::Util::Log("end: $element");
284              
285             # Get the attribute frame for this element, and also the next one up.
286             my $attrsRef = $attrStack[-1];
287             my $nextFrame = $attrStack[-2];
288             my $destRef = undef;
289              
290             # Our element's tag may be a variable. Substitute now.
291             $element = SubstituteAttributes($element);
292              
293             #
294             # If the element should be remapped into something else, do that
295             # now. NOTE: this means that an untemplatted tag can be remapped
296             # into a templatted one, and the template *will* be applied.
297             #
298             if (exists $self->{'_remappings'}->{$element})
299             {
300             $element = $self->{'_remappings'}->{$element};
301             }
302              
303             # Bail if the tag was substituted/remapped to nothing
304             if (!length($element))
305             {
306             XML::GXML::Util::Log("discarding $orig because it's remapped to nil");
307             LeaveAttributeNode();
308             return;
309             }
310              
311             repeat:
312              
313             # Call callback if needed
314             if ($self->{'_cb-end'}->{$element})
315             {
316             $rActions = &{$self->{'_cb-end'}->{$element}}(\%cbParams);
317              
318             # Make sure these are clear, since a previous 'repeat' may
319             # have changed them.
320             undef $discard; undef $repeat; undef $strip;
321              
322             if (defined($rActions) && ref($rActions) eq 'ARRAY')
323             {
324             foreach my $action (@$rActions)
325             {
326             if ($action eq 'discard') { $discard = 1; }
327             elsif ($action eq 'repeat') { $repeat = 1; }
328             elsif ($action eq 'striptag') { $strip = 1; }
329             }
330             }
331             }
332              
333             if ($discard)
334             {
335             XML::GXML::Util::Log("discarding $orig because callback told me to");
336             LeaveAttributeNode();
337             return;
338             }
339              
340             if ($repeat)
341             {
342             #
343             # This requires some explanation. The gxml:foreach start
344             # callback (assuming we're repeating because of foreach) would
345             # have set aside its 'expr' variable as something HandleChar
346             # shouldn't substitute. If that happened, each iteration would
347             # have the same expr value -- it would just sub the first
348             # value in there, and then the variable wouldn't exist
349             # anymore. Thus it must be saved until here and substituted.
350             #
351             # First step: fetch the original body which has the SAVE
352             # marker preserved. If this is the first pass through, grab it
353             # from the attrs.
354             #
355             my $body = $cbParams{'body'};
356              
357             if (!defined($body))
358             {
359             $body = $attrsRef->{_BODY_};
360             $cbParams{"body"} = $body;
361             }
362              
363             # Figure out what we were saving
364             my $var = $cbParams{'expr'} || Attribute('expr');
365              
366             # Now sub back the VAR marker and sub in the attribute
367             $body =~ s/::SAVE::(${var}:?.*?)::SAVE::/::VAR::$1::VAR::/g;
368             $body = SubstituteAttributes($body);
369              
370             # Finally, refresh this for later code
371             $attrsRef->{_BODY_} = $body;
372             }
373              
374             #
375             # If there's a frame above us and we're not the document's base
376             # element, we want to proceed normally. All output should go into
377             # the _BODY_ attribute of the frame above us. Otherwise we want to
378             # dump the current _BODY_ to $output and return.
379             #
380             if ((defined $nextFrame) && ($baseTag ne $element))
381             {
382             $destRef = \$nextFrame->{_BODY_};
383             }
384             else
385             {
386             # Special case for the very top-level element: if the beast has
387             # a template, substitute it and dump the output directory into
388             # $output, since there's no upper-level _BODY_ for it.
389             if (!defined $nextFrame && $self->TemplateExists($element))
390             {
391             $output .= $self->SubstituteTemplate($element);
392             }
393             elsif (defined ($attrsRef->{_BODY_}))
394             {
395             # Otherwise just dump to $output. NOTE: this case also
396             # applies to the base tag of templates.
397              
398             $output .= $attrsRef->{_BODY_};
399             }
400            
401             unless ($html)
402             {
403             $output = "<$element>$output";
404             }
405              
406             LeaveAttributeNode();
407             return;
408             }
409              
410             if ($self->TemplateExists($element))
411             {
412             #
413             # There's a template for this element, so we need to
414             # substitute it in.
415             #
416             XML::GXML::Util::Log("found template for $element");
417              
418             my $substitution = $self->SubstituteTemplate($element);
419             $$destRef .= $substitution if defined($substitution);
420            
421             # Update our _BODY_ to reflect the new substitution.
422             $attrsRef->{_BODY_} = $substitution;
423             }
424             elsif ($strip)
425             {
426             #
427             # If a callback said to strip its tag off the output, just
428             # echo our body without a tag wrapped around it.
429             #
430             $$destRef .= $attrsRef->{_BODY_} if defined($attrsRef->{_BODY_});
431             }
432             else
433             {
434             #
435             # No template, so just echo the tag and relevant _BODY_ in XML
436             # syntax (i.e. single-tag element syntax), unless $html
437             # is set, in which case we want it in HTML syntax.
438             #
439              
440             # Grab a reference to _only_ the attributes in our
441             # current frame.
442             my $attrsRef = $attrStack[-1];
443              
444             # If the tag has an explicit 'html:' namespace prefix, strip
445             # that if we're in HTML mode.
446             $element =~ s/^html:// if $html;
447            
448             # Print the tag.
449             $$destRef .= '<' . $element;
450              
451             # Print the attibute list for this (and only this) element.
452             foreach my $key (keys %$attrsRef)
453             {
454             next if $key =~ /^_[-_A-Z]+_$/; # skip special variables
455              
456             my $cleankey = $key;
457             $cleankey =~ s/^html:// if $html;
458            
459             $$destRef .= " $cleankey=\"" . $attrsRef->{$key} . "\"";
460             }
461              
462             #
463             # If there's character data (i.e. this is not a single-tag
464             # element), print that data and a closing tag.
465             #
466             if (defined($attrsRef->{_BODY_}) && length($attrsRef->{_BODY_}))
467             {
468             # Close the opening tag
469             $$destRef .= '>';
470              
471             $$destRef .= $attrsRef->{_BODY_};
472              
473             $$destRef .= '';
474             }
475             elsif ($html)
476             {
477             # Single-tag element, but in HTML mode.
478             $$destRef .= '>';
479             }
480             else
481             {
482             # Single-tag element, and we're just doing a generic
483             # XML->XML conversion, so preserve syntax.
484             $$destRef .= '/>';
485             }
486             }
487              
488             if ($repeat)
489             {
490             # Callback will be called again at top of this loop.
491             goto repeat;
492             }
493              
494             LeaveAttributeNode();
495             }
496              
497             #
498             # HandleChar
499             #
500             # Substitute any attributes which show up in our input string, and
501             # append the resulting string to the last attr frame's _BODY_ attr.
502             #
503             sub HandleChar()
504             {
505             my ($xp, $string) = @_;
506              
507             # Achtung! We must process the original string, not the one
508             # munged by Expat into UTF-8, which will automatically remap
509             # things like "<" into "<". If the author wrote < in their
510             # XML document, that's probably because they wanted it in their
511             # HTML document, too.
512             $string = $xp->original_string;
513              
514             # Make variable substitutions.
515             $string = SubstituteAttributes($string);
516              
517             # Convert m-dashes if needed.
518             $string =~ s/--/&\#8212;/g if $self->{_dashConvert};
519              
520             # Append the body text to the _BODY_ attribute of the last
521             # attribute frame on the stack (i.e. that of the most immediately
522             # enclosing element).
523             $attrStack[-1]->{_BODY_} .= $string;
524             }
525              
526             #
527             # HandleComment
528             #
529             # Stick comments in the _BODY_ attr, too. Also supports attribute
530             # substitution.
531             #
532             sub HandleComment()
533             {
534             my ($xp, $string) = @_;
535              
536             # Make variable substitutions.
537             $string = SubstituteAttributes($string);
538              
539             # Append the text to the _BODY_ attribute of the last attribute
540             # frame on the stack. Remember to put the comment markers back in!
541             $attrStack[-1]->{_BODY_} .= '';
542             }
543              
544             #
545             # HandleDefault
546             #
547             # Discard all the other stuff which we may encounter.
548             #
549             sub HandleDefault()
550             {
551             my ($xp, $string) = @_;
552              
553             # Discard stuff for now.
554             }
555              
556              
557              
558             #######################################################################
559             # Attribute tree maintenance
560             #######################################################################
561              
562              
563              
564             #
565             # AddAttributeNode
566             #
567             # Add a node to the document tree. The node's contents are the
568             # attributes of that element, both in the tag and the body (via the
569             # _BODY_ attr). This should be called in HandleStart, and paired with
570             # LeaveAttributeNode in HandleEnd.
571             #
572             sub AddAttributeNode
573             {
574             my ($tag, $attrsRef) = @_;
575             my ($parent);
576              
577             # Get our parent if there is one. This will be the last thing
578             # on the stack, as we haven't added ourself yet.
579             if (defined $attrStack[-1])
580             {
581             $parent = $attrStack[-1];
582             }
583             else
584             {
585             # No parent means we're the top-level element, so just add
586             # ourself to the stack and return.
587             push(@attrStack, $attrsRef);
588             return;
589             }
590              
591             # If our parent doesn't have any children yet, it does now.
592             unless (exists $parent->{_CHILDREN_})
593             {
594             $parent->{_CHILDREN_} = { };
595             }
596            
597             # If our parent has children with our tag name, add ourself to
598             # that list. Otherwise create a new list with ourself in it.
599             if (exists $parent->{_CHILDREN_}->{$tag})
600             {
601             push(@{$parent->{_CHILDREN_}->{$tag}}, $attrsRef);
602             }
603             else
604             {
605             $parent->{_CHILDREN_}->{$tag} = [ $attrsRef ];
606             }
607              
608             # Finally, put ourself on the stack.
609             push(@attrStack, $attrsRef);
610             }
611              
612             #
613             # LeaveAttributeNode
614             #
615             # Keep the attribute stack intact.
616             #
617             sub LeaveAttributeNode
618             {
619             pop(@attrStack);
620             }
621              
622             #
623             # Attribute
624             #
625             # Find a given attribute and return its value. If there were multiple
626             # values, only return the first. (Use RotateAttribute to get others.)
627             #
628             sub Attribute
629             {
630             my ($key) = @_;
631              
632             my $attr = FindAttribute($key);
633             my $ref = ref($attr);
634            
635             if ($ref eq 'ARRAY')
636             {
637             $attr = @{$attr}[0]->{_BODY_};
638             }
639              
640             return $attr;
641             }
642              
643             sub AddAttribute
644             {
645             my ($key, $val, $recurse) = @_;
646              
647             # Add to last frame on stack; bail if no frames there.
648             return unless (defined $attrStack[-1]);
649              
650             XML::GXML::Util::Log("addattr: marking " . $attrStack[-1]->{_TAG_} ." ". $val);
651             $attrStack[-1]->{$key} = $val;
652              
653             if ($recurse =~ /^parents/)
654             {
655             foreach my $frame (reverse @attrStack)
656             {
657             # skip if value already defined and weak recurse
658             next if (($recurse eq 'parents-weak') &&
659             defined($frame->{$key}));
660              
661             XML::GXML::Util::Log("addattr: marking " . $frame->{_TAG_} ." ". $val);
662             $frame->{$key} = $val;
663             }
664             }
665             }
666              
667             #
668             # RotateAttribute
669             #
670             # For an attribute which has multiple values, take the first one and
671             # stick it on the end. A subsequent call to Attribute() will then
672             # return the new first element.
673             #
674             sub RotateAttribute
675             {
676             my ($key) = @_;
677              
678             my $attr = FindAttribute($key);
679             my $ref = ref($attr);
680            
681             if ($ref eq 'ARRAY')
682             {
683             my $front = shift @{$attr};
684             push(@{$attr}, $front);
685             }
686             else
687             {
688             XML::GXML::Util::Log("tried to rotate attribute $key which wasn't a list");
689             }
690             }
691              
692             #
693             # NumAttributes
694             #
695             # Return the number of values an attribute has.
696             #
697             sub NumAttributes
698             {
699             my ($key) = @_;
700              
701             my $attr = FindAttribute($key);
702             my $ref = ref($attr);
703             my $num;
704            
705             if (!defined($attr))
706             { return 0; }
707             elsif ($ref eq 'ARRAY')
708             { $num = int @{$attr}; return $num; }
709             elsif (!defined($ref))
710             { return 1; }
711             }
712              
713             #
714             # FindAttribute
715             #
716             # Scan backwards through the attribute stack looking for the first
717             # attribute match. If nothing is found, look for "key-default." If we
718             # find a child element acting as an attribute, return that list. If it
719             # was declared in the start tag, just return the text. Callers should
720             # check ref() on the return value to figure out what it is.
721             #
722             sub FindAttribute
723             {
724             my ($key, @stack) = @_;
725             my ($frame, $parent, $return, $subkeys);
726             my $origkey = $key;
727              
728             @stack = reverse @attrStack unless int(@stack);
729              
730             if ($key =~ /^([^:]+):(.*)$/)
731             {
732             $key = $1;
733             $subkeys = $2;
734             }
735              
736             # Scan backwards through attribute stack trying to find the
737             # requested key.
738             foreach $frame (@stack)
739             {
740             # First check this level for immediate children whose tag
741             # matches what we're looking for.
742             if (exists $frame->{_CHILDREN_} &&
743             exists $frame->{_CHILDREN_}->{$key})
744             {
745             $return = $frame->{_CHILDREN_}->{$key};
746             goto found;
747             }
748            
749             # Otherwise check element params embedded in the tag.
750             return $$frame{$key} if (exists $$frame{$key});
751             }
752              
753             # Call additional attribute method passed to new(), if any.
754             if (defined $self->{_addlAttrs})
755             {
756             my $val = &{$self->{_addlAttrs}}($origkey);
757              
758             return $val if defined ($val);
759             }
760              
761             # Hmm, I guess that didn't work. Now search for the same key with
762             # "-default" tacked on the end.
763             $key .= "-default";
764              
765             # Second verse same as the first...
766             foreach $frame (@stack)
767             {
768             if (exists $frame->{_CHILDREN_} &&
769             exists $frame->{_CHILDREN_}->{$key})
770             {
771             $return = $frame->{_CHILDREN_}->{$key};
772             goto found;
773             }
774            
775             return $$frame{$key} if (exists $$frame{$key});
776             }
777              
778             XML::GXML::Util::Log("couldn't find a value for $key, dude.");
779             return undef;
780              
781             found:
782              
783             if (defined($subkeys))
784             {
785             return FindAttribute($subkeys, @$return);
786             }
787             else
788             {
789             return $return;
790             }
791             }
792              
793             #
794             # SubstituteAttributes
795             #
796             # Dig through a string looking for variables and replace them with
797             # attributes in the current scope.
798             #
799             sub SubstituteAttributes
800             {
801             my ($string, $marker) = @_;
802              
803             # Hack: see note in LoadTemplates about this.
804             $marker = "::VAR::" unless defined($marker);
805              
806             # Change the marker for variables which we don't want substituted.
807             foreach my $var (@$rPreserve)
808             {
809             $string =~ s/${marker}(${var}:?.*?)${marker}/::SAVE::$1::SAVE::/g;
810             }
811              
812             # Special case!!! If someone requests the _BODY_ attribute, we
813             # must scan upwards in the attribute stack and grab the body text
814             # of the element immediately above the current template's base tag.
815             # This will give us the text which is enclosed by the template's
816             # tags (i.e. the character data of the template element).
817             if ($string =~ /${marker}\s*?_BODY_[\w\-:]*?\s*?${marker}/)
818             {
819             # Get index of template element's attr frame minus one more.
820             my $index = -1;
821             while ($attrStack[$index--]->{_TAG_} ne $baseTag) { }
822              
823             # Attribute stack dump is sometimes helpful in debugging.
824             if ($debugMode && 0)
825             {
826             print "_BODY_ sub; index is $index, stack size is " .
827             scalar(@attrStack) . ", matching tag is " .
828             $attrStack[$index]->{_TAG_} . "\n";
829             print "lenth of body in each frame:\n";
830             foreach my $frame (@attrStack)
831             {
832             print " " . $frame->{_TAG_} . ":" . length($frame->{_BODY_});
833             }
834             print "\n";
835             }
836            
837             # ...and substitute that.
838             $string =~ s/${marker}\s*?(_BODY_[\w\-:]*?)\s*?${marker}/
839             MungeAttributeSubstitition($1, $attrStack[$index]->{_BODY_}) /eg;
840             }
841              
842             # Substitute other attributes as required. Start
843             # with plain %%%thing%%% ones first.
844             $string =~ s/${marker}\s*?([\w\-:]+?)\s*?${marker}/
845             MungeAttributeSubstitition($1) /eg;
846            
847             # Now do %%%(thing)%%% ones, which may have contained plain
848             # %%%thing%%% ones that were just sub'd in the line above.
849             $string =~ s/${marker}\(\s*?([\w\-:]+?)\s*?\)${marker}/
850             MungeAttributeSubstitition($1) /eg;
851              
852             return $string;
853             }
854              
855             #
856             # MungeAttributeSubstitition
857             #
858             # Attributes can have post-processors on them. This scans for
859             # processors and applies them as needed (a.k.a. munging), returning
860             # the munged attr. The format of a variable which should be processed
861             # is attr-PROCESSOR, where the attribute is "attr" and the processor
862             # name is "PROCESSOR". Processors can be chained, too.
863             #
864             sub MungeAttributeSubstitition
865             {
866             my ($attribute, $substitute) = @_;
867              
868             my %processors = ("URLENCODED" => \&URLEncode,
869             "LOWERCASE" => \&Lowercase,
870             "UPPERCASE" => \&Uppercase,);
871              
872             # Split the attribute name across dashes, with each chunk being a
873             # potential processor
874             my @attrchunks = split("-", $attribute);
875             my ($chunk, $processor, @processors);
876              
877             # Now scan backwards over our chunks, popping off ones which
878             # match known processors. Stop at the first unknown chunk, which
879             # is part of the attribute name.
880             while (defined ($processor = $processors{$chunk = pop @attrchunks}))
881             {
882             XML::GXML::Util::Log("found processor $processor for $attribute");
883             push(@processors, $processor);
884             }
885             push (@attrchunks, $chunk); # push last one back on
886              
887             # Now restore the attribute name (which may have had dashes in
888             # it), minus the processors chained on the end.
889             $attribute = join("-", @attrchunks);
890            
891             # Use the restored attr name to get the substitute.
892             $substitute = Attribute($attribute)
893             unless (defined $substitute || $attribute eq "_BODY_");
894              
895             # print "final attr $attribute = $substitute\n";
896              
897             # Now apply each processor to the substitute.
898             while ($processor = pop @processors)
899             {
900             $substitute = &$processor($substitute);
901             }
902              
903             # Return an empty string if $substitute is undef.
904             $substitute = '' unless defined($substitute);
905              
906             return $substitute;
907             }
908              
909              
910              
911             #######################################################################
912             # gxml:x commands
913             #######################################################################
914              
915              
916             #
917             # ExistsCommand
918             #
919             # Returns 'discard' to HandleEnd unless the attribute 'expr' is true.
920             # 'expr' may be an attribute name, or some combination of attribute
921             # names with logical operators, e.g. 'name AND NOT age'.
922             #
923             sub ExistsCommand
924             {
925             my ($rParams) = @_;
926              
927             my $element = Attribute('expr');
928              
929             unless (length($element))
930             {
931             XML::GXML::Util::Log("couldn't find element for gxml:ifexists command");
932             return;
933             }
934              
935             #
936             # Sub in perl logical operators in place of English...
937             #
938             $element =~ s/\band\b/\&\&/ig;
939             $element =~ s/\bor\b/\|\|/ig;
940             $element =~ s/\bnot\b/!/ig;
941             $element =~ s/([\w:-_]+)/length(Attribute("$1"))/g;
942            
943             # ...and then eval() it. I love Perl.
944             unless (eval($element))
945             {
946             # discard if expr not true
947             return ['discard'];
948             }
949              
950             # Be sure to discard the gxml:ifexists tag
951             return ['striptag'];
952             }
953              
954             #
955             # EqualsCommand
956             #
957             # Returns 'discard' to HandleEnd unless the attribute 'expr' is
958             # present and equal to 'equalto'.
959             #
960             #
961             sub EqualsCommand
962             {
963             my ($rParams) = @_;
964              
965             my $element = Attribute('expr');
966             my $equalto = Attribute('equalto');
967              
968             unless (length($element))
969             {
970             XML::GXML::Util::Log("couldn't find element for gxml:equals command");
971             return;
972             }
973              
974             XML::GXML::Util::Log("equals: expr is $element, equalto is $equalto");
975              
976             unless (Attribute($element) eq $equalto)
977             {
978             # discard if expr not equal to equalto
979             return ['discard'];
980             }
981              
982             # Be sure to discard the gxml:ifequal tag
983             return ['striptag'];
984             }
985              
986             #
987             # NotEqualsCommand
988             #
989             # Returns 'discard' to HandleEnd unless the attribute 'expr' is
990             # present and NOT equal to 'equalto'.
991             #
992             #
993             sub NotEqualsCommand
994             {
995             my ($rParams) = @_;
996              
997             my $element = Attribute('expr');
998             my $equalto = Attribute('equalto');
999              
1000             unless (length($element))
1001             {
1002             XML::GXML::Util::Log("couldn't find element for gxml:equals command");
1003             return;
1004             }
1005              
1006             XML::GXML::Util::Log("equals: expr is $element, equalto is $equalto");
1007              
1008             unless (Attribute($element) ne $equalto)
1009             {
1010             # discard if expr equal to equalto
1011             return ['discard'];
1012             }
1013              
1014             # Be sure to discard the gxml:ifequal tag
1015             return ['striptag'];
1016             }
1017              
1018             #
1019             # ForEachStart
1020             #
1021             # gxml:foreach will repeat a block for each value of its 'expr' param.
1022             # Each iteration will contain a new value of expr, in the order they
1023             # appear in the XML source. In this start handler we'll need to set up
1024             # the special $rPreserve list with our expr so SubstituteAttributes
1025             # will know to not mess with it.
1026             #
1027             sub ForEachStart
1028             {
1029             my $element = Attribute('expr');
1030              
1031             unless (length($element))
1032             {
1033             XML::GXML::Util::Log("couldn't find element for gxml:foreach command");
1034             return;
1035             }
1036              
1037             $rPreserve = [] unless (defined($rPreserve));
1038              
1039             push(@$rPreserve, $element);
1040             }
1041              
1042             #
1043             # ForEachEnd
1044             #
1045             # Counts the number of times we've interated, and rotates the 'expr'
1046             # attribute to catch each value.
1047             #
1048             sub ForEachEnd
1049             {
1050             my ($rParams) = @_;
1051             my $element = $rParams->{'expr'} || Attribute('expr');
1052             my $repeats = $rParams->{'repeats'};
1053             my $max = $rParams->{'max'};
1054              
1055             unless (length($element))
1056             {
1057             XML::GXML::Util::Log("couldn't find element for gxml:foreach command");
1058             return;
1059             }
1060              
1061             if ($repeats)
1062             {
1063             # We've been through before, so just increment and rotate.
1064             $rParams->{'repeats'} = $repeats + 1;
1065              
1066             RotateAttribute($element);
1067             }
1068             else
1069             {
1070             # First time through. Set up our saved params hash.
1071             $repeats = 1;
1072             $max = NumAttributes($element);
1073              
1074             # Bail if no attributes to iterate over.
1075             return ['discard'] if ($max == 0);
1076              
1077             # Don't need SubstituteAttributes to worry about us anymore.
1078             pop(@$rPreserve);
1079              
1080             $rParams->{'repeats'} = 1;
1081             $rParams->{'max'} = $max;
1082             $rParams->{'expr'} = $element;
1083              
1084             # Repeat and strip the gxml:foreach tag.
1085             return ['striptag', 'repeat'];
1086             }
1087              
1088             # We've rotated back to the start, so discard and stop looping.
1089             return ['discard'] if ($repeats >= $max);
1090              
1091             # We still need to loop. Repeat and strip the gxml:foreach tag.
1092             return ['striptag', 'repeat'];
1093             }
1094              
1095              
1096             #######################################################################
1097             # Attribute post-processors
1098             #######################################################################
1099              
1100              
1101             #
1102             # URLEncode
1103             #
1104             # Simple URL form encoder. Certainly not per-spec, but should work
1105             # okay for now.
1106             #
1107             sub URLEncode
1108             {
1109             my ($string) = @_;
1110              
1111             $string =~ s/^\s*(.*?)\s*$/$1/; # strip leading/trailing ws
1112             $string =~ s/\&/\%26/g;
1113             $string =~ s/\=/\%3d/g;
1114             $string =~ s/\?/\%3f/g;
1115             $string =~ s/ /\+/g;
1116              
1117             return $string;
1118             }
1119              
1120             # Lowercase: does what you'd expect it to.
1121             sub Lowercase
1122             {
1123             my ($string) = @_;
1124            
1125             $string =~ tr/A-Z/a-z/;
1126            
1127             return $string;
1128             }
1129              
1130             # Uppercase: ditto.
1131             sub Uppercase
1132             {
1133             my ($string) = @_;
1134            
1135             $string =~ tr/a-z/A-Z/;
1136            
1137             return $string;
1138             }
1139              
1140              
1141             #######################################################################
1142             # GXML class template management
1143             #######################################################################
1144              
1145              
1146             #
1147             # TemplateMgr
1148             #
1149             # Returns a reference to the template manager.
1150             #
1151             sub TemplateMgr
1152             {
1153             my $self = shift;
1154              
1155             return $self->{_templateMgr};
1156             }
1157              
1158             #
1159             # TemplateExists
1160             #
1161             # Helper method; returns TemplateExists() from the template manager.
1162             #
1163             sub TemplateExists
1164             {
1165             my ($self, $name) = @_;
1166              
1167             return $self->{_templateMgr}->TemplateExists($name);
1168             }
1169              
1170             #
1171             # SubstituteTemplate
1172             #
1173             # Copy the template and parse it as a separate XML blob, but retain
1174             # the existing attribute stack. Returns the resulting text.
1175             #
1176             sub SubstituteTemplate
1177             {
1178             my ($self, $templateName) = @_;
1179              
1180             # Make our own copy of the template so we can parse and substitute
1181             # our attributes into it.
1182             my $template = ${$self->TemplateMgr()->Template($templateName)};
1183            
1184             # Create our own aliai of relevant globals
1185             local ($output, $baseTag);
1186              
1187             #
1188             # Now create a new parser and parse the template. This will, of
1189             # course, recurse as necessary.
1190             #
1191             my $xp = new XML::Parser(ErrorContext => 2);
1192             $xp->setHandlers(Char => \&HandleChar,
1193             Start => \&HandleStart,
1194             End => \&HandleEnd,
1195             Comment => \&HandleComment,
1196             Default => \&HandleDefault);
1197            
1198             $xp->parse($template);
1199            
1200             return $output;
1201             }
1202              
1203              
1204             #######################################################################
1205             # XML::GXML::TemplateManager
1206             #######################################################################
1207              
1208              
1209             package XML::GXML::TemplateManager;
1210              
1211             use Cwd;
1212              
1213             sub new
1214             {
1215             my ($pkg, $templateDir, $addlTemplates,
1216             $addlTemplate, $addlTempExists, $varMarker) = @_;
1217             my $baseDir = cwd();
1218              
1219             # Create the new beast
1220             my $self = bless
1221             {
1222             _templateDir => $templateDir,
1223             _varMarker => $varMarker,
1224             }, $pkg;
1225              
1226             $self->{_addlTemplates} = $addlTemplates if defined($addlTemplates);
1227             $self->{_addlTemplate} = $addlTemplate if defined($addlTemplate);
1228             $self->{_addlTempExists} = $addlTempExists if defined($addlTempExists);
1229              
1230             # Assemble the list of files in the templates directory
1231             chdir($templateDir);
1232             my $templateListRef = XML::GXML::Util::GetFileList();
1233             chdir($baseDir);
1234              
1235             foreach my $filename (@$templateListRef)
1236             {
1237             # Only grab .xml files
1238             next unless ($filename =~ /\.xml$/ || $filename =~ /\.xhtml$/);
1239            
1240             # Strip ".xml" for saving in template hash; these will be
1241             # referenced sans .xml extension
1242             $filename =~ s/\.xml$//;
1243             $filename =~ s/\.xhtml$//;
1244              
1245             # Store blank placeholder
1246             $self->{$filename} = '';
1247             }
1248              
1249             return $self;
1250             }
1251              
1252             sub DESTROY
1253             {
1254             # nothing needed for now
1255             }
1256              
1257             #
1258             # LoadTemplate
1259             #
1260             # Loads a given template name into the cache.
1261             #
1262             sub LoadTemplate
1263             {
1264             my ($self, $name) = @_;
1265             my $baseDir = cwd();
1266              
1267             XML::GXML::Util::Log("loading template $name");
1268              
1269             my $filename = XML::GXML::Util::ChangeToDirectory(
1270             File::Spec->catfile($self->{_templateDir},
1271             $name . '.xml'));
1272              
1273             unless (open(TEMPLATE, $filename))
1274             {
1275             # Try .xhtml for file extension
1276             $filename =~ s/\.xml$/.xhtml/;
1277              
1278             unless (open(TEMPLATE, $filename))
1279             {
1280             XML::GXML::Util::Log("ERROR: couldn't open template $name: $!");
1281             chdir($baseDir);
1282             return;
1283             }
1284             }
1285              
1286             # slurp everything
1287             local $/;
1288             undef $/; # turn on slurp mode
1289             my $file =