]> git.sesse.net Git - xml-template/blob - perl-sax/XML/TemplateSAX.pm
ff68c1f9c464b552c3a907d4b54c94b32b2f0fc5
[xml-template] / perl-sax / XML / TemplateSAX.pm
1 #! /usr/bin/perl
2
3 #
4 # SAX version of XML::Template. Advantages over DOM: Doesn't have to load
5 # the entire thing into memory, and you can chain filters. Disadvantages:
6 # Slightly kludgier interface.
7 #
8 # Differences from the DOM version:
9
10 # - There is no process(). Instead, it works as a SAX filter, so you put it
11 #   in the stream, usually between a parser and a writer (ie.
12 #   parser -> XML::TemplateSAX::Handler -> writer). process_file works as
13 #   before, but it returns a _string_, not a DOM tree.
14 # - You can no longer insert a DOM tree. Instead, what you have is -- FIXME:
15 #   figure out this :-)
16 #
17
18 use XML::SAX::Expat;
19 use XML::SAX::Writer;
20 use Data::Dumper;
21
22 package XML::TemplateSAX::Handler;
23 use base qw(XML::SAX::Base);
24
25 sub new {
26         my $class = shift;
27         my %options = @_;
28
29         my $self = {
30                 obj => $options{'Content'},
31                 stack => [],
32                 Handler => $options{'Handler'}
33         };
34         bless($self, $class);
35         return $self;
36 }
37
38 sub start_element {
39         my ($self, $data) = @_;
40         my $obj = $self->{'obj'};
41
42         # find the ID, if any
43         my $id = $data->{'Attributes'}->{'{http://template.sesse.net/}id'};
44         $id = $id->{'Value'} if (defined($id));
45
46         # within a replacement; just ignore everything  
47         return if (!defined($obj));
48
49         # substitution: see if this element matches anything. if so,
50         # descend down into the tree.
51         if (ref($obj) eq 'HASH') {
52                 my $match = undef;
53                 for my $key (keys %$obj) {
54                         if ($key =~ /^#(.*)$/) {
55                                 if (defined($id) && $id eq $1) {
56                                         $match = $obj->{$key};
57                                         last;
58                                 }
59                         } else {
60                                 if ($data->{'LocalName'} eq $key) {
61                                         $match = $obj->{$key};
62                                         last;
63                                 }
64                         }
65                 }
66
67                 if (defined($match)) {
68                         $self->SUPER::start_element($data);
69
70                         push @{$self->{'stack'}}, [ $data->{'Name'}, $obj ];
71                         
72                         #
73                         # This is sort of ugly. We special-case replacement by outputting
74                         # the string immediately, and then just ignoring the rest of the
75                         # events until we get to the right end tag. It's not 100% technically
76                         # correct for the case where you replace an entire document by a
77                         # string, but that's nonsensical anyway.
78                         #
79                         if (!ref($match)) {
80                                 $self->SUPER::characters({ Data => $match });
81                                 $match = undef;
82                         }
83
84                         $self->{'obj'} = $match;
85                         return;
86                 }
87         }
88         
89         $self->SUPER::start_element($data);
90 }
91
92 sub characters {
93         my ($self, $data) = @_;
94         return if (!defined($self->{'obj'}));
95         $self->SUPER::characters($data);
96 }
97
98 sub end_element {
99         my ($self, $data) = @_;
100
101         my $stack = $self->{'stack'};
102         if (scalar @$stack > 0) {
103                 my $top = $stack->[$#stack];
104                 
105                 if ($data->{'Name'} eq $top->[0]) {
106                         $self->SUPER::end_element($data);
107                         $self->{'obj'} = $top->[1];
108                         pop @$stack;
109                         return;
110                 }
111         }
112         
113         return if (!defined($self->{'obj'}));
114
115         $self->SUPER::end_element($data);
116 }
117
118 package XML::TemplateSAX::Cleaner;
119 use base qw(XML::SAX::Base);
120
121 sub start_element {
122         my ($self, $data) = @_;
123         my $attrs = $data->{'Attributes'};
124
125         for my $a (keys %$attrs) {
126                 if ($attrs->{$a}->{'NamespaceURI'} eq 'http://template.sesse.net/') {
127                         delete $attrs->{$a};
128                 }
129         }
130
131         $self->SUPER::start_element($data);
132 }
133
134 package XML::TemplateSAX;
135
136 sub process_file {
137         my ($filename, $obj, $clean) = @_;
138         $clean = 1 unless (defined($clean));
139
140         my ($writer, $cleaner, $filter, $parser);
141         my $str = '';
142
143         # FIXME: hardcoding expat = not good?
144         $writer = XML::SAX::Writer->new(Output => \$str);
145
146         if ($clean) {
147                 $cleaner = XML::TemplateSAX::Cleaner->new(Handler => $writer, Content => $obj);
148                 $filter = XML::TemplateSAX::Handler->new(Handler => $cleaner, Content => $obj);
149         } else {
150                 $filter = XML::TemplateSAX::Handler->new(Handler => $writer, Content => $obj);
151         }
152
153         $parser = XML::SAX::Expat->new(Handler => $filter);
154         $parser->parse_file($filename);
155
156         return $str;
157 }
158
159 1;