File Coverage

blib/lib/Excel/ValueReader/XLSX/Backend/LibXML.pm
Criterion Covered Total %
statement 128 134 95.5
branch 67 74 90.5
condition 28 36 77.7
subroutine 16 16 100.0
pod 1 1 100.0
total 240 261 91.9


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