File Coverage

blib/lib/XML/MyXML/Object.pm
Criterion Covered Total %
statement 182 193 94.3
branch 105 126 83.3
condition 26 31 83.8
subroutine 20 21 95.2
pod 0 12 0.0
total 333 383 86.9


line stmt bran cond sub pod time code
1             package XML::MyXML::Object;
2              
3 5     5   40 use strict;
  5         9  
  5         153  
4 5     5   24 use warnings;
  5         8  
  5         152  
5              
6 5     5   2085 use XML::MyXML::Util 'trim', 'strip_ns';
  5         27  
  5         349  
7              
8 5     5   3927 use Encode;
  5         46889  
  5         420  
9 5     5   52 use Carp;
  5         11  
  5         363  
10 5     5   36 use Scalar::Util 'weaken';
  5         20  
  5         14010  
11              
12             our $VERSION = "1.08";
13              
14             sub new {
15 0     0 0 0 my $class = shift;
16 0         0 my $xml = shift;
17              
18 0         0 return bless XML::MyXML::xml_to_object($xml), $class;
19             }
20              
21             my $ch0 = chr(0);
22             sub _string_unescape {
23 50     50   1591 my $string = shift;
24              
25 50 100       124 defined $string or return undef;
26              
27 33         1993 my $ret = eval "qq${ch0}$string${ch0}";
28 33 50       134 defined $ret or croak "Can't unescape this string: $string";
29              
30 33         151 return $ret;
31             }
32              
33             sub _parse_description {
34 77     77   149 my ($desc) = @_;
35              
36 77         508 my ($el_name, $el_ns, $attrs_str) = $desc =~ /
37             # start anchor
38             ^
39              
40             # element name
41             (
42             (?:
43             \\ \[
44             |
45             \\ \{
46             |
47             [^\[\{]
48             )*
49             )
50              
51             # element namespace
52             (?:
53             # opening curly bracket
54             \{
55              
56             # namespace name
57             ((?: \\ \} | [^\}] )*)
58              
59             # closing curly bracket
60             \}
61             )?
62              
63             # attributes string
64             (.*)
65              
66             # end anchor
67             \z
68             /x;
69              
70 77         228 my @attrs = $attrs_str =~ /
71             # opening square bracket
72             \[
73              
74             # attribute name
75             (
76             # attribute characters
77             (?: \\ \] | \\ \= | \\ \{ | [^\]\=\{] )+
78             )
79              
80             # optional namespace
81             (?:
82             # opening curly bracket
83             \{
84              
85             # namespace name
86             ((?: \\ \} | [^\}] )*)
87              
88             # closing curly bracket
89             \}
90             )?
91              
92             # value option
93             (?:
94             # equals sign
95             \=
96              
97             # value
98             (
99             (?: \\ \] | [^\]] )*
100             )
101             )?
102              
103             # closing square bracket
104             \]
105             /gx;
106              
107 77         113 my %attrs;
108 77         189 while (@attrs) {
109 15         45 my ($attr_name, $attr_ns, $attr_value) = splice @attrs, 0, 3;
110             # $attr_value =~ s/^\"|\"\z//g if defined $attr_value;
111 15         41 $attrs{_string_unescape $attr_name} = {
112             ns => _string_unescape($attr_ns),
113             value => _string_unescape($attr_value),
114             };
115             }
116              
117 77         258 return ($el_name, $el_ns, \%attrs);
118             }
119              
120             sub cmp_element {
121 155     155 0 277 my ($self, $desc) = @_;
122              
123             my ($el_name, $el_ns, $attrs) = ref $desc
124 155 100       411 ? @$desc{qw/ el_name el_ns attrs /}
125             : _parse_description($desc);
126              
127             # check element name
128 155 100       289 if (length $el_name) {
129 147 100       255 if (! defined $el_ns) {
    50          
130 145 100       417 $self->{el_name} eq $el_name or return 0;
131             } elsif (length $el_ns) {
132 2 50       7 $el_name !~ /\:/ or croak 'You can either have a ns requirement, or a ":" in your path segment';
133 2 50       11 exists $self->{ns_data}{"$el_ns:"} or return 0;
134 2 50       10 strip_ns($self->{el_name}) eq $el_name or return 0;
135             } else {
136             # ! grep /\:\z/, keys %{ $self->{ns_data} } or return 0;
137             # $self->{el_name} eq $el_name or return 0;
138 0         0 croak 'empty ns in path segment';
139             }
140             }
141              
142             # check attributes
143 115         266 foreach my $attr_name (keys %$attrs) {
144 50         78 my ($attr_ns, $attr_value) = @{ $attrs->{$attr_name} }{qw/ ns value /};
  50         95  
145 50 100       91 if (! defined $attr_ns) {
    50          
146 48         87 my $actual_attr_value = $self->attr($attr_name);
147 48 100       131 defined $actual_attr_value or return 0;
148 35 100 100     175 ! defined $attr_value or $attr_value eq $actual_attr_value or return 0;
149             } elsif (length $attr_ns) {
150 2 50       10 $attr_name !~ /\:/ or croak 'You can either have a ns requirement, or a ":" in your path segment';
151 2         7 my $actual_attr_value = $self->{ns_data}{"$attr_ns:$attr_name"};
152 2 100       12 defined $actual_attr_value or return 0;
153 1 50 33     6 ! defined $attr_value or $attr_value eq $actual_attr_value or return 0;
154             } else {
155             # my $actual_attr_value = $self->attr($attr_name);
156             # defined $actual_attr_value or return 0;
157             # ! exists $self->{ns_data}{}
158 0         0 croak 'empty ns in path segment';
159             }
160             }
161              
162 82         340 return 1;
163             }
164              
165             sub children {
166 197     197 0 292 my $self = shift;
167 197         291 my $path_segment = shift;
168              
169 197 100       413 $path_segment = '' if ! defined $path_segment;
170              
171 197         267 my @all_children = grep { defined $_->{el_name} } @{$self->{content}};
  299         597  
  197         379  
172 197 100       735 length $path_segment or return @all_children;
173              
174 65         136 my ($el_name, $el_ns, $attrs) = _parse_description($path_segment);
175 65         220 my $desc = { el_name => $el_name, el_ns => $el_ns, attrs => $attrs };
176              
177 65         186 return grep $_->cmp_element($desc), @all_children;
178             }
179              
180             sub path {
181 41     41 0 8909 my $self = shift;
182 41         86 my $path = shift;
183              
184 41         76 my $original_path = $path;
185 41         129 my $path_starts_with_root = $path =~ m|^/|;
186 41 100       149 $path = "/$path" unless $path_starts_with_root;
187 41         491 my @path_segments = $path =~ m!
188             # slash
189             \/
190              
191             (
192             # allowed strings
193             (?:
194             # escaped "/"
195             \\ \/
196             |
197             # escaped "["
198             \\ \[
199             |
200             # escaped "{"
201             \\ \{
202             |
203             # non- "/", "[", "]"
204             [^\/\[\{]
205             |
206             # attribute
207             \[
208             (?: \\ \] | [^\]] )*
209             \]
210             |
211             # namespace
212             \{
213             (?: \\ \} | [^\}] )*
214             \}
215             )*
216             )
217             !gx;
218              
219 41         92 my @result = ($self);
220 41 100 100     121 $self->cmp_element(shift @path_segments) or return if $path_starts_with_root;
221 40         92 foreach my $path_segment (@path_segments) {
222 58 50       157 @result = map $_->children($path_segment), @result or return;
223             }
224 40 100       269 return wantarray ? @result : $result[0];
225             }
226              
227             sub text {
228 53     53 0 87 my $self = shift;
229 53 100 100     186 my $flags = (@_ and ref $_[-1]) ? pop : {};
230 53 100       150 my $set_value = @_ ? (defined $_[0] ? shift : '') : undef;
    100          
231              
232 53 100       120 if (! defined $set_value) {
233 50         79 my $value = '';
234 50 100       124 if ($self->{content}) {
235 28         38 $value .= $_->text($flags) foreach @{ $self->{content} };
  28         86  
236             }
237 50 100       104 if ($self->{text}) {
238 22         46 my $temp_value = $self->{text};
239 22 100       53 $temp_value = trim $temp_value if $flags->{strip};
240 22         49 $value .= $temp_value;
241             }
242 50         201 return $value;
243             } else {
244 3 100       9 if (length $set_value) {
245 1         4 my $entry = bless {
246             text => $set_value,
247             parent => $self
248             }, 'XML::MyXML::Object';
249 1         5 weaken $entry->{parent};
250 1         7 $self->{content} = [ $entry ];
251             } else {
252 2         9 $self->{content} = [];
253             }
254             }
255             }
256              
257             *value = \&text;
258              
259             sub inner_xml {
260 7     7 0 2280 my $self = shift;
261 7 100 100     40 my $flags = (@_ and ref $_[-1]) ? pop : {};
262 7 100       26 my $set_xml = @_ ? defined $_[0] ? shift : '' : undef;
    100          
263              
264 7 100       18 if (! defined $set_xml) {
265 3         11 my $xml = $self->to_xml($flags);
266 3         20 $xml =~ s/^\<.*?\>//s;
267 3         15 $xml =~ s/\<\/[^\>]*\>\z//s; # nothing to remove if empty element
268 3         17 return $xml;
269             } else {
270 4         13 my $xml = "
$set_xml
";
271 4         31 my $obj = XML::MyXML::xml_to_object($xml, $flags);
272 4         24 $self->{content} = [];
273 4 100       10 foreach my $child (@{ $obj->{content} || [] }) {
  4         25  
274 5         9 $child->{parent} = $self;
275 5         17 weaken $child->{parent};
276 5         7 push @{ $self->{content} }, $child;
  5         9  
277 5 100       19 $child->_apply_namespace_declarations if $child->{el_name};
278             }
279             }
280             }
281              
282             sub attr {
283 199     199 0 288 my $self = shift;
284 199         278 my $attr_name = shift;
285 199 50       408 my $flags = ref $_[-1] ? pop : {};
286 199         333 my ($set_to, $must_set);
287 199 100       434 if (@_) {
288 4         10 $set_to = shift;
289 4         7 $must_set = 1;
290             }
291              
292 199 100       389 if (defined $attr_name) {
293 67 100       117 if ($must_set) {
294 4 100       12 if (defined ($set_to)) {
295 3         9 $self->{attrs}{$attr_name} = $set_to;
296             } else {
297 1         4 delete $self->{attrs}{$attr_name};
298             }
299 4 100       22 if ($attr_name =~ /^xmlns(\:|\z)/) {
300 2         5 $self->_apply_namespace_declarations;
301             }
302 4         13 return $set_to;
303             } else {
304 63         227 return $self->{attrs}->{$attr_name};
305             }
306             } else {
307 132         169 return %{$self->{attrs}};
  132         568  
308             }
309             }
310              
311             sub tag {
312 5     5 0 17 my $self = shift;
313 5   100     24 my $flags = shift || {};
314              
315 5         10 my $el_name = $self->{el_name};
316 5 50       13 if (defined $el_name) {
317 5 100       23 $el_name =~ s/^.*\:// if $flags->{strip_ns};
318 5         29 return $el_name;
319             } else {
320 0         0 return undef;
321             }
322             }
323              
324             *name = \&tag;
325              
326             sub parent {
327 2     2 0 6 my $self = shift;
328              
329 2         11 return $self->{parent};
330             }
331              
332             sub simplify {
333 13     13 0 27 my $self = shift;
334 13   100     34 my $flags = shift || {};
335              
336 13         49 my $simple = XML::MyXML::_objectarray_to_simple([$self], $flags);
337              
338 13 100       43 if ($flags->{internal}) {
339 5 0       23 $simple =
    50          
340             ref $simple eq 'HASH' ? (values %$simple)[0]
341             : ref $simple eq 'ARRAY' ? $simple->[1]
342             : croak;
343             }
344              
345 13         72 return $simple;
346             }
347              
348             sub to_xml {
349 21     21 0 1526 my $self = shift;
350 21   100     81 my $flags = shift || {};
351              
352 21         40 my $decl = '';
353 21 50       54 $decl .= qq'\n' if $flags->{complete};
354 21         82 my $xml = XML::MyXML::_objectarray_to_xml([$self]);
355             $xml = XML::MyXML::tidy_xml($xml, {
356             %$flags,
357             bytes => 0,
358             complete => 0,
359             save => undef
360 21 100       80 }) if $flags->{tidy};
361 21         50 $xml = $decl . $xml;
362 21 50       66 if (defined $flags->{save}) {
363 0 0       0 open my $fh, '>', $flags->{save} or croak "Error: Couldn't open file '$flags->{save}' for writing: $!";
364 0         0 binmode $fh, ':encoding(UTF-8)';
365 0         0 print $fh $xml;
366 0         0 close $fh;
367             }
368 21 100       65 $xml = encode_utf8 $xml if $flags->{bytes};
369 21         131 return $xml;
370             }
371              
372             sub to_tidy_xml {
373 1     1 0 791 my $self = shift;
374 1   50     6 my $flags = shift || {};
375              
376 1         6 return $self->to_xml({ %$flags, tidy => 1 });
377             }
378              
379             sub _apply_namespace_declarations {
380 132     132   215 my $self = shift;
381              
382             # only elements
383 132 50       322 $self->{el_name} or return;
384              
385 132         303 my %attr = $self->attr;
386              
387             # parse namespace declarations
388 132         290 my ($ns_info, @cancel_declarations) = ({});
389 132         370 foreach my $ns_decl_attr_name (grep /^xmlns(\:|\z)/, keys %attr) {
390 7         31 my ($ns_prefix) = $ns_decl_attr_name =~ /^xmlns(?:\:(.+))?\z/;
391 7 100       23 $ns_prefix = '' if ! defined $ns_prefix;
392 7 100       22 if (length $attr{$ns_decl_attr_name}) {
393 5         17 $ns_info->{$ns_prefix} = $attr{$ns_decl_attr_name};
394             } else {
395 2         7 push @cancel_declarations, $ns_prefix;
396             }
397             }
398              
399             # insert these declarations into the full_ns_info hashref
400             $self->{full_ns_info} = (%$ns_info or @cancel_declarations) ? {
401 6         36 %{ $self->{parent}{full_ns_info} },
402             %$ns_info,
403 132 100 100     614 } : $self->{parent}{full_ns_info};
404              
405             # remove cancelled declarations (can cancel with ns name = "")
406 132         247 delete @{ $self->{full_ns_info} }{@cancel_declarations};
  132         264  
407              
408             # ns_data is...
409             # $ns_name: => undef for element name
410             # $ns_name:$attr_localpart => $attr_value for attributes
411 132         267 $self->{ns_data} = {};
412              
413             # apply all active declarations to element
414 132         225 my $el_name = $self->{el_name};
415 132         340 my $num_colons = () = $el_name =~ /(\:)/g;
416 132         207 my $ns_name = do {
417 132 100       277 if ($num_colons == 0) {
    50          
418 128         237 $self->{full_ns_info}{''};
419             } elsif ($num_colons == 1) {
420 4         31 my ($prefix) = $el_name =~ /^(.+)?\:./; # colon must not be at start or end
421 4 50       20 defined $prefix ? $self->{full_ns_info}{$prefix} : undef;
422             } else {
423 0         0 undef;
424             }
425             };
426 132 100 66     355 $self->{ns_data}{"$ns_name:"} = undef if defined $ns_name and length $ns_name;
427              
428             # apply all active declarations to attributes
429 132         302 foreach my $attr_name (keys %attr) {
430 58 100       157 if ($attr_name =~ /^([^\:]+)\:([^\:]+)\z/) { # if has one colon, not at the edges
431 7         25 my ($prefix, $localpart) = ($1, $2);
432 7         15 my $ns_name = $self->{full_ns_info}{$prefix};
433 7 100 66     43 $self->{ns_data}{"$ns_name:$localpart"} = $attr{$attr_name}
434             if defined $ns_name and length $ns_name;
435             }
436             }
437              
438             # continue by applying to all children (and further ancestors)
439 132         307 $_->_apply_namespace_declarations foreach $self->children;
440             }
441              
442             1;