our
$VERSION
= 0.15;
our
@ISA
=
qw(Exporter)
;
sub
TIEHASH {
my
(
$p
,
$f
,
%O
) =
@_
;
tie
my
@a
,
'Tie::File'
,
$f
,
recsep
=>
"\r\n"
or
die
"failed to open ical file\n"
;
$O
{A} = \
@a
;
$O
{i} = 0;
$O
{C} = ();
bless
\
%O
=>
$p
;
}
sub
FETCH {
my
$self
=
shift
;
my
$uid
=
shift
;
my
$index
=
$self
->seekUid(
$uid
);
return
defined
$index
?
$self
->toHash(
$index
) :
undef
;
}
sub
EXISTS {
my
$self
=
shift
;
my
$uid
=
shift
;
my
$index
=
$self
->seekUid(
$uid
);
return
defined
$index
? 1 : 0;
}
sub
FIRSTKEY {
my
$self
=
shift
;
$self
->{i} = 0;
for
my
$line
(@{
$self
->{A}}) {
if
(
substr
(
$line
, 0, 3) eq
'UID'
) {
if
(
$self
->unfold(
$self
->{i}) =~ /^UID.*:(.*)$/) {
$self
->{C}->{$1} =
$self
->{i};
return
$1;
}
else
{
warn
(
"FIRSTKEY: discovered illegal UID property format, should be like UID;...:..., ignoring for now\n"
);
}
}
$self
->{i}++;
}
}
sub
NEXTKEY {
my
$self
=
shift
;
my
$start_idx
= ++
$self
->{i};
for
my
$line
(@{
$self
->{A}}[
$start_idx
.. (@{
$self
->{A}} - 1)]) {
if
(
$line
=~ m/^UID/) {
if
(
$self
->unfold(
$self
->{i}) =~ /^UID.*:(.*)$/) {
$self
->{C}->{$1} =
$self
->{i};
return
$1;
}
else
{
warn
(
"NEXTKEY: discovered illegal UID property format, should be like UID;...:..., ignoring for now\n"
);
}
}
$self
->{i}++;
}
return
undef
;
}
sub
SCALAR {
my
$self
=
shift
;
my
$count
= 0;
for
my
$line
(@{
$self
->{A}}) {
$count
++
if
substr
(
$line
, 0, 3) eq
'UID'
;
}
return
$count
;
}
sub
ceil {
return
int
(
$_
[0]) + (
int
(
$_
[0]) !=
$_
[0]);
}
sub
fold {
my
$MAXLENGTH
= 75;
my
@A
;
foreach
my
$string
(
@_
) {
my
@B
=
unpack
(
"A$MAXLENGTH"
x (
&ceil
(
length
(
$string
)/
$MAXLENGTH
)),
$string
);
push
@A
,
$B
[0],
map
{
' '
.
$_
}
@B
[1..
$#B
];
}
return
@A
;
}
sub
STORE {
my
$self
=
shift
;
my
$uid
=
shift
;
my
$c
=
shift
;
die
"event must be array!\n"
if
ref
$c
ne
'ARRAY'
;
$self
->DELETE(
$uid
);
push
@{
$self
->{A}}, fold(
$self
->toiCal(
$uid
,
$c
));
}
sub
DELETE {
my
$self
=
shift
;
my
$uid
=
shift
;
my
$index
=
$self
->seekUid(
$uid
);
return
defined
$index
?
$self
->removeComponent(
$index
) : 0;
}
sub
CLEAR {
my
$self
=
shift
;
@{
$self
->{A}} = ();
}
sub
DESTROY {
my
$self
=
shift
;
untie
$self
->{A};
}
sub
debug {
my
$self
=
shift
;
print
(STDERR
shift
,
"\n"
)
if
$self
->{debug};
}
sub
unfold {
my
$self
=
shift
;
my
$index
=
shift
;
my
$result
= ${
$self
->{A}}[
$index
];
my
$i
= 1;
until
(${
$self
->{A}}[
$index
+
$i
] !~ /^ (.*)$/s) {
$result
.= $1;
$i
++;
}
$self
->debug(
"unfolded index $index to $result"
);
return
$result
;
}
sub
seekUid {
my
$self
=
shift
;
my
$uid
=
shift
;
my
$index
;
if
(
exists
$self
->{C}->{
$uid
}) {
$self
->debug(
"found cached index for $uid, checking.."
);
$index
=
$self
->{C}->{
$uid
};
if
(
$self
->unfold(
$index
) =~ /^UID.*:(.*)$/) {
if
($1 eq
$uid
) {
$self
->debug(
"found key $uid in cache"
);
return
$index
;
}
else
{
$self
->debug(
"could not find key $uid in cache, deleting"
);
delete
$self
->{C}->{
$uid
};
}
}
else
{
warn
(
"seekUid: discovered illegal UID property format, should be like UID;...:..., ignoring for now\n"
);
}
}
$index
= 0;
for
my
$line
(@{
$self
->{A}}) {
if
(
substr
(
$line
, 0, 3) eq
'UID'
) {
if
(
$self
->unfold(
$index
) =~ /^UID.*:(.*)$/) {
$self
->{C}->{$1} =
$index
;
if
($1 eq
$uid
) {
$self
->debug(
"found key $uid"
);
return
$index
;
}
}
else
{
warn
(
"discovered illegal UID property format, should be like UID;...:..., ignoring for now\n"
);
}
}
$index
++;
}
return
undef
;
}
sub
removeComponent {
my
$self
=
shift
;
my
$index
=
shift
;
my
$i
;
$i
= 0;
$i
++
until
${
$self
->{A}}[
$index
-
$i
] =~ /^BEGIN:(\w+)$/;
my
$si
=
$index
-
$i
;
my
$component
= $1;
$i
= 0;
$i
++
until
${
$self
->{A}}[
$index
+
$i
] =~ /^END:
$component
/;
my
$fi
=
$index
+
$i
;
$self
->debug(
"component $component found between [$si, $fi]"
);
splice
@{
$self
->{A}},
$si
,
$fi
-
$si
+ 1;
}
sub
toiCal {
my
$self
=
shift
;
my
$uid
=
shift
;
my
$c
=
shift
;
my
$excludeComponent
=
shift
;
my
@lines
;
my
(
$component
,
$e
) =
$excludeComponent
? (
undef
,
$c
) :
@$c
;
push
@lines
,
"BEGIN:VCALENDAR"
,
"VERSION:2.0"
,
"PRODID:-//Numen Inest/NONSGML Tie::iCal $VERSION//EN"
,
"BEGIN:$component"
,
"UID:$uid"
if
!
$excludeComponent
;
foreach
my
$name
(
keys
%$e
) {
if
(
$name
eq
'RRULE'
) {
if
(
ref
(
$$e
{
$name
}) ne
'HASH'
) {
warn
"RRULE property should be expressed as a hash, ignoring..\n"
;
}
else
{
my
@rrule
;
foreach
my
$k
(
keys
%{
$$e
{
$name
}}) {
push
@rrule
,
ref
(${
$$e
{
$name
}}{
$k
}) eq
'ARRAY'
?
"$k="
.
join
(
','
, @{${
$$e
{
$name
}}{
$k
}}) :
"$k="
.${
$$e
{
$name
}}{
$k
};
}
push
@lines
,
"$name:"
.
join
(
';'
,
@rrule
);
}
}
elsif
(
ref
(\
$$e
{
$name
}) eq
'SCALAR'
) {
push
@lines
,
"$name:$$e{$name}"
;
}
elsif
(
ref
(
$$e
{
$name
}) eq
'ARRAY'
) {
if
(@{
$$e
{
$name
}} && !
grep
({
ref
(
$_
) ne
'HASH'
} @{
$$e
{
$name
}})) {
push
@lines
,
"BEGIN:$name"
;
foreach
my
$sc
(@{
$$e
{
$name
}}) {
push
@lines
,
$self
->toiCal(
undef
,
$sc
, 1);
}
push
@lines
,
"END:$name"
;
}
elsif
(@{
$$e
{
$name
}} && !
grep
({
ref
(
$_
) ne
'ARRAY'
} @{
$$e
{
$name
}})) {
foreach
my
$cl
(@{
$$e
{
$name
}}) {
if
(
ref
(${
$cl
}[0]) eq
'HASH'
) {
my
(
$params
,
@values
) = @{
$cl
};
push
@lines
,
"$name;"
.
join
(
";"
,
map
{
"$_=$$params{$_}"
}
keys
(
%$params
)).
":"
.
join
(
','
,
@values
);
}
else
{
push
@lines
,
"$name:"
.
join
(
','
,@{
$cl
});
}
}
}
else
{
my
(
$params
,
@values
) = @{
$$e
{
$name
}};
push
@lines
,
"$name;"
.
join
(
";"
,
map
{
"$_=$$params{$_}"
}
keys
(
%$params
)).
":"
.
join
(
','
,
@values
);
}
}
else
{
warn
"ignoring unimplemented "
,
ref
(\${
$e
}{
$name
}),
" -> "
,
$name
.
"\n"
;
}
}
push
@lines
,
"END:$component"
,
"END:VCALENDAR"
if
!
$excludeComponent
;
return
@lines
;
}
sub
parse_line {
no
warnings;
my
(
$delimiter
,
$line
) =
@_
;
my
(
$word
,
@pieces
);
while
(
length
(
$line
)) {
$line
=~ s/^(["])
((?:\\.|(?!\1)[^\\])*)
\1
|
^((?:\\.|[^\\"])*?)
(\Z(?!\n)|(?-x:
$delimiter
)|(?!^)(?=["]))
//xs or
return
;
my
(
$quote
,
$quoted
,
$unquoted
,
$delim
) = ($1, $2, $3, $4);
return
()
unless
(
defined
(
$quote
) ||
length
(
$unquoted
) ||
length
(
$delim
));
$quoted
=
"$quote$quoted$quote"
;
$word
.=
defined
$quote
?
$quoted
:
$unquoted
;
if
(
length
(
$delim
)) {
push
(
@pieces
,
$word
);
undef
$word
;
}
if
(!
length
(
$line
)) {
push
(
@pieces
,
$word
);
}
}
return
(
@pieces
);
}
sub
toHash {
my
$self
=
shift
;
my
$index
=
shift
;
my
$excludeComponent
=
shift
;
my
$i
;
$i
= 0;
$i
++
until
${
$self
->{A}}[
$index
-
$i
] =~ /^BEGIN:(\w+)$/;
my
$si
=
$index
-
$i
;
my
$component
= $1;
$i
= 0;
$i
++
until
${
$self
->{A}}[
$index
+
$i
] =~ /^END:
$component
/;
my
$fi
=
$index
+
$i
;
$self
->debug(
"component $component found between [$si, $fi]"
);
my
%e
;
my
$subComponent
=
''
;
for
my
$i
(
$si
+1..
$fi
-1) {
next
if
${
$self
->{A}}[
$i
] =~ m/^UID/;
if
(${
$self
->{A}}[
$i
] =~ m/^\w+/) {
my
$contentLine
=
$self
->unfold(
$i
);
if
(
$subComponent
ne
''
) {
$subComponent
=
''
if
$contentLine
=~ /^END:
$subComponent
$/;
next
;
}
elsif
(
$contentLine
=~ /^BEGIN:(\w+)$/) {
$subComponent
= $1;
push
@{
$e
{
$subComponent
}},
$self
->toHash(
$i
, 1);
}
elsif
(
$contentLine
=~ /^[\w-]+;.*$/s) {
my
(
$nameAndParamString
,
@valueFragments
) =
&parse_line
(
':'
,
$contentLine
);
my
@values
=
&parse_line
(
','
,
join
(
':'
,
@valueFragments
));
my
(
$name
,
@params
) =
&parse_line
(
';'
,
$nameAndParamString
);
my
%params
=
map
{
my
(
$p
,
$v
) =
split
(/=/,
$_
);
$p
=>
$v
}
@params
;
if
(
exists
$e
{
$name
}) {
if
(!(@{
$e
{
$name
}} && !
grep
({
ref
(
$_
) ne
'ARRAY'
} @{
$e
{
$name
}}))) {
$self
->debug(
"found singleton data, converting to list.."
);
$e
{
$name
} = [
$e
{
$name
}, [{
%params
},
@values
]];
}
else
{
push
@{
$e
{
$name
}}, [{
%params
},
@values
];
}
}
else
{
$e
{
$name
} = [{
%params
},
@values
];
}
}
elsif
(
$contentLine
=~ /^[\w-]+:.*$/s) {
my
(
$name
,
@valueFragments
) =
&parse_line
(
':'
,
$contentLine
);
my
@values
;
if
(
$name
eq
'RRULE'
) {
my
@params
=
&parse_line
(
';'
,
join
(
':'
,
@valueFragments
));
my
%params
=
map
{
my
(
$p
,
$v
) =
split
(/=/,
$_
);
$p
=>
$v
=~ /,/ ? [
split
(/,/,
$v
)] :
$v
}
@params
;
push
@values
, {
%params
};
}
else
{
@values
=
&parse_line
(
','
,
join
(
':'
,
@valueFragments
));
}
if
(
exists
$e
{
$name
}) {
if
(!(
ref
(
$e
{
$name
}) eq
'ARRAY'
&& @{
$e
{
$name
}} && !
grep
({
ref
(
$_
) ne
'ARRAY'
} @{
$e
{
$name
}}))) {
$self
->debug(
"found singleton data, converting to list.."
);
$e
{
$name
} = [
$e
{
$name
}, [
@values
]];
}
else
{
push
@{
$e
{
$name
}}, [
@values
];
}
}
else
{
if
(
@values
== 0) {
$e
{
$name
} =
""
;
}
elsif
(
@values
== 1) {
$e
{
$name
} =
$values
[0];
}
else
{
$e
{
$name
} = [
@values
];
}
}
}
else
{
warn
(
"discovered illegal property format, should be like NAME;...:..., ignoring for now\n"
);
}
}
}
return
$excludeComponent
? \
%e
: [
$component
, \
%e
] ;
}
1;