line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
#$Id: SAX.pm 505 2008-06-27 22:53:18Z rick $ |
2
|
|
|
|
|
|
|
package iCal::Parser::SAX; |
3
|
1
|
|
|
1
|
|
24169
|
use strict; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
41
|
|
4
|
|
|
|
|
|
|
|
5
|
1
|
|
|
1
|
|
5
|
use base qw(XML::SAX::Base); |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
1410
|
|
6
|
1
|
|
|
1
|
|
33496
|
use iCal::Parser; |
|
1
|
|
|
|
|
303334
|
|
|
1
|
|
|
|
|
45
|
|
7
|
1
|
|
|
1
|
|
8
|
use IO::File; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
115
|
|
8
|
1
|
|
|
1
|
|
5
|
use IO::String; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
17
|
|
9
|
1
|
|
|
1
|
|
4
|
use DateTime; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
2219
|
|
10
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
# Get version from subversion url of tag or branch. |
12
|
|
|
|
|
|
|
our $VERSION= do {(q$URL: svn+ssh://xpc/var/lib/svn/rick/perl/ical/iCal-Parser-SAX/tags/1.09/lib/iCal/Parser/SAX.pm $=~ m$.*/(?:tags|branches)/([^/ ]+)$)[0]||'0.1'}; |
13
|
|
|
|
|
|
|
|
14
|
|
|
|
|
|
|
our %NAMES=('X-WR-RELCALID'=>'id', 'X-WR-CALNAME'=>'name', |
15
|
|
|
|
|
|
|
'X-WR-CALDESC'=>'description'); |
16
|
|
|
|
|
|
|
sub new { |
17
|
1
|
|
|
1
|
1
|
13
|
my($class,%options)=@_; |
18
|
1
|
|
|
|
|
2
|
my $handler=delete $options{Handler}; |
19
|
1
|
50
|
|
|
|
10
|
my $self=XML::SAX::Base->new($handler ? (Handler=>$handler) : ()); |
20
|
1
|
|
|
|
|
64
|
$self=bless $self,$class; |
21
|
1
|
|
|
|
|
7
|
$self->{no_escape}=delete $options{no_escape}; |
22
|
1
|
|
|
|
|
7
|
$self->{_calparser}=iCal::Parser->new(%options); |
23
|
1
|
|
|
|
|
11104
|
return $self; |
24
|
|
|
|
|
|
|
} |
25
|
|
|
|
|
|
|
sub _parse_characterstream { |
26
|
0
|
|
|
0
|
|
|
shift->_parse_fh(@_); |
27
|
|
|
|
|
|
|
} |
28
|
|
|
|
|
|
|
sub _parse_bytestream { |
29
|
0
|
|
|
0
|
|
|
shift->_parse_fh(@_); |
30
|
|
|
|
|
|
|
} |
31
|
|
|
|
|
|
|
sub _parse_systemid { |
32
|
0
|
|
|
0
|
|
|
my ($self, $sysid, $options) = @_; |
33
|
0
|
|
|
|
|
|
$self->_parse_fh(__systemid_to_fh($sysid)); |
34
|
|
|
|
|
|
|
} |
35
|
|
|
|
|
|
|
sub _parse_string { |
36
|
0
|
|
|
0
|
|
|
my ($self, $str, $options) = @_; |
37
|
|
|
|
|
|
|
|
38
|
0
|
|
|
|
|
|
$self->_parse_fh(IO::String->new($str)); |
39
|
|
|
|
|
|
|
} |
40
|
|
|
|
|
|
|
sub _parse_fh { |
41
|
0
|
|
|
0
|
|
|
my($self,$fh,$options)=@_; |
42
|
|
|
|
|
|
|
|
43
|
0
|
|
|
|
|
|
return $self->parse_hash($self->{_calparser}->parse($fh)); |
44
|
|
|
|
|
|
|
} |
45
|
|
|
|
|
|
|
sub parse_uris { |
46
|
0
|
|
|
0
|
1
|
|
my $self=shift; |
47
|
|
|
|
|
|
|
|
48
|
0
|
|
|
|
|
|
foreach my $uri (@_) { |
49
|
0
|
|
|
|
|
|
$self->{_calparser}->parse(__systemid_to_fh($uri)); |
50
|
|
|
|
|
|
|
} |
51
|
0
|
|
|
|
|
|
return $self->parse_hash($self->{_calparser}->calendar); |
52
|
|
|
|
|
|
|
} |
53
|
|
|
|
|
|
|
sub __systemid_to_fh { |
54
|
0
|
|
|
0
|
|
|
my $sysid=shift; |
55
|
0
|
0
|
|
|
|
|
if($sysid =~ m{^(http|ftp|https)://}) { |
56
|
0
|
|
|
|
|
|
eval {require LWP::UserAgent;}; |
|
0
|
|
|
|
|
|
|
57
|
0
|
0
|
|
|
|
|
die "LWP required for $sysid\n" if $@; |
58
|
0
|
|
|
|
|
|
my $req=HTTP::Request->new(GET => $sysid); |
59
|
0
|
|
|
|
|
|
my $ua=LWP::UserAgent->new; |
60
|
0
|
|
|
|
|
|
$ua->agent(__PACKAGE__); |
61
|
0
|
|
|
|
|
|
my $res=$ua->request($req); |
62
|
0
|
0
|
|
|
|
|
unless($res->is_success) { |
63
|
0
|
|
|
|
|
|
die "Can't read $sysid\n"; |
64
|
|
|
|
|
|
|
} |
65
|
0
|
|
|
|
|
|
return IO::String->new($res->content); |
66
|
|
|
|
|
|
|
} else { |
67
|
0
|
0
|
|
|
|
|
return IO::File->new($sysid,'r') or die "Can't open $sysid, $!\n"; |
68
|
|
|
|
|
|
|
} |
69
|
|
|
|
|
|
|
} |
70
|
|
|
|
|
|
|
sub parse_hash { |
71
|
0
|
|
|
0
|
1
|
|
my($self,$hash)=@_; |
72
|
|
|
|
|
|
|
|
73
|
0
|
|
|
|
|
|
$self->SUPER::start_document; |
74
|
0
|
|
|
|
|
|
$self->start('ical'); |
75
|
0
|
|
|
|
|
|
foreach my $cal (@{ $hash->{cals} }) { |
|
0
|
|
|
|
|
|
|
76
|
0
|
|
0
|
|
|
|
$self->start('calendar',{ map { |
77
|
0
|
|
|
|
|
|
($NAMES{$_}||lc $_)=>$cal->{$_} |
78
|
|
|
|
|
|
|
} keys %$cal }); |
79
|
0
|
|
|
|
|
|
$self->end('calendar'); |
80
|
|
|
|
|
|
|
} |
81
|
0
|
|
|
|
|
|
$self->process_events($hash); |
82
|
|
|
|
|
|
|
|
83
|
0
|
0
|
|
|
|
|
if(scalar @{$hash->{todos}}) { |
|
0
|
|
|
|
|
|
|
84
|
0
|
|
|
|
|
|
$self->start('todos'); |
85
|
0
|
|
|
|
|
|
map {$self->process_component($_,'todo')} @{ $hash->{todos} }; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
86
|
0
|
|
|
|
|
|
$self->end('todos'); |
87
|
|
|
|
|
|
|
} |
88
|
0
|
|
|
|
|
|
$self->end('ical'); |
89
|
0
|
|
|
|
|
|
$self->SUPER::end_document; |
90
|
|
|
|
|
|
|
} |
91
|
|
|
|
|
|
|
sub process_events { |
92
|
0
|
|
|
0
|
0
|
|
my($self,$hash)=@_; |
93
|
0
|
|
|
|
|
|
my $events=$hash->{events}; |
94
|
0
|
0
|
|
|
|
|
return unless $events; |
95
|
0
|
|
|
|
|
|
my $cals=$hash->{cals}; |
96
|
|
|
|
|
|
|
|
97
|
0
|
|
|
|
|
|
$self->start('events'); |
98
|
0
|
|
|
|
|
|
my @years=sort { $a <=> $b } keys %$events; |
|
0
|
|
|
|
|
|
|
99
|
0
|
|
|
|
|
|
foreach my $y (@years) { |
100
|
0
|
|
|
|
|
|
$self->start('year',{year=>$y}); |
101
|
0
|
|
|
|
|
|
my $year=$events->{$y}; |
102
|
|
|
|
|
|
|
#fill in missing months from start->end |
103
|
0
|
|
|
|
|
|
my @months=sort { $a <=> $b } keys %$year; |
|
0
|
|
|
|
|
|
|
104
|
0
|
|
|
|
|
|
my $sm= $months[0]; |
105
|
0
|
|
|
|
|
|
my $se= $months[-1]; |
106
|
|
|
|
|
|
|
|
107
|
0
|
|
|
|
|
|
foreach my $m ($sm .. $se) { |
108
|
0
|
|
|
|
|
|
my $month=$year->{$m}; |
109
|
0
|
|
|
|
|
|
my $d1=DateTime->new(year=>$y,month=>$m,day=>1); |
110
|
0
|
0
|
|
|
|
|
warn $d1->ymd, " ---\n" if $self->{debug}; |
111
|
0
|
|
|
|
|
|
$self->start('month', {month=>$m}); |
112
|
0
|
|
|
|
|
|
my $week=$d1->week_number; |
113
|
0
|
|
|
|
|
|
$self->start('week',{week=>$week}); |
114
|
|
|
|
|
|
|
#pad beggining of week |
115
|
0
|
|
|
|
|
|
my $dow=$d1->day_of_week; |
116
|
0
|
|
|
|
|
|
for($d1->subtract(days=>$d1->day_of_week-1);$d1->day_of_week!=$dow; |
117
|
|
|
|
|
|
|
$d1->add(days=>1)) { |
118
|
0
|
|
|
|
|
|
$self->process_day($d1,$self->day($d1,$events)); |
119
|
|
|
|
|
|
|
} |
120
|
0
|
|
|
|
|
|
for(;$d1->month == $m;$d1->add(days=>1)) { |
121
|
0
|
0
|
|
|
|
|
if($d1->week != $week) { |
122
|
0
|
|
|
|
|
|
$self->end('week',{week=>$week}); |
123
|
0
|
|
|
|
|
|
$week=$d1->week; |
124
|
0
|
|
|
|
|
|
$self->start('week',{week=>$week}); |
125
|
|
|
|
|
|
|
} |
126
|
0
|
|
|
|
|
|
$self->process_day($d1,$self->day($d1,$events)); |
127
|
|
|
|
|
|
|
} |
128
|
|
|
|
|
|
|
#pad end of month |
129
|
0
|
|
|
|
|
|
for(;$d1->day_of_week != 1;$d1->add(days=>1)) { |
130
|
0
|
|
|
|
|
|
$self->process_day($d1,$self->day($d1,$events)); |
131
|
|
|
|
|
|
|
} |
132
|
0
|
|
|
|
|
|
$self->end('week'); |
133
|
0
|
|
|
|
|
|
$self->end('month'); |
134
|
|
|
|
|
|
|
} |
135
|
0
|
|
|
|
|
|
$self->end('year'); |
136
|
|
|
|
|
|
|
} |
137
|
0
|
|
|
|
|
|
$self->end('events'); |
138
|
|
|
|
|
|
|
} |
139
|
|
|
|
|
|
|
sub day { |
140
|
0
|
|
|
0
|
0
|
|
my($self,$d,$events)=@_; |
141
|
0
|
|
|
|
|
|
my($yr,$mo); |
142
|
0
|
0
|
|
|
|
|
return unless $yr=$events->{$d->year}; |
143
|
0
|
0
|
|
|
|
|
return unless $mo=$yr->{$d->month}; |
144
|
0
|
|
|
|
|
|
return $mo->{$d->day}; |
145
|
|
|
|
|
|
|
} |
146
|
|
|
|
|
|
|
sub process_day { |
147
|
0
|
|
|
0
|
0
|
|
my($self,$d,$day)=@_; |
148
|
|
|
|
|
|
|
#warn $d->ymd,"\n" if $self->{debug}; |
149
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
# figure out max# conflicting appointments. and output in xml |
151
|
|
|
|
|
|
|
# makes html generation of weekly/daily calendar easier |
152
|
0
|
|
|
|
|
|
my @events=(); |
153
|
0
|
|
|
|
|
|
my $conflict=0; |
154
|
0
|
0
|
|
|
|
|
if($day) { |
155
|
0
|
|
|
|
|
|
@events=sort by_type_time values %$day; |
156
|
0
|
|
|
|
|
|
my @a=(); #event span |
157
|
0
|
|
|
|
|
|
foreach my $e (@events) { |
158
|
0
|
0
|
|
|
|
|
if($e->{allday}) { |
159
|
0
|
|
|
|
|
|
push @a,undef; |
160
|
0
|
|
|
|
|
|
next; |
161
|
|
|
|
|
|
|
} |
162
|
|
|
|
|
|
|
#if an event ends at e.g., 9am and another starts |
163
|
|
|
|
|
|
|
#at 9, intersect will generate an overlap. |
164
|
|
|
|
|
|
|
#so, subtract 1 sec from the end of each event |
165
|
|
|
|
|
|
|
## unless start == end |
166
|
|
|
|
|
|
|
## note start > end is an error! |
167
|
0
|
0
|
|
|
|
|
my $end=$e->{DTSTART}->compare($e->{DTEND}) < 0 |
168
|
|
|
|
|
|
|
? $e->{DTEND}->clone->subtract(seconds=>1) : $e->{DTEND}; |
169
|
0
|
|
|
|
|
|
push @a, DateTime::Span->from_datetimes |
170
|
|
|
|
|
|
|
(start=>$e->{DTSTART}, end=>$end); |
171
|
|
|
|
|
|
|
} |
172
|
0
|
|
|
|
|
|
my @overlap=(0); |
173
|
|
|
|
|
|
|
# each conflict adds one to the count of conflicts for the event |
174
|
|
|
|
|
|
|
# it conflicts with |
175
|
0
|
|
|
|
|
|
foreach my $i (1..$#a) { |
176
|
0
|
|
|
|
|
|
my $span=$a[$i]; |
177
|
0
|
|
|
|
|
|
$overlap[$i]=0; |
178
|
0
|
0
|
|
|
|
|
next unless $span; |
179
|
0
|
|
|
|
|
|
foreach my $j (0..$i-1) { |
180
|
0
|
0
|
|
|
|
|
next unless $a[$j]; |
181
|
0
|
0
|
|
|
|
|
$overlap[$i]=$overlap[$j]+1 if $span->intersects($a[$j]); |
182
|
|
|
|
|
|
|
} |
183
|
0
|
0
|
|
|
|
|
$events[$i]->{'conflict-number'}=$overlap[$i] if $overlap[$i]; |
184
|
|
|
|
|
|
|
} |
185
|
0
|
0
|
|
|
|
|
map { $conflict = $_ if $_ > $conflict } @overlap; |
|
0
|
|
|
|
|
|
|
186
|
|
|
|
|
|
|
} |
187
|
0
|
0
|
|
|
|
|
$self->start('day',{date=>$d->ymd, |
188
|
|
|
|
|
|
|
$conflict ? (conflict=>$conflict) : ()}); |
189
|
0
|
|
|
|
|
|
map {$self->process_component($_,'event')} @events; |
|
0
|
|
|
|
|
|
|
190
|
0
|
|
|
|
|
|
$self->end('day'); |
191
|
|
|
|
|
|
|
} |
192
|
|
|
|
|
|
|
sub by_type_time { # For sorting lists of events |
193
|
|
|
|
|
|
|
# Two events on the same day? All day events come first |
194
|
0
|
0
|
0
|
0
|
0
|
|
return -1 if $a->{allday} && !$b->{allday}; |
195
|
0
|
0
|
0
|
|
|
|
return 1 if $b->{allday} && !$a->{allday}; |
196
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
# If they're both all day events, sort by summary text |
198
|
0
|
0
|
0
|
|
|
|
return $a->{SUMMARY} cmp $b->{SUMMARY} if $a->{allday} && $b->{allday}; |
199
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
# Otherwise, sort by start time |
201
|
0
|
|
|
|
|
|
return $a->{DTSTART} <=> $b->{DTSTART}; |
202
|
|
|
|
|
|
|
} |
203
|
|
|
|
|
|
|
sub process_component { |
204
|
0
|
|
|
0
|
0
|
|
my($self,$ee,$type)=@_; |
205
|
0
|
|
|
|
|
|
my %attrs=(); |
206
|
|
|
|
|
|
|
# pull out attributes before generic processing |
207
|
|
|
|
|
|
|
# of key/value pairs into elements |
208
|
|
|
|
|
|
|
#clone in case event processed more than once |
209
|
0
|
|
|
|
|
|
my %e=%$ee; |
210
|
0
|
0
|
|
|
|
|
$attrs{uid}=delete $e{UID} if $e{UID}; |
211
|
0
|
0
|
|
|
|
|
$attrs{idref}=delete $e{idref} if $e{idref}; |
212
|
0
|
0
|
|
|
|
|
$attrs{'all-day'}=delete $e{allday} if $e{allday}; |
213
|
|
|
|
|
|
|
# used in xslt stylesheet to figure out which |
214
|
|
|
|
|
|
|
# overlapping event this is |
215
|
0
|
0
|
|
|
|
|
$attrs{'conflict-number'}=delete $e{'conflict-number'} |
216
|
|
|
|
|
|
|
if $e{'conflict-number'}; |
217
|
|
|
|
|
|
|
|
218
|
0
|
|
|
|
|
|
$self->start($type,%attrs); |
219
|
0
|
|
|
|
|
|
while(my($k,$v)=each(%e)) { |
220
|
0
|
0
|
|
|
|
|
if(ref $v eq 'ARRAY') { |
|
|
0
|
|
|
|
|
|
221
|
0
|
|
|
|
|
|
my $list=$k . 's'; |
222
|
0
|
|
|
|
|
|
$self->start($list,count=>scalar @$v); |
223
|
0
|
|
|
|
|
|
map {$self->process_component($_,$k)} @$v; |
|
0
|
|
|
|
|
|
|
224
|
0
|
|
|
|
|
|
$self->end($list); |
225
|
|
|
|
|
|
|
} elsif(ref $v eq 'HASH') { |
226
|
0
|
|
|
|
|
|
$self->process_component($v,$k); |
227
|
|
|
|
|
|
|
} else { |
228
|
0
|
|
|
|
|
|
$self->text_element($k,$v); |
229
|
|
|
|
|
|
|
} |
230
|
|
|
|
|
|
|
} |
231
|
0
|
|
|
|
|
|
$self->end($type); |
232
|
|
|
|
|
|
|
} |
233
|
|
|
|
|
|
|
sub start { |
234
|
0
|
|
|
0
|
0
|
|
my $self=shift; |
235
|
0
|
|
|
|
|
|
$self->SUPER::start_element($self->make_element(@_)); |
236
|
|
|
|
|
|
|
} |
237
|
|
|
|
|
|
|
sub end { |
238
|
0
|
|
|
0
|
0
|
|
my $self=shift; |
239
|
0
|
|
|
|
|
|
$self->SUPER::end_element($self->make_element(@_)); |
240
|
|
|
|
|
|
|
} |
241
|
|
|
|
|
|
|
sub make_element { |
242
|
0
|
|
|
0
|
0
|
|
my $self=shift; |
243
|
0
|
|
|
|
|
|
my $n=lc shift; |
244
|
0
|
0
|
|
|
|
|
my %a=ref $_[0] ? %{$_[0]} : @_; |
|
0
|
|
|
|
|
|
|
245
|
0
|
|
|
|
|
|
my %h=(Name=>"$n"); |
246
|
0
|
0
|
|
|
|
|
return \%h unless %a; |
247
|
0
|
|
|
|
|
|
while(my($k,$v) = each %a) { |
248
|
0
|
|
|
|
|
|
$h{Attributes}->{"{}$k"} = {Name=>$k, Value=>escape($v,$self->{no_escape})}; |
249
|
|
|
|
|
|
|
} |
250
|
0
|
|
|
|
|
|
return \%h; |
251
|
|
|
|
|
|
|
} |
252
|
|
|
|
|
|
|
sub escape { |
253
|
0
|
|
|
0
|
0
|
|
my $text=shift; |
254
|
0
|
|
|
|
|
|
my $no_escape=shift; |
255
|
0
|
0
|
|
|
|
|
return '' unless $text; |
256
|
0
|
0
|
|
|
|
|
unless($no_escape) { |
257
|
0
|
|
|
|
|
|
$text=~s/&/\&/go; |
258
|
0
|
|
|
|
|
|
$text=~s/"/\"/go; |
259
|
0
|
|
|
|
|
|
$text=~s/'/'/go; |
260
|
|
|
|
|
|
|
} |
261
|
0
|
|
|
|
|
|
$text=~s/</go; |
262
|
0
|
|
|
|
|
|
$text=~s/\\n/
/go; |
263
|
0
|
|
|
|
|
|
$text=~s/\\//go; |
264
|
0
|
|
|
|
|
|
return $text; |
265
|
|
|
|
|
|
|
} |
266
|
|
|
|
|
|
|
sub text_element { |
267
|
0
|
|
|
0
|
0
|
|
my($self, $n, $v, %a)=@_; |
268
|
0
|
|
|
|
|
|
$self->start($n, %a); |
269
|
0
|
0
|
|
|
|
|
if($v) { |
270
|
0
|
|
|
|
|
|
my $text=escape($v,$self->{no_escape}); |
271
|
0
|
|
|
|
|
|
$self->SUPER::characters({Data=>$text}); |
272
|
|
|
|
|
|
|
} |
273
|
0
|
|
|
|
|
|
$self->end($n); |
274
|
|
|
|
|
|
|
} |
275
|
|
|
|
|
|
|
1; |
276
|
|
|
|
|
|
|
__END__ |