line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
# Copyrights 2008-2020 by [Mark Overmeer]. |
2
|
|
|
|
|
|
|
# For other contributors see ChangeLog. |
3
|
|
|
|
|
|
|
# See the manual pages for details on the licensing terms. |
4
|
|
|
|
|
|
|
# Pod stripped from pm file by OODoc 2.02. |
5
|
|
|
|
|
|
|
# This code is part of distribution XML-LibXML-Simple. Meta-POD processed |
6
|
|
|
|
|
|
|
# with OODoc into POD and HTML manual-pages. See README.md |
7
|
|
|
|
|
|
|
# Copyright Mark Overmeer. Licensed under the same terms as Perl itself. |
8
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
package XML::LibXML::Simple; |
10
|
2
|
|
|
2
|
|
47401
|
use vars '$VERSION'; |
|
2
|
|
|
|
|
4
|
|
|
2
|
|
|
|
|
110
|
|
11
|
|
|
|
|
|
|
$VERSION = '1.00'; |
12
|
|
|
|
|
|
|
|
13
|
2
|
|
|
2
|
|
11
|
use base 'Exporter'; |
|
2
|
|
|
|
|
4
|
|
|
2
|
|
|
|
|
210
|
|
14
|
|
|
|
|
|
|
|
15
|
2
|
|
|
2
|
|
14
|
use strict; |
|
2
|
|
|
|
|
3
|
|
|
2
|
|
|
|
|
52
|
|
16
|
2
|
|
|
2
|
|
9
|
use warnings; |
|
2
|
|
|
|
|
4
|
|
|
2
|
|
|
|
|
98
|
|
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
our @EXPORT = qw(XMLin); |
19
|
|
|
|
|
|
|
our @EXPORT_OK = qw(xml_in); |
20
|
|
|
|
|
|
|
|
21
|
2
|
|
|
2
|
|
526
|
use XML::LibXML (); |
|
2
|
|
|
|
|
33390
|
|
|
2
|
|
|
|
|
44
|
|
22
|
2
|
|
|
2
|
|
11
|
use File::Basename qw/fileparse/; |
|
2
|
|
|
|
|
3
|
|
|
2
|
|
|
|
|
152
|
|
23
|
2
|
|
|
2
|
|
12
|
use File::Spec (); |
|
2
|
|
|
|
|
4
|
|
|
2
|
|
|
|
|
23
|
|
24
|
2
|
|
|
2
|
|
8
|
use Carp; |
|
2
|
|
|
|
|
3
|
|
|
2
|
|
|
|
|
90
|
|
25
|
2
|
|
|
2
|
|
9
|
use Scalar::Util qw/blessed/; |
|
2
|
|
|
|
|
4
|
|
|
2
|
|
|
|
|
69
|
|
26
|
|
|
|
|
|
|
|
27
|
2
|
|
|
2
|
|
10
|
use Data::Dumper; #to be removed |
|
2
|
|
|
|
|
2
|
|
|
2
|
|
|
|
|
5136
|
|
28
|
|
|
|
|
|
|
|
29
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
my %known_opts = map +($_ => 1), |
31
|
|
|
|
|
|
|
qw(keyattr keeproot forcecontent contentkey noattr searchpath |
32
|
|
|
|
|
|
|
forcearray grouptags nsexpand normalisespace normalizespace |
33
|
|
|
|
|
|
|
valueattr nsstrip parser parseropts hooknodes suppressempty); |
34
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
my @default_attributes = qw(name key id); |
36
|
|
|
|
|
|
|
my $default_content_key = 'content'; |
37
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
#------------- |
39
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
sub new(@) |
41
|
84
|
|
|
84
|
1
|
140
|
{ my $class = shift; |
42
|
84
|
|
|
|
|
134
|
my $self = bless {}, $class; |
43
|
84
|
|
|
|
|
158
|
my $opts = $self->{opts} = $self->_take_opts(@_); |
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
# parser object cannot be reused |
46
|
|
|
|
|
|
|
!defined $opts->{parser} |
47
|
84
|
50
|
|
|
|
167
|
or error __x"parser option for XMLin only"; |
48
|
|
|
|
|
|
|
|
49
|
84
|
|
|
|
|
125
|
$self; |
50
|
|
|
|
|
|
|
} |
51
|
|
|
|
|
|
|
|
52
|
|
|
|
|
|
|
#------------- |
53
|
|
|
|
|
|
|
|
54
|
|
|
|
|
|
|
sub XMLin |
55
|
84
|
50
|
66
|
84
|
1
|
75319
|
{ my $self = @_ > 1 && blessed $_[0] && $_[0]->isa(__PACKAGE__) ? shift |
56
|
|
|
|
|
|
|
: __PACKAGE__->new; |
57
|
84
|
|
|
|
|
113
|
my $target = shift; |
58
|
|
|
|
|
|
|
|
59
|
84
|
|
|
|
|
122
|
my $this = $self->_take_opts(@_); |
60
|
82
|
|
|
|
|
173
|
my $opts = $self->_init($self->{opts}, $this); |
61
|
|
|
|
|
|
|
|
62
|
82
|
50
|
|
|
|
145
|
my $xml = $self->_get_xml($target, $opts) |
63
|
|
|
|
|
|
|
or return; |
64
|
|
|
|
|
|
|
|
65
|
79
|
50
|
|
|
|
432
|
if(my $cb = $opts->{hooknodes}) |
66
|
0
|
|
|
|
|
0
|
{ $self->{XCS_hooks} = $cb->($self, $xml); |
67
|
|
|
|
|
|
|
} |
68
|
|
|
|
|
|
|
|
69
|
79
|
|
|
|
|
145
|
my $top = $self->collapse($xml, $opts); |
70
|
79
|
100
|
|
|
|
131
|
if($opts->{keeproot}) |
71
|
|
|
|
|
|
|
{ my $subtop |
72
|
1
|
50
|
33
|
|
|
7
|
= $opts->{forcearray_always} && ref $top ne 'ARRAY' ? [$top] : $top; |
73
|
1
|
|
|
|
|
12
|
$top = +{ $xml->localName => $subtop }; |
74
|
|
|
|
|
|
|
} |
75
|
|
|
|
|
|
|
|
76
|
79
|
|
|
|
|
190
|
$top; |
77
|
|
|
|
|
|
|
} |
78
|
|
|
|
|
|
|
*xml_in = \&XMLin; |
79
|
|
|
|
|
|
|
|
80
|
|
|
|
|
|
|
sub _get_xml($$) |
81
|
82
|
|
|
82
|
|
125
|
{ my ($self, $source, $opts) = @_; |
82
|
|
|
|
|
|
|
|
83
|
82
|
100
|
|
|
|
135
|
$source = $self->default_data_source($opts) |
84
|
|
|
|
|
|
|
unless defined $source; |
85
|
|
|
|
|
|
|
|
86
|
82
|
100
|
|
|
|
120
|
$source = \*STDIN |
87
|
|
|
|
|
|
|
if $source eq '-'; |
88
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
my $parser = $opts->{parser} |
90
|
82
|
|
33
|
|
|
198
|
|| $self->_create_parser($opts->{parseropts}); |
91
|
|
|
|
|
|
|
|
92
|
|
|
|
|
|
|
my $xml |
93
|
|
|
|
|
|
|
= blessed $source && |
94
|
|
|
|
|
|
|
( $source->isa('XML::LibXML::Document') |
95
|
|
|
|
|
|
|
|| $source->isa('XML::LibXML::Element' )) ? $source |
96
|
|
|
|
|
|
|
: ref $source eq 'SCALAR' ? $parser->parse_string($$source) |
97
|
|
|
|
|
|
|
: ref $source ? $parser->parse_fh($source) |
98
|
|
|
|
|
|
|
: $source =~ m{^\s*\<.*?\>\s*$}s ? $parser->parse_string($source) |
99
|
|
|
|
|
|
|
: $parser->parse_file |
100
|
82
|
100
|
66
|
|
|
6977
|
($self->find_xml_file($source, @{$opts->{searchpath}})); |
|
7
|
100
|
|
|
|
18
|
|
|
|
50
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
101
|
|
|
|
|
|
|
|
102
|
79
|
50
|
|
|
|
11562
|
$xml = $xml->documentElement |
103
|
|
|
|
|
|
|
if $xml->isa('XML::LibXML::Document'); |
104
|
|
|
|
|
|
|
|
105
|
79
|
|
|
|
|
514
|
$xml; |
106
|
|
|
|
|
|
|
} |
107
|
|
|
|
|
|
|
|
108
|
|
|
|
|
|
|
sub _create_parser(@) |
109
|
82
|
|
|
82
|
|
106
|
{ my $self = shift; |
110
|
82
|
50
|
|
|
|
172
|
my @popt = @_ != 1 ? @_ : ref $_[0] eq 'HASH' ? %{$_[0]} : @{$_[0]}; |
|
82
|
50
|
|
|
|
152
|
|
|
0
|
|
|
|
|
0
|
|
111
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
XML::LibXML->new |
113
|
|
|
|
|
|
|
( line_numbers => 1 |
114
|
|
|
|
|
|
|
, no_network => 1 |
115
|
|
|
|
|
|
|
, expand_xinclude => 0 |
116
|
|
|
|
|
|
|
, expand_entities => 1 |
117
|
|
|
|
|
|
|
, load_ext_dtd => 0 |
118
|
|
|
|
|
|
|
, ext_ent_handler => |
119
|
0
|
|
|
0
|
|
0
|
sub { alert __x"parsing external entities disabled"; '' } |
|
0
|
|
|
|
|
0
|
|
120
|
|
|
|
|
|
|
, @popt |
121
|
82
|
|
|
|
|
392
|
); |
122
|
|
|
|
|
|
|
} |
123
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
sub _take_opts(@) |
125
|
168
|
|
|
168
|
|
198
|
{ my $self = shift; |
126
|
|
|
|
|
|
|
|
127
|
168
|
|
|
|
|
184
|
my %opts; |
128
|
168
|
100
|
|
|
|
341
|
@_ % 2==0 |
129
|
|
|
|
|
|
|
or die "ERROR: odd number of options.\n"; |
130
|
|
|
|
|
|
|
|
131
|
167
|
|
|
|
|
273
|
while(@_) |
132
|
119
|
|
|
|
|
205
|
{ my ($key, $val) = (shift, shift); |
133
|
119
|
|
|
|
|
159
|
my $lkey = lc $key; |
134
|
119
|
|
|
|
|
186
|
$lkey =~ s/_//g; |
135
|
119
|
100
|
|
|
|
415
|
$known_opts{$lkey} or croak "Unrecognised option: $key"; |
136
|
118
|
|
|
|
|
267
|
$opts{$lkey} = $val; |
137
|
|
|
|
|
|
|
} |
138
|
|
|
|
|
|
|
|
139
|
166
|
|
|
|
|
296
|
\%opts; |
140
|
|
|
|
|
|
|
} |
141
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
# Returns the name of the XML file to parse if no filename or XML string |
143
|
|
|
|
|
|
|
# was provided explictly. |
144
|
|
|
|
|
|
|
|
145
|
|
|
|
|
|
|
sub default_data_source($) |
146
|
1
|
|
|
1
|
0
|
2
|
{ my ($self, $opts) = @_; |
147
|
|
|
|
|
|
|
|
148
|
1
|
|
|
|
|
66
|
my ($basename, $script_dir, $ext) = fileparse $0, qr[\.[^\.]+]; |
149
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
# Add script directory to searchpath |
151
|
1
|
50
|
|
|
|
6
|
unshift @{$opts->{searchpath}}, $script_dir |
|
1
|
|
|
|
|
3
|
|
152
|
|
|
|
|
|
|
if $script_dir; |
153
|
|
|
|
|
|
|
|
154
|
1
|
|
|
|
|
4
|
"$basename.xml"; |
155
|
|
|
|
|
|
|
} |
156
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
sub _init($$) |
158
|
82
|
|
|
82
|
|
123
|
{ my ($self, $global, $this) = @_; |
159
|
82
|
|
|
|
|
253
|
my %opt = (%$global, %$this); |
160
|
|
|
|
|
|
|
|
161
|
82
|
100
|
|
|
|
155
|
if(defined $opt{contentkey}) |
162
|
48
|
|
|
|
|
192
|
{ $opt{collapseagain} = $opt{contentkey} =~ s/^\-// } |
163
|
34
|
|
|
|
|
47
|
else { $opt{contentkey} = $default_content_key } |
164
|
|
|
|
|
|
|
|
165
|
82
|
|
100
|
|
|
388
|
$opt{normalisespace} ||= $opt{normalizespace} || 0; |
|
|
|
100
|
|
|
|
|
166
|
|
|
|
|
|
|
|
167
|
82
|
|
100
|
|
|
255
|
$opt{searchpath} ||= []; |
168
|
|
|
|
|
|
|
ref $opt{searchpath} eq 'ARRAY' |
169
|
82
|
100
|
|
|
|
174
|
or $opt{searchpath} = [ $opt{searchpath} ]; |
170
|
|
|
|
|
|
|
|
171
|
82
|
|
100
|
|
|
183
|
my $fa = delete $opt{forcearray} || 0; |
172
|
82
|
|
|
|
|
105
|
my (@fa_regex, %fa_elem); |
173
|
82
|
100
|
|
|
|
118
|
if(ref $fa) |
174
|
6
|
100
|
|
|
|
16
|
{ foreach (ref $fa eq 'ARRAY' ? @$fa : $fa) |
175
|
8
|
100
|
|
|
|
15
|
{ if(ref $_ eq 'Regexp') { push @fa_regex, $_ } |
|
3
|
|
|
|
|
6
|
|
176
|
5
|
|
|
|
|
11
|
else { $fa_elem{$_} = 1 } |
177
|
|
|
|
|
|
|
} |
178
|
|
|
|
|
|
|
} |
179
|
76
|
|
|
|
|
98
|
else { $opt{forcearray_always} = $fa } |
180
|
82
|
|
|
|
|
110
|
$opt{forcearray_regex} = \@fa_regex; |
181
|
82
|
|
|
|
|
129
|
$opt{forcearray_elem} = \%fa_elem; |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
# Special cleanup for {keyattr} which could be arrayref or hashref, |
184
|
|
|
|
|
|
|
# which behave differently. |
185
|
|
|
|
|
|
|
|
186
|
82
|
|
100
|
|
|
176
|
my $ka = $opt{keyattr} || \@default_attributes; |
187
|
82
|
100
|
|
|
|
140
|
$ka = [ $ka ] unless ref $ka; |
188
|
|
|
|
|
|
|
|
189
|
82
|
100
|
|
|
|
139
|
if(ref $ka eq 'ARRAY') |
|
|
50
|
|
|
|
|
|
190
|
62
|
100
|
|
|
|
102
|
{ if(@$ka) { $opt{keyattr} = $ka } |
|
60
|
|
|
|
|
91
|
|
191
|
2
|
|
|
|
|
3
|
else { delete $opt{keyattr} } |
192
|
|
|
|
|
|
|
} |
193
|
|
|
|
|
|
|
elsif(ref $ka eq 'HASH') |
194
|
|
|
|
|
|
|
{ # Convert keyattr => { elem => '+attr' } |
195
|
|
|
|
|
|
|
# to keyattr => { elem => [ 'attr', '+' ] } |
196
|
20
|
|
|
|
|
22
|
my %at; |
197
|
20
|
|
|
|
|
57
|
while(my($k,$v) = each %$ka) |
198
|
23
|
|
|
|
|
86
|
{ $v =~ /^(\+|-)?(.*)$/; |
199
|
23
|
|
100
|
|
|
128
|
$at{$k} = [ $2, $1 || '' ]; |
200
|
|
|
|
|
|
|
} |
201
|
20
|
|
|
|
|
36
|
$opt{keyattr} = \%at; |
202
|
|
|
|
|
|
|
} |
203
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
# Special cleanup for {valueattr} which could be arrayref or hashref |
205
|
|
|
|
|
|
|
|
206
|
82
|
|
100
|
|
|
188
|
my $va = delete $opt{valueattr} || {}; |
207
|
82
|
100
|
|
|
|
155
|
$va = +{ map +($_ => 1), @$va } if ref $va eq 'ARRAY'; |
208
|
82
|
|
|
|
|
119
|
$opt{valueattrlist} = $va; |
209
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
# make sure there's nothing weird in {grouptags} |
211
|
|
|
|
|
|
|
|
212
|
82
|
50
|
66
|
|
|
161
|
!$opt{grouptags} || ref $opt{grouptags} eq 'HASH' |
213
|
|
|
|
|
|
|
or croak "Illegal value for 'GroupTags' option -expected a hashref"; |
214
|
|
|
|
|
|
|
|
215
|
82
|
|
50
|
|
|
242
|
$opt{parseropts} ||= {}; |
216
|
|
|
|
|
|
|
|
217
|
82
|
|
|
|
|
154
|
\%opt; |
218
|
|
|
|
|
|
|
} |
219
|
|
|
|
|
|
|
|
220
|
|
|
|
|
|
|
sub find_xml_file($@) |
221
|
7
|
|
|
7
|
0
|
14
|
{ my ($self, $file) = (shift, shift); |
222
|
7
|
100
|
|
|
|
18
|
my @search_path = @_ ? @_ : '.'; |
223
|
|
|
|
|
|
|
|
224
|
7
|
|
|
|
|
134
|
my ($filename, $filedir) = fileparse $file; |
225
|
|
|
|
|
|
|
|
226
|
7
|
100
|
|
|
|
92
|
if($filename eq $file) |
|
|
100
|
|
|
|
|
|
227
|
4
|
|
|
|
|
8
|
{ foreach my $path (@search_path) |
228
|
6
|
|
|
|
|
48
|
{ my $fullpath = File::Spec->catfile($path, $file); |
229
|
6
|
100
|
|
|
|
126
|
return $fullpath if -e $fullpath; |
230
|
|
|
|
|
|
|
} |
231
|
|
|
|
|
|
|
} |
232
|
|
|
|
|
|
|
elsif(-e $file) # Ignore searchpath if dir component |
233
|
2
|
|
|
|
|
14
|
{ return $file; |
234
|
|
|
|
|
|
|
} |
235
|
|
|
|
|
|
|
|
236
|
3
|
|
|
|
|
10
|
local $" = ':'; |
237
|
3
|
|
|
|
|
38
|
die "data source $file not found in @search_path\n"; |
238
|
|
|
|
|
|
|
} |
239
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
sub _add_kv($$$$) |
241
|
540
|
|
|
540
|
|
849
|
{ my ($d, $k, $v, $opts) = @_; |
242
|
|
|
|
|
|
|
|
243
|
540
|
100
|
66
|
|
|
1832
|
if(defined $d->{$k}) |
|
|
100
|
66
|
|
|
|
|
|
|
100
|
100
|
|
|
|
|
|
|
100
|
|
|
|
|
|
244
|
|
|
|
|
|
|
{ # Combine duplicate attributes into arrayref if required |
245
|
97
|
100
|
|
|
|
188
|
if(ref $d->{$k} eq 'ARRAY') { push @{$d->{$k}}, $v } |
|
46
|
|
|
|
|
52
|
|
|
46
|
|
|
|
|
94
|
|
246
|
51
|
|
|
|
|
109
|
else { $d->{$k} = [ $d->{$k}, $v ] } } |
247
|
2
|
|
|
|
|
3
|
elsif(ref $v eq 'ARRAY') { push @{$d->{$k}}, $v } |
|
2
|
|
|
|
|
6
|
|
248
|
|
|
|
|
|
|
elsif(ref $v eq 'HASH' |
249
|
|
|
|
|
|
|
&& $k ne $opts->{contentkey} |
250
|
24
|
|
|
|
|
33
|
&& $opts->{forcearray_always}) { push @{$d->{$k}}, $v } |
|
24
|
|
|
|
|
56
|
|
251
|
|
|
|
|
|
|
elsif($opts->{forcearray_elem}{$k} |
252
|
412
|
|
|
|
|
980
|
|| grep $k =~ $_, @{$opts->{forcearray_regex}} |
253
|
12
|
|
|
|
|
18
|
) { push @{$d->{$k}}, $v } |
|
12
|
|
|
|
|
32
|
|
254
|
405
|
|
|
|
|
706
|
else { $d->{$k} = $v } |
255
|
540
|
|
|
|
|
1058
|
$d->{$k}; |
256
|
|
|
|
|
|
|
} |
257
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
# Takes the parse tree that XML::LibXML::Parser produced from the supplied |
259
|
|
|
|
|
|
|
# XML and recurse through it 'collapsing' unnecessary levels of indirection |
260
|
|
|
|
|
|
|
# (nested arrays etc) to produce a data structure that is easier to work with. |
261
|
|
|
|
|
|
|
|
262
|
|
|
|
|
|
|
sub _expand_name($) |
263
|
0
|
|
|
0
|
|
0
|
{ my $node = shift; |
264
|
0
|
|
0
|
|
|
0
|
my $uri = $node->namespaceURI || ''; |
265
|
0
|
0
|
|
|
|
0
|
(length $uri ? "{$uri}" : '') . $node->localName; |
266
|
|
|
|
|
|
|
} |
267
|
|
|
|
|
|
|
|
268
|
|
|
|
|
|
|
sub collapse($$) |
269
|
383
|
|
|
383
|
0
|
535
|
{ my ($self, $xml, $opts) = @_; |
270
|
383
|
50
|
|
|
|
697
|
$xml->isa('XML::LibXML::Element') or return; |
271
|
|
|
|
|
|
|
|
272
|
383
|
|
|
|
|
424
|
my (%data, $text); |
273
|
383
|
|
|
|
|
453
|
my $hooks = $self->{XCS_hooks}; |
274
|
|
|
|
|
|
|
|
275
|
383
|
100
|
|
|
|
521
|
unless($opts->{noattr}) |
276
|
|
|
|
|
|
|
{ |
277
|
|
|
|
|
|
|
ATTR: |
278
|
366
|
|
|
|
|
622
|
foreach my $attr ($xml->attributes) |
279
|
|
|
|
|
|
|
{ |
280
|
236
|
|
|
|
|
905
|
my $value; |
281
|
236
|
50
|
33
|
|
|
407
|
if($hooks && (my $hook = $hooks->{$attr->unique_key})) |
282
|
0
|
|
|
|
|
0
|
{ $value = $hook->($attr); |
283
|
0
|
0
|
|
|
|
0
|
defined $value or next ATTR; |
284
|
|
|
|
|
|
|
} |
285
|
|
|
|
|
|
|
else |
286
|
236
|
|
|
|
|
592
|
{ $value = $attr->value; |
287
|
|
|
|
|
|
|
} |
288
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
$value = $self->normalise_space($value) |
290
|
236
|
100
|
66
|
|
|
676
|
if !ref $value && $opts->{normalisespace}==2; |
291
|
|
|
|
|
|
|
|
292
|
|
|
|
|
|
|
my $name |
293
|
|
|
|
|
|
|
= !$attr->isa('XML::LibXML::Attr') ? $attr->nodeName |
294
|
|
|
|
|
|
|
: $opts->{nsexpand} ? _expand_name($attr) |
295
|
236
|
50
|
|
|
|
816
|
: $opts->{nsstrip} ? $attr->localName |
|
|
50
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
296
|
|
|
|
|
|
|
: $attr->nodeName; |
297
|
|
|
|
|
|
|
|
298
|
236
|
|
|
|
|
369
|
_add_kv \%data, $name => $value, $opts; |
299
|
|
|
|
|
|
|
} |
300
|
|
|
|
|
|
|
} |
301
|
383
|
|
|
|
|
1794
|
my $nr_attrs = keys %data; |
302
|
383
|
|
|
|
|
435
|
my $nr_elems = 0; |
303
|
|
|
|
|
|
|
|
304
|
|
|
|
|
|
|
CHILD: |
305
|
383
|
|
|
|
|
598
|
foreach my $child ($xml->childNodes) |
306
|
|
|
|
|
|
|
{ |
307
|
840
|
100
|
|
|
|
3376
|
if($child->isa('XML::LibXML::Text')) |
308
|
536
|
|
|
|
|
1379
|
{ $text .= $child->data; |
309
|
536
|
|
|
|
|
904
|
next CHILD; |
310
|
|
|
|
|
|
|
} |
311
|
|
|
|
|
|
|
|
312
|
304
|
50
|
|
|
|
542
|
$child->isa('XML::LibXML::Element') |
313
|
|
|
|
|
|
|
or next CHILD; |
314
|
|
|
|
|
|
|
|
315
|
304
|
|
|
|
|
317
|
$nr_elems++; |
316
|
|
|
|
|
|
|
|
317
|
304
|
|
|
|
|
310
|
my $v; |
318
|
304
|
50
|
33
|
|
|
498
|
if($hooks && (my $hook = $hooks->{$child->unique_key})) |
319
|
0
|
|
|
|
|
0
|
{ $v = $hook->($child) } |
320
|
304
|
|
|
|
|
493
|
else { $v = $self->collapse($child, $opts) } |
321
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
next CHILD |
323
|
304
|
0
|
33
|
|
|
529
|
if ! defined $v && $opts->{suppressempty}; |
324
|
|
|
|
|
|
|
|
325
|
|
|
|
|
|
|
my $name |
326
|
|
|
|
|
|
|
= $opts->{nsexpand} ? _expand_name($child) |
327
|
304
|
50
|
|
|
|
940
|
: $opts->{nsstrip} ? $child->localName |
|
|
50
|
|
|
|
|
|
328
|
|
|
|
|
|
|
: $child->nodeName; |
329
|
|
|
|
|
|
|
|
330
|
304
|
|
|
|
|
505
|
_add_kv \%data, $name => $v, $opts; |
331
|
|
|
|
|
|
|
} |
332
|
|
|
|
|
|
|
|
333
|
|
|
|
|
|
|
$text = $self->normalise_space($text) |
334
|
383
|
100
|
100
|
|
|
1454
|
if defined $text && $opts->{normalisespace}==2; |
335
|
|
|
|
|
|
|
|
336
|
383
|
100
|
100
|
|
|
3357
|
return $opts->{forcecontent} ? { $opts->{contentkey} => $text } : $text |
|
|
100
|
|
|
|
|
|
337
|
|
|
|
|
|
|
if $nr_attrs+$nr_elems==0 && defined $text; |
338
|
|
|
|
|
|
|
|
339
|
247
|
100
|
100
|
|
|
501
|
$data{$opts->{contentkey}} = $text |
340
|
|
|
|
|
|
|
if defined $text && $nr_elems==0; |
341
|
|
|
|
|
|
|
|
342
|
|
|
|
|
|
|
# Roll up 'value' attributes (but only if no nested elements) |
343
|
|
|
|
|
|
|
|
344
|
247
|
100
|
|
|
|
405
|
if(keys %data==1) |
345
|
101
|
|
|
|
|
208
|
{ my ($k) = keys %data; |
346
|
101
|
100
|
|
|
|
206
|
return $data{$k} if $opts->{valueattrlist}{$k}; |
347
|
|
|
|
|
|
|
} |
348
|
|
|
|
|
|
|
|
349
|
|
|
|
|
|
|
# Turn arrayrefs into hashrefs if key fields present |
350
|
|
|
|
|
|
|
|
351
|
240
|
100
|
|
|
|
380
|
if($opts->{keyattr}) |
352
|
233
|
|
|
|
|
574
|
{ while(my ($key, $val) = each %data) |
353
|
459
|
100
|
|
|
|
1155
|
{ $data{$key} = $self->array_to_hash($key, $val, $opts) |
354
|
|
|
|
|
|
|
if ref $val eq 'ARRAY'; |
355
|
|
|
|
|
|
|
} |
356
|
|
|
|
|
|
|
} |
357
|
|
|
|
|
|
|
|
358
|
|
|
|
|
|
|
# disintermediate grouped tags |
359
|
|
|
|
|
|
|
|
360
|
240
|
100
|
|
|
|
398
|
if(my $gr = $opts->{grouptags}) |
361
|
|
|
|
|
|
|
{ |
362
|
|
|
|
|
|
|
ELEMENT: |
363
|
21
|
|
|
|
|
49
|
while(my ($key, $val) = each %data) |
364
|
43
|
100
|
|
|
|
111
|
{ my $sub = $gr->{$key} or next; |
365
|
8
|
50
|
|
|
|
17
|
if(ref $val eq 'ARRAY') |
366
|
|
|
|
|
|
|
{ next ELEMENT |
367
|
0
|
0
|
|
|
|
0
|
if grep { keys %$_!=1 || !exists $_->{$sub} } @$val; |
|
0
|
0
|
|
|
|
0
|
|
368
|
0
|
|
|
|
|
0
|
$data{$key} = { map { %{$_->{$sub}} } @$val }; |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
369
|
|
|
|
|
|
|
} |
370
|
|
|
|
|
|
|
else |
371
|
8
|
50
|
33
|
|
|
21
|
{ ref $val eq 'HASH' && keys %$val==1 or next; |
372
|
8
|
|
|
|
|
17
|
my ($child_key, $child_val) = %$val; |
373
|
|
|
|
|
|
|
$data{$key} = $child_val |
374
|
8
|
100
|
|
|
|
37
|
if $gr->{$key} eq $child_key; |
375
|
|
|
|
|
|
|
} |
376
|
|
|
|
|
|
|
} |
377
|
|
|
|
|
|
|
} |
378
|
|
|
|
|
|
|
|
379
|
|
|
|
|
|
|
# Fold hashes containing a single anonymous array up into just the array |
380
|
|
|
|
|
|
|
return $data{anon} |
381
|
|
|
|
|
|
|
if keys %data == 1 |
382
|
|
|
|
|
|
|
&& exists $data{anon} |
383
|
240
|
100
|
100
|
|
|
515
|
&& ref $data{anon} eq 'ARRAY'; |
|
|
|
66
|
|
|
|
|
384
|
|
|
|
|
|
|
|
385
|
|
|
|
|
|
|
# Suppress empty elements? |
386
|
228
|
50
|
66
|
|
|
382
|
if(! keys %data && exists $opts->{suppressempty}) { |
387
|
0
|
|
|
|
|
0
|
my $sup = $opts->{suppressempty}; |
388
|
0
|
0
|
0
|
|
|
0
|
return +(defined $sup && $sup eq '') ? '' : undef; |
389
|
|
|
|
|
|
|
} |
390
|
|
|
|
|
|
|
|
391
|
|
|
|
|
|
|
# Roll up named elements with named nested 'value' attributes |
392
|
228
|
50
|
|
|
|
362
|
if(my $va = $opts->{valueattrlist}) |
393
|
228
|
|
|
|
|
476
|
{ while(my($key, $val) = each %data) |
394
|
458
|
50
|
66
|
|
|
1206
|
{ $va->{$key} && ref $val eq 'HASH' && keys %$val==1 or next; |
|
|
|
66
|
|
|
|
|
395
|
4
|
|
|
|
|
14
|
$data{$key} = $val->{$va->{$key}}; |
396
|
|
|
|
|
|
|
} |
397
|
|
|
|
|
|
|
} |
398
|
|
|
|
|
|
|
|
399
|
|
|
|
|
|
|
$nr_elems+$nr_attrs ? \%data |
400
|
|
|
|
|
|
|
: !defined $text ? {} |
401
|
228
|
0
|
|
|
|
501
|
: $opts->{forcecontent} ? { $opts->{contentkey} => $text } |
|
|
50
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
402
|
|
|
|
|
|
|
: $text; |
403
|
|
|
|
|
|
|
} |
404
|
|
|
|
|
|
|
|
405
|
|
|
|
|
|
|
sub normalise_space($) |
406
|
20
|
|
|
20
|
0
|
120
|
{ my $self = shift; |
407
|
20
|
|
|
|
|
28
|
local $_ = shift; |
408
|
20
|
|
|
|
|
70
|
s/^\s+//s; |
409
|
20
|
|
|
|
|
66
|
s/\s+$//s; |
410
|
20
|
|
|
|
|
55
|
s/\s\s+/ /sg; |
411
|
20
|
|
|
|
|
38
|
$_; |
412
|
|
|
|
|
|
|
} |
413
|
|
|
|
|
|
|
|
414
|
|
|
|
|
|
|
# Attempts to 'fold' an array of hashes into an hash of hashes. Returns a |
415
|
|
|
|
|
|
|
# reference to the hash on success or the original array if folding is |
416
|
|
|
|
|
|
|
# not possible. Behaviour is controlled by 'keyattr' option. |
417
|
|
|
|
|
|
|
# |
418
|
|
|
|
|
|
|
|
419
|
|
|
|
|
|
|
sub array_to_hash($$$$) |
420
|
84
|
|
|
84
|
0
|
136
|
{ my ($self, $name, $in, $opts) = @_; |
421
|
84
|
|
|
|
|
91
|
my %out; |
422
|
|
|
|
|
|
|
|
423
|
84
|
50
|
|
|
|
150
|
my $ka = $opts->{keyattr} or return $in; |
424
|
|
|
|
|
|
|
|
425
|
84
|
100
|
|
|
|
133
|
if(ref $ka eq 'HASH') |
426
|
28
|
100
|
|
|
|
77
|
{ my $newkey = $ka->{$name} or return $in; |
427
|
20
|
|
|
|
|
32
|
my ($key, $flag) = @$newkey; |
428
|
|
|
|
|
|
|
|
429
|
20
|
|
|
|
|
32
|
foreach my $h (@$in) |
430
|
44
|
100
|
66
|
|
|
124
|
{ unless(ref $h eq 'HASH' && defined $h->{$key}) |
431
|
2
|
100
|
|
|
|
16
|
{ warn "<$name> element has no '$key' key attribute\n" if $^W; |
432
|
2
|
|
|
|
|
14
|
return $in; |
433
|
|
|
|
|
|
|
} |
434
|
|
|
|
|
|
|
|
435
|
42
|
|
|
|
|
60
|
my $val = $h->{$key}; |
436
|
42
|
100
|
|
|
|
55
|
if(ref $val) |
437
|
2
|
100
|
|
|
|
16
|
{ warn "<$name> element has non-scalar '$key' key attribute\n" if $^W; |
438
|
2
|
|
|
|
|
14
|
return $in; |
439
|
|
|
|
|
|
|
} |
440
|
|
|
|
|
|
|
|
441
|
|
|
|
|
|
|
$val = $self->normalise_space($val) |
442
|
40
|
100
|
|
|
|
63
|
if $opts->{normalisespace}==1; |
443
|
|
|
|
|
|
|
|
444
|
|
|
|
|
|
|
warn "<$name> element has non-unique value in '$key' " |
445
|
40
|
100
|
100
|
|
|
97
|
. "key attribute: $val\n" if $^W && defined $out{$val}; |
446
|
|
|
|
|
|
|
|
447
|
40
|
|
|
|
|
142
|
$out{$val} = { %$h }; |
448
|
40
|
100
|
|
|
|
78
|
$out{$val}{"-$key"} = $out{$val}{$key} if $flag eq '-'; |
449
|
40
|
100
|
|
|
|
96
|
delete $out{$val}{$key} if $flag ne '+'; |
450
|
|
|
|
|
|
|
} |
451
|
|
|
|
|
|
|
} |
452
|
|
|
|
|
|
|
|
453
|
|
|
|
|
|
|
else # Arrayref |
454
|
56
|
|
|
|
|
130
|
{ my $default_keys = "@default_attributes" eq "@$ka"; |
455
|
|
|
|
|
|
|
|
456
|
|
|
|
|
|
|
ELEMENT: |
457
|
56
|
|
|
|
|
85
|
foreach my $h (@$in) |
458
|
78
|
100
|
|
|
|
203
|
{ ref $h eq 'HASH' or return $in; |
459
|
|
|
|
|
|
|
|
460
|
50
|
|
|
|
|
63
|
foreach my $key (@$ka) |
461
|
81
|
|
|
|
|
104
|
{ my $val = $h->{$key}; |
462
|
81
|
100
|
|
|
|
120
|
defined $val or next; |
463
|
|
|
|
|
|
|
|
464
|
42
|
100
|
|
|
|
62
|
if(ref $val) |
465
|
2
|
100
|
66
|
|
|
24
|
{ warn "<$name> element has non-scalar '$key' key attribute" |
466
|
|
|
|
|
|
|
if $^W && ! $default_keys; |
467
|
2
|
|
|
|
|
13
|
return $in; |
468
|
|
|
|
|
|
|
} |
469
|
|
|
|
|
|
|
|
470
|
|
|
|
|
|
|
$val = $self->normalise_space($val) |
471
|
40
|
100
|
|
|
|
65
|
if $opts->{normalisespace} == 1; |
472
|
|
|
|
|
|
|
|
473
|
|
|
|
|
|
|
warn "<$name> element has non-unique value in '$key' " |
474
|
40
|
100
|
100
|
|
|
101
|
. "key attribute: $val" if $^W && $out{$val}; |
475
|
|
|
|
|
|
|
|
476
|
40
|
|
|
|
|
137
|
$out{$val} = { %$h }; |
477
|
40
|
|
|
|
|
76
|
delete $out{$val}{$key}; |
478
|
40
|
|
|
|
|
66
|
next ELEMENT; |
479
|
|
|
|
|
|
|
} |
480
|
8
|
|
|
|
|
27
|
return $in; # No keyfield matched |
481
|
|
|
|
|
|
|
} |
482
|
|
|
|
|
|
|
} |
483
|
|
|
|
|
|
|
|
484
|
|
|
|
|
|
|
$opts->{collapseagain} |
485
|
34
|
100
|
|
|
|
88
|
or return \%out; |
486
|
|
|
|
|
|
|
|
487
|
|
|
|
|
|
|
# avoid over-complicated structures like |
488
|
|
|
|
|
|
|
# dir => { libexecdir => { content => '$exec_prefix/libexec' }, |
489
|
|
|
|
|
|
|
# localstatedir => { content => '$prefix' }, |
490
|
|
|
|
|
|
|
# } |
491
|
|
|
|
|
|
|
# into |
492
|
|
|
|
|
|
|
# dir => { libexecdir => '$exec_prefix/libexec', |
493
|
|
|
|
|
|
|
# localstatedir => '$prefix', |
494
|
|
|
|
|
|
|
# } |
495
|
|
|
|
|
|
|
|
496
|
27
|
|
|
|
|
41
|
my $contentkey = $opts->{contentkey}; |
497
|
|
|
|
|
|
|
|
498
|
|
|
|
|
|
|
# first go through the values, checking that they are fit to collapse |
499
|
27
|
|
|
|
|
61
|
foreach my $v (values %out) |
500
|
35
|
50
|
|
|
|
52
|
{ next if !defined $v; |
501
|
35
|
100
|
66
|
|
|
151
|
next if ref $v eq 'HASH' && keys %$v == 1 && exists $v->{$contentkey}; |
|
|
|
100
|
|
|
|
|
502
|
21
|
50
|
33
|
|
|
60
|
next if ref $v eq 'HASH' && !keys %$v; |
503
|
21
|
|
|
|
|
110
|
return \%out; |
504
|
|
|
|
|
|
|
} |
505
|
|
|
|
|
|
|
|
506
|
6
|
|
|
|
|
30
|
$out{$_} = $out{$_}{$contentkey} for keys %out; |
507
|
6
|
|
|
|
|
41
|
\%out; |
508
|
|
|
|
|
|
|
} |
509
|
|
|
|
|
|
|
|
510
|
|
|
|
|
|
|
1; |
511
|
|
|
|
|
|
|
|
512
|
|
|
|
|
|
|
__END__ |