File: | blib/lib/XML/Parser/Lite/Tree.pm |
Coverage: | 95.6% |
line | stmt | bran | cond | sub | time | code |
---|---|---|---|---|---|---|
1 | package XML::Parser::Lite::Tree; | |||||
2 | ||||||
3 | 8 8 8 | 74 14 13 | use 5.006; | |||
4 | 8 8 8 | 38 11 47 | use strict; | |||
5 | 8 8 8 | 41 10 37 | use warnings; | |||
6 | 8 8 8 | 48 13 100 | use XML::Parser::LiteCopy; | |||
7 | ||||||
8 | our $VERSION = '0.12'; | |||||
9 | ||||||
10 | 8 8 8 | 40 11 31 | use vars qw( $parser ); | |||
11 | ||||||
12 | sub instance { | |||||
13 | 4 | 23 | return $parser if $parser; | |||
14 | 4 | 18 | $parser = __PACKAGE__->new; | |||
15 | } | |||||
16 | ||||||
17 | sub new { | |||||
18 | 9 | 71 | my $class = shift; | |||
19 | 9 | 46 | my $self = bless {}, $class; | |||
20 | ||||||
21 | 9 0 | 63 0 | my %opts = (ref $_[0]) ? ((ref $_[0] eq 'HASH') ? %{$_[0]} : () ) : @_; | |||
22 | 9 | 43 | $self->{opts} = \%opts; | |||
23 | ||||||
24 | $self->{__parser} = new XML::Parser::LiteCopy | |||||
25 | Handlers => { | |||||
26 | 29 | 98 | Start => sub { $self->_start_tag(@_); }, | |||
27 | 21 | 70 | Char => sub { $self->_do_char(@_); }, | |||
28 | 1 | 4 | CData => sub { $self->_do_cdata(@_); }, | |||
29 | 29 | 94 | End => sub { $self->_end_tag(@_); }, | |||
30 | 1 | 4 | Comment => sub { $self->_do_comment(@_); }, | |||
31 | 2 | 8 | PI => sub { $self->_do_pi(@_); }, | |||
32 | 1 | 5 | Doctype => sub { $self->_do_doctype(@_); }, | |||
33 | 9 | 267 | }; | |||
34 | 9 | 119 | $self->{process_ns} = $self->{opts}->{process_ns} || 0; | |||
35 | 9 | 80 | $self->{skip_white} = $self->{opts}->{skip_white} || 0; | |||
36 | ||||||
37 | 9 | 36 | return $self; | |||
38 | } | |||||
39 | ||||||
40 | sub parse { | |||||
41 | 9 | 78 | my ($self, $content) = @_; | |||
42 | ||||||
43 | 9 | 52 | my $root = { | |||
44 | 'type' => 'root', | |||||
45 | 'children' => [], | |||||
46 | }; | |||||
47 | ||||||
48 | 9 | 43 | $self->{tag_stack} = [$root]; | |||
49 | ||||||
50 | 9 | 47 | $self->{__parser}->parse($content); | |||
51 | ||||||
52 | 9 | 36 | $self->cleanup($root); | |||
53 | ||||||
54 | 9 | 44 | if ($self->{skip_white}){ | |||
55 | 5 | 16 | $self->strip_white($root); | |||
56 | } | |||||
57 | ||||||
58 | 9 | 39 | if ($self->{process_ns}){ | |||
59 | 1 | 3 | $self->{ns_stack} = {}; | |||
60 | 1 | 4 | $self->mark_namespaces($root); | |||
61 | } | |||||
62 | ||||||
63 | 9 | 35 | return $root; | |||
64 | } | |||||
65 | ||||||
66 | sub _start_tag { | |||||
67 | 29 | 75 | my $self = shift; | |||
68 | 29 | 58 | shift; | |||
69 | ||||||
70 | 29 | 188 | my $new_tag = { | |||
71 | 'type' => 'element', | |||||
72 | 'name' => shift, | |||||
73 | 'attributes' => {}, | |||||
74 | 'children' => [], | |||||
75 | }; | |||||
76 | ||||||
77 | 29 | 130 | while (my $a_name = shift @_){ | |||
78 | 14 | 35 | my $a_value = shift @_; | |||
79 | 14 | 91 | $new_tag->{attributes}->{$a_name} = $a_value; | |||
80 | } | |||||
81 | ||||||
82 | 29 29 | 54 131 | push @{$self->{tag_stack}->[-1]->{children}}, $new_tag; | |||
83 | 29 29 | 56 98 | push @{$self->{tag_stack}}, $new_tag; | |||
84 | 29 | 98 | 1; | |||
85 | } | |||||
86 | ||||||
87 | sub _do_char { | |||||
88 | 21 | 50 | my $self = shift; | |||
89 | 21 | 43 | shift; | |||
90 | ||||||
91 | 21 | 58 | for my $content(@_){ | |||
92 | ||||||
93 | 21 | 89 | my $new_tag = { | |||
94 | 'type' => 'text', | |||||
95 | 'content' => $content, | |||||
96 | }; | |||||
97 | ||||||
98 | 21 21 | 45 125 | push @{$self->{tag_stack}->[-1]->{children}}, $new_tag; | |||
99 | } | |||||
100 | 21 | 51 | 1; | |||
101 | } | |||||
102 | ||||||
103 | sub _do_cdata { | |||||
104 | 1 | 3 | my $self = shift; | |||
105 | 1 | 3 | shift; | |||
106 | ||||||
107 | 1 | 4 | for my $content(@_){ | |||
108 | ||||||
109 | 1 | 5 | my $new_tag = { | |||
110 | 'type' => 'cdata', | |||||
111 | 'content' => $content, | |||||
112 | }; | |||||
113 | ||||||
114 | 1 1 | 3 7 | push @{$self->{tag_stack}->[-1]->{children}}, $new_tag; | |||
115 | } | |||||
116 | 1 | 2 | 1; | |||
117 | } | |||||
118 | ||||||
119 | sub _end_tag { | |||||
120 | 29 | 73 | my $self = shift; | |||
121 | ||||||
122 | 29 29 | 46 89 | pop @{$self->{tag_stack}}; | |||
123 | 29 | 93 | 1; | |||
124 | } | |||||
125 | ||||||
126 | sub _do_comment { | |||||
127 | 1 | 3 | my $self = shift; | |||
128 | 1 | 3 | shift; | |||
129 | ||||||
130 | 1 | 3 | for my $content(@_){ | |||
131 | ||||||
132 | 1 | 5 | my $new_tag = { | |||
133 | 'type' => 'comment', | |||||
134 | 'content' => $content, | |||||
135 | }; | |||||
136 | ||||||
137 | 1 1 | 2 6 | push @{$self->{tag_stack}->[-1]->{children}}, $new_tag; | |||
138 | } | |||||
139 | 1 | 3 | 1; | |||
140 | } | |||||
141 | ||||||
142 | sub _do_pi { | |||||
143 | 2 | 6 | my $self = shift; | |||
144 | 2 | 4 | shift; | |||
145 | ||||||
146 | 2 2 | 4 16 | push @{$self->{tag_stack}->[-1]->{children}}, { | |||
147 | 'type' => 'pi', | |||||
148 | 'content' => shift, | |||||
149 | }; | |||||
150 | 2 | 6 | 1; | |||
151 | } | |||||
152 | ||||||
153 | sub _do_doctype { | |||||
154 | 1 | 4 | my $self = shift; | |||
155 | 1 | 2 | shift; | |||
156 | ||||||
157 | 1 1 | 2 10 | push @{$self->{tag_stack}->[-1]->{children}}, { | |||
158 | 'type' => 'dtd', | |||||
159 | 'content' => shift, | |||||
160 | }; | |||||
161 | 1 | 3 | 1; | |||
162 | } | |||||
163 | ||||||
164 | sub mark_namespaces { | |||||
165 | 5 | 14 | my ($self, $obj) = @_; | |||
166 | ||||||
167 | 5 | 8 | my @ns_keys; | |||
168 | ||||||
169 | # | |||||
170 | # mark | |||||
171 | # | |||||
172 | ||||||
173 | 5 | 22 | if ($obj->{type} eq 'element'){ | |||
174 | ||||||
175 | # | |||||
176 | # first, add any new NS's to the stack | |||||
177 | # | |||||
178 | ||||||
179 | 4 4 | 6 17 | my @keys = keys %{$obj->{attributes}}; | |||
180 | ||||||
181 | 4 | 13 | for my $k(@keys){ | |||
182 | ||||||
183 | 4 | 15 | if ($k =~ /^xmlns:(.*)$/){ | |||
184 | ||||||
185 | 2 2 | 4 12 | push @{$self->{ns_stack}->{$1}}, $obj->{attributes}->{$k}; | |||
186 | 2 | 8 | push @ns_keys, $1; | |||
187 | 2 | 6 | delete $obj->{attributes}->{$k}; | |||
188 | } | |||||
189 | ||||||
190 | 4 | 17 | if ($k eq 'xmlns'){ | |||
191 | ||||||
192 | 2 2 | 4 12 | push @{$self->{ns_stack}->{__default__}}, $obj->{attributes}->{$k}; | |||
193 | 2 | 4 | push @ns_keys, '__default__'; | |||
194 | 2 | 10 | delete $obj->{attributes}->{$k}; | |||
195 | } | |||||
196 | } | |||||
197 | ||||||
198 | ||||||
199 | # | |||||
200 | # now - does this tag have a NS? | |||||
201 | # | |||||
202 | ||||||
203 | 4 | 17 | if ($obj->{name} =~ /^(.*?):(.*)$/){ | |||
204 | ||||||
205 | 1 | 4 | $obj->{local_name} = $2; | |||
206 | 1 | 5 | $obj->{ns_key} = $1; | |||
207 | 1 | 5 | $obj->{ns} = $self->{ns_stack}->{$1}->[-1]; | |||
208 | }else{ | |||||
209 | 3 | 11 | $obj->{local_name} = $obj->{name}; | |||
210 | 3 | 15 | $obj->{ns} = $self->{ns_stack}->{__default__}->[-1]; | |||
211 | } | |||||
212 | ||||||
213 | ||||||
214 | # | |||||
215 | # finally, add xpath-style namespace nodes | |||||
216 | # | |||||
217 | ||||||
218 | 4 | 11 | $obj->{namespaces} = {}; | |||
219 | ||||||
220 | 4 4 | 9 16 | for my $key (keys %{$self->{ns_stack}}){ | |||
221 | ||||||
222 | 9 9 | 13 39 | if (scalar @{$self->{ns_stack}->{$key}}){ | |||
223 | ||||||
224 | 9 | 33 | my $uri = $self->{ns_stack}->{$key}->[-1]; | |||
225 | 9 | 40 | $obj->{namespaces}->{$key} = $uri; | |||
226 | } | |||||
227 | } | |||||
228 | } | |||||
229 | ||||||
230 | ||||||
231 | # | |||||
232 | # descend | |||||
233 | # | |||||
234 | ||||||
235 | 5 | 44 | if ($obj->{type} eq 'root' || $obj->{type} eq 'element'){ | |||
236 | ||||||
237 | 5 5 | 10 19 | for my $child (@{$obj->{children}}){ | |||
238 | ||||||
239 | 4 | 29 | $self->mark_namespaces($child); | |||
240 | } | |||||
241 | } | |||||
242 | ||||||
243 | ||||||
244 | # | |||||
245 | # pop from stack | |||||
246 | # | |||||
247 | ||||||
248 | 5 | 19 | for my $k (@ns_keys){ | |||
249 | 4 4 | 10 23 | pop @{$self->{ns_stack}->{$k}}; | |||
250 | } | |||||
251 | } | |||||
252 | ||||||
253 | sub strip_white { | |||||
254 | 20 | 56 | my ($self, $obj) = @_; | |||
255 | ||||||
256 | 20 | 156 | if ($obj->{type} eq 'root' || $obj->{type} eq 'element'){ | |||
257 | ||||||
258 | 20 | 44 | my $new_kids = []; | |||
259 | ||||||
260 | 20 20 | 36 70 | for my $child (@{$obj->{children}}){ | |||
261 | ||||||
262 | 38 | 187 | if ($child->{type} eq 'text'){ | |||
263 | ||||||
264 | 19 | 94 | if ($child->{content} =~ m/\S/){ | |||
265 | ||||||
266 | 1 1 | 2 5 | push @{$new_kids}, $child; | |||
267 | } | |||||
268 | ||||||
269 | }elsif ($child->{type} eq 'element'){ | |||||
270 | ||||||
271 | 15 | 61 | $self->strip_white($child); | |||
272 | 15 15 | 34 55 | push @{$new_kids}, $child; | |||
273 | }else{ | |||||
274 | 4 4 | 7 18 | push @{$new_kids}, $child; | |||
275 | } | |||||
276 | } | |||||
277 | ||||||
278 | 20 | 68 | $obj->{children} = $new_kids; | |||
279 | } | |||||
280 | } | |||||
281 | ||||||
282 | sub cleanup { | |||||
283 | 64 | 178 | my ($self, $obj) = @_; | |||
284 | ||||||
285 | # | |||||
286 | # cleanup PIs | |||||
287 | # | |||||
288 | ||||||
289 | 64 | 236 | if ($obj->{type} eq 'pi'){ | |||
290 | ||||||
291 | 2 | 15 | my ($x, $y) = split /\s+/, $obj->{content}, 2; | |||
292 | 2 | 6 | $obj->{target} = $x; | |||
293 | 2 | 5 | $obj->{content} = $y; | |||
294 | } | |||||
295 | ||||||
296 | ||||||
297 | # | |||||
298 | # cleanup DTDs | |||||
299 | # | |||||
300 | ||||||
301 | 64 | 233 | if ($obj->{type} eq 'dtd'){ | |||
302 | ||||||
303 | 1 | 6 | my ($x, $y) = split /\s+/, $obj->{content}, 2; | |||
304 | 1 | 3 | $obj->{name} = $x; | |||
305 | 1 | 4 | $obj->{content} = $y; | |||
306 | } | |||||
307 | ||||||
308 | ||||||
309 | # | |||||
310 | # recurse | |||||
311 | # | |||||
312 | ||||||
313 | 64 | 547 | if ($obj->{type} eq 'root' || $obj->{type} eq 'element'){ | |||
314 | ||||||
315 | 38 38 | 70 165 | for my $child (@{$obj->{children}}){ | |||
316 | ||||||
317 | 55 | 200 | $self->cleanup($child); | |||
318 | } | |||||
319 | } | |||||
320 | } | |||||
321 | ||||||
322 | ||||||
323 | 1; |