File Coverage

blib/lib/Excel/ValueReader/XLSX/Backend/LibXML.pm
Criterion Covered Total %
statement 134 140 95.7
branch 71 78 91.0
condition 30 39 76.9
subroutine 16 16 100.0
pod 1 1 100.0
total 252 274 91.9


line stmt bran cond sub pod time code
1             package Excel::ValueReader::XLSX::Backend::LibXML;
2 1     1   882 use utf8;
  1         3  
  1         8  
3 1     1   48 use 5.10.1;
  1         4  
4 1     1   6 use Moose;
  1         2  
  1         8  
5 1     1   7171 use Scalar::Util qw/looks_like_number/;
  1         43  
  1         73  
6 1     1   560 use XML::LibXML::Reader qw/XML_READER_TYPE_END_ELEMENT/;
  1         33541  
  1         1319  
7              
8             extends 'Excel::ValueReader::XLSX::Backend';
9              
10             our $VERSION = '1.10';
11              
12             #======================================================================
13             # LAZY ATTRIBUTE CONSTRUCTORS
14             #======================================================================
15              
16             sub _strings {
17 4     4   8 my $self = shift;
18              
19 4         11 my $reader = $self->_xml_reader_for_zip_member('xl/sharedStrings.xml');
20              
21 4         14 my @strings;
22             my $last_string;
23             NODE:
24 4         177 while ($reader->read) {
25 1328 100       3735 next NODE if $reader->nodeType == XML_READER_TYPE_END_ELEMENT;
26 800         1662 my $node_name = $reader->name;
27              
28 800 100       2140 if ($node_name eq 'si') {
    100          
29 259 100       663 push @strings, $last_string if defined $last_string;
30 259         687 $last_string = '';
31             }
32             elsif ($node_name eq '#text') {
33 260         902 $last_string .= $reader->value;
34             }
35             }
36              
37 4 50       16 push @strings, $last_string if defined $last_string;
38              
39 4         21 return \@strings;
40             }
41              
42              
43             sub _workbook_data {
44 7     7   16 my $self = shift;
45              
46 7         15 my %sheets;
47 7         14 my $sheet_id = 1;
48 7         13 my $base_year = 1900;
49              
50 7         18 my $reader = $self->_xml_reader_for_zip_member('xl/workbook.xml');
51              
52             NODE:
53 7         301 while ($reader->read) {
54 170 100       506 next NODE if $reader->nodeType == XML_READER_TYPE_END_ELEMENT;
55              
56 124 100 100     985 if ($reader->name eq 'sheet') {
    100          
57 24 50       86 my $name = $reader->getAttribute('name')
58             or die "sheet node without name";
59 24         172 $sheets{$name} = $sheet_id++;
60             }
61             elsif ($reader->name eq 'workbookPr' and my $attr_value = $reader->getAttribute('date1904')) {
62 2 100 66     17 $base_year = 1904 if $attr_value eq '1' or $attr_value eq 'true'; # this workbook uses the 1904 calendar
63             }
64             }
65              
66 7         58 return {sheets => \%sheets, base_year => $base_year};
67             }
68              
69              
70             sub _date_styles {
71 5     5   11 my $self = shift;
72              
73 5         13 state $date_style_regex = qr{[dy]|\bmm\b};
74 5         15 my @date_styles;
75              
76             # read from the styles.xml zip member
77 5         17 my $xml_reader = $self->_xml_reader_for_zip_member('xl/styles.xml');
78              
79             # start with Excel builtin number formats for dates and times
80 5         25 my @numFmt = $self->Excel_builtin_date_formats;
81              
82 5         13 my $expected_subnode = undef;
83              
84             # add other date formats explicitly specified in this workbook
85             NODE:
86 5         191 while ($xml_reader->read) {
87 729 100       2075 next NODE if $xml_reader->nodeType == XML_READER_TYPE_END_ELEMENT;
88              
89             # special treatment for some specific subtrees -- see 'numFmt' and 'xf' below
90 559 100       983 if ($expected_subnode) {
91 193         416 my ($name, $depth, $handler) = @$expected_subnode;
92 193 100 66     781 if ($xml_reader->name eq $name && $xml_reader->depth == $depth) {
    100          
93             # process that subnode and go to the next node
94 128         298 $handler->();
95 128         804 next NODE;
96             }
97             elsif ($xml_reader->depth < $depth) {
98             # finished handling subnodes; back to regular node treatment
99 9         51 $expected_subnode = undef;
100             }
101             }
102              
103             # regular node treatement
104 431 100       2866 if ($xml_reader->name eq 'numFmts') {
    100          
105             # start parsing nodes for numeric formats
106             $expected_subnode = [numFmt => $xml_reader->depth+1 => sub {
107 38     38   101 my $id = $xml_reader->getAttribute('numFmtId');
108 38         98 my $code = $xml_reader->getAttribute('formatCode');
109 38 100 33     309 $numFmt[$id] = $code if $id && $code && $code =~ $date_style_regex;
      66        
110 4         47 }];
111             }
112              
113             elsif ($xml_reader->name eq 'cellXfs') {
114             # start parsing nodes for cell formats
115             $expected_subnode = [xf => $xml_reader->depth+1 => sub {
116 90     90   135 state $xf_count = 0;
117 90         274 my $numFmtId = $xml_reader->getAttribute('numFmtId');
118 90         187 my $code = $numFmt[$numFmtId]; # may be undef
119 90         199 $date_styles[$xf_count++] = $code;
120 5         72 }];
121             }
122             }
123              
124 5         39 return \@date_styles;
125             }
126              
127              
128              
129             #======================================================================
130             # METHODS
131             #======================================================================
132              
133             sub _xml_reader {
134 42     42   91 my ($self, $xml) = @_;
135              
136 42         288 my $reader = XML::LibXML::Reader->new(string => $xml,
137             no_blanks => 1,
138             no_network => 1,
139             huge => 1);
140 42         3794 return $reader;
141             }
142              
143              
144             sub _xml_reader_for_zip_member {
145 32     32   77 my ($self, $member_name) = @_;
146              
147 32         94 my $contents = $self->_zip_member_contents($member_name);
148 32         97 return $self->_xml_reader($contents);
149             }
150              
151              
152             sub values {
153 16     16 1 190 my ($self, $sheet) = @_;
154              
155             # prepare for traversing the XML structure
156 16         533 my $has_date_formatter = $self->frontend->date_formatter;
157 16         64 my $sheet_member_name = $self->_zip_member_name_for_sheet($sheet);
158 16         48 my $xml_reader = $self->_xml_reader_for_zip_member($sheet_member_name);
159 16         28 my @data;
160 16         34 my ($row, $col) = (0, 0);
161 16         30 my ($cell_type, $cell_style, $seen_node);
162              
163             # iterate through XML nodes
164             NODE:
165 16         568 while ($xml_reader->read) {
166 7124         17602 my $node_name = $xml_reader->name;
167 7124         13266 my $node_type = $xml_reader->nodeType;
168              
169 7124 100 100     14558 last NODE if $node_name eq 'sheetData' && $node_type == XML_READER_TYPE_END_ELEMENT;
170 7109 100       20131 next NODE if $node_type == XML_READER_TYPE_END_ELEMENT;
171              
172 4224 100       7505 if ($node_name eq 'row') {
173 730         1889 my $row_num = $xml_reader->getAttribute('r');
174 730   66     1597 $row = $row_num // $row + 1;
175 730         1119 $col = 0;
176             }
177              
178 4224 100       14851 if ($node_name eq 'c') {
    100          
    100          
179             # new cell node : store its col/row reference and its type
180 1205         3284 my $A1_cell_ref = $xml_reader->getAttribute('r');
181 1205 100       2360 ($col, $row) = $A1_cell_ref ? do {my ($c, $r) = ($A1_cell_ref =~ /^([A-Z]+)(\d+)$/);
  1193         4720  
182 1193         3610 ($self->A1_to_num($c), $r )}
183             : ($col+1, $row);
184 1205         3738 $cell_type = $xml_reader->getAttribute('t');
185 1205         2654 $cell_style = $xml_reader->getAttribute('s');
186 1205         4555 $seen_node = '';
187             }
188              
189             elsif ($node_name =~ /^[vtf]$/) {
190             # remember that we have seen a 'value' or 'text' or 'formula' node
191 1057         3937 $seen_node = $node_name;
192             }
193              
194             elsif ($node_name eq '#text') {
195             #start processing cell content
196              
197 1057         2675 my $val = $xml_reader->value;
198 1057   100     2997 $cell_type //= '';
199              
200 1057 100 33     1905 if ($seen_node eq 'v') {
    50          
    50          
201 1054 100       2543 if ($cell_type eq 's') {
    50          
    50          
202 653 50       1721 if (looks_like_number($val)) {
203 653         20980 $val = $self->strings->[$val]; # string -- pointer into the global
204             # array of shared strings
205             }
206             else {
207 0         0 warn "unexpected non-numerical value: $val inside a node of shape <v t='s'>\n";
208             }
209             }
210             elsif ($cell_type eq 'e') {
211 0         0 $val = undef; # error -- silently replace by undef
212             }
213             elsif ($cell_type =~ /^(n|d|b|str|)$/) {
214             # number, date, boolean, formula string or no type : content is already in $val
215              
216             # if this is a date, replace the numeric value by the formatted date
217 401 100 100     2112 if ($has_date_formatter && $cell_style && looks_like_number($val) && $val >= 0) {
      100        
      66        
218 243         8338 my $date_style = $self->date_styles->[$cell_style];
219 243 100       751 $val = $self->formatted_date($val, $date_style) if $date_style;
220             }
221             }
222             else {
223             # handle unexpected cases
224 0         0 warn "unsupported type '$cell_type' in cell L${row}C${col}\n";
225 0         0 $val = undef;
226             }
227              
228             # insert this value into the global data array
229 1054         5993 $data[$row-1][$col-1] = $val;
230             }
231              
232             elsif ($seen_node eq 't' && $cell_type eq 'inlineStr') {
233             # inline string -- accumulate all #text nodes until next cell
234 1     1   9 no warnings 'uninitialized';
  1         3  
  1         529  
235 0         0 $data[$row-1][$col-1] .= $val;
236             }
237              
238             elsif ($seen_node eq 'f') {
239             # formula -- just ignore it
240             }
241              
242             else {
243             # handle unexpected cases
244 0         0 warn "unexpected text node in cell L${row}C${col}: $val\n";
245             }
246             }
247             }
248              
249             # insert arrayrefs for empty rows
250 16   100     224 $_ //= [] foreach @data;
251              
252 16         100 return \@data;
253             }
254              
255              
256              
257              
258             sub _table_targets {
259 5     5   11 my ($self, $rel_xml) = @_;
260              
261 5         12 my $xml_reader = $self->_xml_reader($rel_xml);
262              
263 5         8 my @table_targets;
264              
265             # iterate through XML nodes
266             NODE:
267 5         122 while ($xml_reader->read) {
268 18         56 my $node_name = $xml_reader->name;
269 18         41 my $node_type = $xml_reader->nodeType;
270 18 100       45 next NODE if $node_type == XML_READER_TYPE_END_ELEMENT;
271              
272 13 100       36 if ($node_name eq 'Relationship') {
273 8         28 my $target = $xml_reader->getAttribute('Target');
274 8 100       49 if ($target =~ m[tables/table(\d+)\.xml]) {
275             # just store the table id (positive integer)
276 5         36 push @table_targets, $1;
277             }
278             }
279             }
280              
281 5         21 return @table_targets;
282             }
283              
284              
285             sub _parse_table_xml {
286 5     5   11 my ($self, $xml) = @_;
287              
288 5         10 my ($name, $ref, $no_headers, @columns);
289              
290 5         12 my $xml_reader = $self->_xml_reader($xml);
291              
292             # iterate through XML nodes
293             NODE:
294 5         181 while ($xml_reader->read) {
295 45         118 my $node_name = $xml_reader->name;
296 45         86 my $node_type = $xml_reader->nodeType;
297 45 100       111 next NODE if $node_type == XML_READER_TYPE_END_ELEMENT;
298              
299 35 100       109 if ($node_name eq 'table') {
    100          
300 5         21 $name = $xml_reader->getAttribute('displayName');
301 5         13 $ref = $xml_reader->getAttribute('ref');
302 5   100     32 $no_headers = ($xml_reader->getAttribute('headerRowCount') // "") eq "0";
303             }
304             elsif ($node_name eq 'tableColumn') {
305 16         76 push @columns, $xml_reader->getAttribute('name');
306             }
307             }
308              
309 5         24 return ($name, $ref, \@columns, $no_headers);
310             }
311              
312              
313              
314              
315             1;
316              
317              
318             __END__
319              
320              
321             =head1 NAME
322              
323             Excel::ValueReader::XLSX::Backend::LibXML - using LibXML for extracting values from Excel workbooks
324              
325             =head1 DESCRIPTION
326              
327             This is one of two backend modules for L<Excel::ValueReader::XLSX>; the other
328             possible backend is L<Excel::ValueReader::XLSX::Backend::Regex>.
329              
330             This backend parses OOXML structures using L<XML::LibXML::Reader>.
331              
332             =head1 AUTHOR
333              
334             Laurent Dami, E<lt>dami at cpan.orgE<gt>
335              
336             =head1 COPYRIGHT AND LICENSE
337              
338             Copyright 2020-2022 by Laurent Dami.
339              
340             This library is free software; you can redistribute it and/or modify
341             it under the same terms as Perl itself.