File Coverage

blib/lib/Excel/ValueReader/XLSX/Backend/LibXML.pm
Criterion Covered Total %
statement 129 135 95.5
branch 67 74 90.5
condition 28 36 77.7
subroutine 16 16 100.0
pod 1 1 100.0
total 241 262 91.9


line stmt bran cond sub pod time code
1             package Excel::ValueReader::XLSX::Backend::LibXML;
2 1     1   720 use utf8;
  1         35  
  1         7  
3 1     1   38 use 5.10.1;
  1         3  
4 1     1   4 use Moose;
  1         2  
  1         7  
5 1     1   6011 use Scalar::Util qw/looks_like_number/;
  1         2  
  1         53  
6 1     1   439 use XML::LibXML::Reader qw/XML_READER_TYPE_END_ELEMENT/;
  1         29819  
  1         1067  
7              
8             extends 'Excel::ValueReader::XLSX::Backend';
9              
10             our $VERSION = '1.08';
11              
12             #======================================================================
13             # LAZY ATTRIBUTE CONSTRUCTORS
14             #======================================================================
15              
16             sub _strings {
17 2     2   5 my $self = shift;
18              
19 2         5 my $reader = $self->_xml_reader_for_zip_member('xl/sharedStrings.xml');
20              
21 2         5 my @strings;
22 2         4 my $last_string = '';
23             NODE:
24 2         90 while ($reader->read) {
25 1231 100       3075 next NODE if $reader->nodeType == XML_READER_TYPE_END_ELEMENT;
26 742         1255 my $node_name = $reader->name;
27              
28 742 100       1681 if ($node_name eq 'si') {
    100          
29 240 100       429 push @strings, $last_string if $last_string;
30 240         526 $last_string = '';
31             }
32             elsif ($node_name eq '#text') {
33 242         700 $last_string .= $reader->value;
34             }
35             }
36              
37 2 50       7 push @strings, $last_string if $last_string;
38              
39 2         8 return \@strings;
40             }
41              
42              
43             sub _workbook_data {
44 5     5   9 my $self = shift;
45              
46 5         10 my %sheets;
47 5         8 my $sheet_id = 1;
48 5         7 my $base_year = 1900;
49              
50 5         14 my $reader = $self->_xml_reader_for_zip_member('xl/workbook.xml');
51              
52             NODE:
53 5         220 while ($reader->read) {
54 106 100       278 next NODE if $reader->nodeType == XML_READER_TYPE_END_ELEMENT;
55              
56 78 100 100     513 if ($reader->name eq 'sheet') {
    100          
57 21 50       65 my $name = $reader->getAttribute('name')
58             or die "sheet node without name";
59 21         116 $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 5         40 return {sheets => \%sheets, base_year => $base_year};
67             }
68              
69              
70             sub _date_styles {
71 4     4   7 my $self = shift;
72              
73 4         9 state $date_style_regex = qr{[dy]|\bmm\b};
74 4         7 my @date_styles;
75              
76             # read from the styles.xml zip member
77 4         9 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 4         23 my @numFmt = $self->Excel_builtin_date_formats;
81              
82 4         9 my $expected_subnode = undef;
83              
84             # add other date formats explicitly specified in this workbook
85             NODE:
86 4         128 while ($xml_reader->read) {
87 627 100       1524 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 493 100       729 if ($expected_subnode) {
91 187         287 my ($name, $depth, $handler) = @$expected_subnode;
92 187 100 66     623 if ($xml_reader->name eq $name && $xml_reader->depth == $depth) {
    100          
93             # process that subnode and go to the next node
94 125         228 $handler->();
95 125         658 next NODE;
96             }
97             elsif ($xml_reader->depth < $depth) {
98             # finished handling subnodes; back to regular node treatment
99 8         34 $expected_subnode = undef;
100             }
101             }
102              
103             # regular node treatement
104 368 100       1815 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   96 my $id = $xml_reader->getAttribute('numFmtId');
108 38         86 my $code = $xml_reader->getAttribute('formatCode');
109 38 100 33     287 $numFmt[$id] = $code if $id && $code && $code =~ $date_style_regex;
      66        
110 4         45 }];
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 87     87   102 state $xf_count = 0;
117 87         188 my $numFmtId = $xml_reader->getAttribute('numFmtId');
118 87         151 my $code = $numFmt[$numFmtId]; # may be undef
119 87         149 $date_styles[$xf_count++] = $code;
120 4         39 }];
121             }
122             }
123              
124 4         26 return \@date_styles;
125             }
126              
127              
128              
129             #======================================================================
130             # METHODS
131             #======================================================================
132              
133             sub _xml_reader {
134 35     35   74 my ($self, $xml) = @_;
135              
136 35         204 my $reader = XML::LibXML::Reader->new(string => $xml,
137             no_blanks => 1,
138             no_network => 1,
139             huge => 1);
140 35         2988 return $reader;
141             }
142              
143              
144             sub _xml_reader_for_zip_member {
145 25     25   51 my ($self, $member_name) = @_;
146              
147 25         71 my $contents = $self->_zip_member_contents($member_name);
148 25         68 return $self->_xml_reader($contents);
149             }
150              
151              
152             sub values {
153 14     14 1 153 my ($self, $sheet) = @_;
154              
155             # prepare for traversing the XML structure
156 14         360 my $has_date_formatter = $self->frontend->date_formatter;
157 14         52 my $sheet_member_name = $self->_zip_member_name_for_sheet($sheet);
158 14         51 my $xml_reader = $self->_xml_reader_for_zip_member($sheet_member_name);
159 14         29 my @data;
160 14         36 my ($row, $col, $cell_type, $cell_style, $seen_node);
161              
162             # iterate through XML nodes
163             NODE:
164 14         381 while ($xml_reader->read) {
165 6290         13107 my $node_name = $xml_reader->name;
166 6290         9920 my $node_type = $xml_reader->nodeType;
167              
168 6290 100 100     10507 last NODE if $node_name eq 'sheetData' && $node_type == XML_READER_TYPE_END_ELEMENT;
169 6277 100       14799 next NODE if $node_type == XML_READER_TYPE_END_ELEMENT;
170              
171 3726 100       10905 if ($node_name eq 'c') {
    100          
    100          
172             # new cell node : store its col/row reference and its type
173 1054         2446 my $A1_cell_ref = $xml_reader->getAttribute('r');
174 1054         3586 ($col, $row) = ($A1_cell_ref =~ /^([A-Z]+)(\d+)$/);
175 1054         2430 $col = $self->A1_to_num($col);
176 1054         2605 $cell_type = $xml_reader->getAttribute('t');
177 1054         1999 $cell_style = $xml_reader->getAttribute('s');
178 1054         3444 $seen_node = '';
179             }
180              
181             elsif ($node_name =~ /^[vtf]$/) {
182             # remember that we have seen a 'value' or 'text' or 'formula' node
183 906         2745 $seen_node = $node_name;
184             }
185              
186             elsif ($node_name eq '#text') {
187             #start processing cell content
188              
189 906         1916 my $val = $xml_reader->value;
190 906   100     2180 $cell_type //= '';
191              
192 906 100 33     1326 if ($seen_node eq 'v') {
    50          
    50          
193 903 100       1736 if ($cell_type eq 's') {
    50          
    50          
194 535 50       1100 if (looks_like_number($val)) {
195 535         14532 $val = $self->strings->[$val]; # string -- pointer into the global
196             # array of shared strings
197             }
198             else {
199 0         0 warn "unexpected non-numerical value: $val inside a node of shape <v t='s'>\n";
200             }
201             }
202             elsif ($cell_type eq 'e') {
203 0         0 $val = undef; # error -- silently replace by undef
204             }
205             elsif ($cell_type =~ /^(n|d|b|str|)$/) {
206             # number, date, boolean, formula string or no type : content is already in $val
207              
208             # if this is a date, replace the numeric value by the formatted date
209 368 100 100     1586 if ($has_date_formatter && $cell_style && looks_like_number($val) && $val >= 0) {
      100        
      66        
210 238         6696 my $date_style = $self->date_styles->[$cell_style];
211 238 100       553 $val = $self->formatted_date($val, $date_style) if $date_style;
212             }
213             }
214             else {
215             # handle unexpected cases
216 0         0 warn "unsupported type '$cell_type' in cell L${row}C${col}\n";
217 0         0 $val = undef;
218             }
219              
220             # insert this value into the global data array
221 903         4484 $data[$row-1][$col-1] = $val;
222             }
223              
224             elsif ($seen_node eq 't' && $cell_type eq 'inlineStr') {
225             # inline string -- accumulate all #text nodes until next cell
226 1     1   9 no warnings 'uninitialized';
  1         2  
  1         492  
227 0         0 $data[$row-1][$col-1] .= $val;
228             }
229              
230             elsif ($seen_node eq 'f') {
231             # formula -- just ignore it
232             }
233              
234             else {
235             # handle unexpected cases
236 0         0 warn "unexpected text node in cell L${row}C${col}: $val\n";
237             }
238             }
239             }
240              
241             # insert arrayrefs for empty rows
242 14   100     175 $_ //= [] foreach @data;
243              
244 14         85 return \@data;
245             }
246              
247              
248              
249              
250             sub _table_targets {
251 5     5   10 my ($self, $rel_xml) = @_;
252              
253 5         11 my $xml_reader = $self->_xml_reader($rel_xml);
254              
255 5         10 my @table_targets;
256              
257             # iterate through XML nodes
258             NODE:
259 5         94 while ($xml_reader->read) {
260 18         48 my $node_name = $xml_reader->name;
261 18         33 my $node_type = $xml_reader->nodeType;
262 18 100       41 next NODE if $node_type == XML_READER_TYPE_END_ELEMENT;
263              
264 13 100       31 if ($node_name eq 'Relationship') {
265 8         24 my $target = $xml_reader->getAttribute('Target');
266 8 100       51 if ($target =~ m[tables/table(\d+)\.xml]) {
267             # just store the table id (positive integer)
268 5         31 push @table_targets, $1;
269             }
270             }
271             }
272              
273 5         19 return @table_targets;
274             }
275              
276              
277             sub _parse_table_xml {
278 5     5   14 my ($self, $xml) = @_;
279              
280 5         9 my ($name, $ref, $no_headers, @columns);
281              
282 5         11 my $xml_reader = $self->_xml_reader($xml);
283              
284             # iterate through XML nodes
285             NODE:
286 5         142 while ($xml_reader->read) {
287 45         100 my $node_name = $xml_reader->name;
288 45         80 my $node_type = $xml_reader->nodeType;
289 45 100       90 next NODE if $node_type == XML_READER_TYPE_END_ELEMENT;
290              
291 35 100       91 if ($node_name eq 'table') {
    100          
292 5         15 $name = $xml_reader->getAttribute('displayName');
293 5         15 $ref = $xml_reader->getAttribute('ref');
294 5   100     34 $no_headers = ($xml_reader->getAttribute('headerRowCount') // "") eq "0";
295             }
296             elsif ($node_name eq 'tableColumn') {
297 16         57 push @columns, $xml_reader->getAttribute('name');
298             }
299             }
300              
301 5         20 return ($name, $ref, \@columns, $no_headers);
302             }
303              
304              
305              
306              
307             1;
308              
309              
310             __END__
311              
312              
313             =head1 NAME
314              
315             Excel::ValueReader::XLSX::Backend::LibXML - using LibXML for extracting values from Excel workbooks
316              
317             =head1 DESCRIPTION
318              
319             This is one of two backend modules for L<Excel::ValueReader::XLSX>; the other
320             possible backend is L<Excel::ValueReader::XLSX::Backend::Regex>.
321              
322             This backend parses OOXML structures using L<XML::LibXML::Reader>.
323              
324             =head1 AUTHOR
325              
326             Laurent Dami, E<lt>dami at cpan.orgE<gt>
327              
328             =head1 COPYRIGHT AND LICENSE
329              
330             Copyright 2020-2022 by Laurent Dami.
331              
332             This library is free software; you can redistribute it and/or modify
333             it under the same terms as Perl itself.