File Coverage

blib/lib/Text/vFile/asData.pm
Criterion Covered Total %
statement 94 98 95.9
branch 38 42 90.4
condition 12 18 66.6
subroutine 9 9 100.0
pod 0 4 0.0
total 153 171 89.4


line stmt bran cond sub pod time code
1             package Text::vFile::asData;
2 4     4   60545 use strict;
  4         10  
  4         172  
3 4     4   23 use warnings;
  4         10  
  4         156  
4 4     4   40 no warnings 'uninitialized';
  4         10  
  4         177  
5 4     4   22 use base qw( Class::Accessor::Chained::Fast );
  4         6  
  4         4234  
6             __PACKAGE__->mk_accessors(qw( preserve_params ));
7             our $VERSION = '0.08';
8              
9             =head1 NAME
10              
11             Text::vFile::asData - parse vFile formatted files into data structures
12              
13             =head1 SYNOPSIS
14              
15             use Text::vFile::asData;
16             open my $fh, "foo.ics"
17             or die "couldn't open ics: $!";
18             my $data = Text::vFile::asData->new->parse( $fh );
19              
20             =head1 DESCRIPTION
21              
22             Text::vFile::asData reads vFile format files, such as vCard (RFC 2426) and
23             vCalendar (RFC 2445).
24              
25             =cut
26              
27             sub _unwrap_lines {
28 116     116   1035 my $self = shift;
29 116         147 my @lines;
30 116         264 for (@_) {
31 2017         2528 my $line = $_; # $_ may be readonly
32 2017         5416 $line =~ s{[\r\n]+$}{}; # lines SHOULD end CRLF
33 2017 100       5348 if ($line =~ /^[ \t](.*)/) { # Continuation line (RFC Sect. 4.1)
34 717 50       1339 die "Continuation line, but no preceding line" unless @lines;
35 717         1606 $lines[-1] .= $1;
36 717         1174 next;
37             }
38 1300         2465 push @lines, $line;
39             }
40 116         734 return @lines;
41             }
42              
43             sub parse {
44 50     50 0 9565 my $self = shift;
45 50         103 my $fh = shift;
46 50         3473 return $self->parse_lines( <$fh> );
47             }
48              
49             # like Text::ParseWords' parse_line, only C-style so the regex engine doesn't
50             # blow its stack, and it's also got a $limit like split
51              
52             # this only took a trainride, so I'm pretty sure there are lurking
53             # corner cases - when I get a tuit I'll take the Text::ParseWords
54             # tests and run them through it
55              
56             sub parse_line {
57 1938     1938 0 11307 my ($delim, $keep, $text, $limit) = @_;
58              
59 1938         1905 my ($current, @parts);
60 0         0 my ($quote, $escaped);
61 1938         4085 while (length $text) {
62 21417 50       45329 if ($text =~ s{^(\\)}{}) {
63 0 0 0     0 $current .= $1 if $escaped || $keep;
64 0         0 $escaped = !$escaped;
65 0         0 next;
66             }
67 21417 100 66     153943 if (!$quote && !$escaped && $text =~ s{^$delim}{}) {
      100        
68 1360         2526 push @parts, $current;
69 1360         1460 $current = undef;
70 1360 100 66     5299 if (defined $limit && @parts == $limit -1) {
71 963         3607 return @parts, $text;
72             }
73             }
74             else {
75             # pull the character off to take a looksee
76 20057         57710 $text =~ s{(.)}{};
77 20057         33516 my $char = $1;
78 20057 100 66     47046 if ($char eq '"' && !$escaped) {
79             # either it's defined and matches, in which case we
80             # clear the quote variable, or it's undefined which
81             # makes this quote an opening quote
82 28         39 $quote = !$quote;
83 28 100       61 $current .= $char if $keep;
84             }
85             else {
86 20029         30851 $current .= $char;
87             }
88             }
89 20454         49764 $escaped = 0;
90             }
91              
92 975         3331 return @parts, $current;
93             }
94              
95             sub parse_lines {
96 115     115 0 24047 my $self = shift;
97              
98 115         165 my @path;
99             my $current;
100 115         381 for ($self->_unwrap_lines( @_ )) {
101             # Ignore leading or trailing blank lines at the top/bottom of the
102             # input. Not sure about completely blank lines within the input
103 1299 100 100     3806 next if scalar @path == 0 and $_ =~ /^\s*$/;
104              
105 1294 100       3917 if (/^BEGIN:(.*)/i) {
106 165         251 push @path, $current;
107 165         1305 $current = { type => $1 };
108 165         209 push @{ $path[-1]{objects} }, $current;
  165         524  
109 165         354 next;
110             }
111 1129 100       2724 if (/^END:(.*)/i) {
112 164 100       741 die "END $1 in $current->{type}"
113             unless lc $current->{type} eq lc $1;
114 163         322 $current = pop @path;
115 163         398 next;
116             }
117              
118             # we'd use Text::ParseWords here, but it likes to segfault.
119 965         1941 my ($name, $value) = parse_line( ':', 1, $_, 2);
120 965 100       2365 $value = '' unless defined $value;
121 965         1906 my @params = parse_line( ';', 0, $name );
122 965         2148 $name = shift @params;
123              
124 965         4029 $value = { value => $value };
125              
126 965         1818 foreach my $param (@params) {
127 389         1110 my ($p_name, $p_value) = split /=/, $param;
128 389 100       1334 push @{ $value->{params} }, { $p_name => $p_value }
  197         1748  
129             if $self->preserve_params;
130 389         2848 $value->{param}{ $p_name } = $p_value;
131             }
132 965         1202 push @{ $current->{properties}{ $name } }, $value;
  965         4149  
133             }
134              
135             # something did a BEGIN but no END - TODO, unwind this nicely as
136             # it may be more than one level
137 114 100       459 die "BEGIN $current->{type} without matching END"
138             if @path;
139              
140 113         723 return $current;
141             }
142              
143             # this might not strictly comply
144             sub generate_lines {
145 126     126 0 32778 my $self = shift;
146 126         172 my $this = shift;
147              
148 126         165 my @lines;
149             # XXX all the existence checks are to prevent auto-vivification
150             # breaking if_diff tests - do we mind, or should the fields have been
151             # there anyway?
152              
153 126 100       433 push @lines, "BEGIN:$this->{type}" if exists $this->{type};
154 126 100       304 if (exists $this->{properties}) {
155 78         142 while (my ($name, $v) = each %{ $this->{properties} } ) {
  484         1705  
156 406         635 for my $value (@$v) {
157             # XXX so we're taking params in preference to param,
158             # let's be sure to document that when we document this
159             # method
160 498         583 my $param = join ';', '', map {
161 468 100       2105 my $hash = $_;
162 178 100       863 map {
163 498         1378 "$_" . (defined $hash->{$_} ? "=" . $hash->{$_} : "")
164             } keys %$hash
165 468         480 } @{ $value->{params} || [ $value->{param} ] };
166 468         1292 my $line = "$name$param:$value->{value}";
167             # wrapping, but done ugly
168 468         1858 my @chunks = $line =~ m/(.{1,72})/g;
169 468         749 push @lines, shift @chunks;
170 468         1374 push @lines, map { " $_" } @chunks;
  60         240  
171             }
172             }
173             }
174              
175 126 100       322 if (exists $this->{objects}) {
176 58         83 push @lines, $self->generate_lines( $_ ) for @{ $this->{objects} }
  58         255  
177             }
178 126 100       385 push @lines, "END:$this->{type}" if exists $this->{type};
179 126         823 return @lines;
180             }
181              
182              
183             1;
184             __END__