1# $Id$
2#
3# Copyright (c) 2005-2007 Daisuke Maki <daisuke@endeworks.jp>
4# All rights reserved.
5
6package XML::RSS::LibXML::V2_0;
7use strict;
8use warnings;
9use base qw(XML::RSS::LibXML::ImplBase);
10use DateTime::Format::W3CDTF;
11use DateTime::Format::Mail;
12
13my %DcElements = (
14    (map { ("dc:$_" => [ { module => 'dc', element => $_ } ]) }
15        qw(language rights date publisher creator title subject description contributer type format identifier source relation coverage)),
16);
17
18my %SynElements = (
19    (map { ("syn:$_" => [ { module => 'syn', element => $_ } ]) }
20        qw(updateBase updateFrequency updatePeriod)),
21);
22my $format_dates = sub {
23    my $v = eval {
24        DateTime::Format::Mail->format_datetime(
25            DateTime::Format::W3CDTF->parse_datetime($_[0])
26        );
27    };
28    if ($v && ! $@) {
29        $_[0] = $v;
30    }
31};
32
33my %ChannelElements = (
34    %DcElements,
35    %SynElements,
36    (map { ($_ => [ $_ ]) } qw(title link description)),
37    language => [ { module => 'dc', element => 'language' }, 'language' ],
38    copyright => [ { module => 'dc', element => 'rights' }, 'copyright' ],
39    pubDate   => {
40        candidates => [ 'pubDate', { module => 'dc', element => 'date' } ],
41        callback   => $format_dates,
42    },
43    lastBuildDate => {
44        candidates => [ { module => 'dc', element => 'date' }, 'lastBuildDate' ],
45        callback   => $format_dates,
46    },
47    docs => [ 'docs' ],
48    managingEditor => [ { module => 'dc', element => 'publisher' }, 'managingEditor' ],
49    webMaster => [ { module => 'dc', element => 'creator' }, 'webMaster' ],
50    category => [ { module => 'dc', element => 'category' }, 'category' ],
51    generator => [ { module => 'dc', element => 'generator' }, 'generator' ],
52    ttl => [ { module => 'dc', element => 'ttl' }, 'ttl' ],
53);
54
55my %ItemElements = (
56    %DcElements,
57    enclosure => ['enclosure'],
58    map { ($_ => [$_]) }
59        qw(title link description author category comments pubDate)
60);
61
62my %ImageElements = (
63    (map { ($_ => [$_]) } qw(title url link width height description)),
64    %DcElements,
65);
66
67my %TextInputElements = (
68    (map { ($_ => [$_]) } qw(title link description name)),
69    %DcElements
70);
71
72sub definition
73{
74    return +{
75        channel => {
76            title          => '',
77            'link'         => '',
78            description    => '',
79            language       => undef,
80            copyright      => undef,
81            managingEditor => undef,
82            webMaster      => undef,
83            pubDate        => undef,
84            lastBuildDate  => undef,
85            category       => undef,
86            generator      => undef,
87            docs           => undef,
88            cloud          => '',
89            ttl            => undef,
90            image          => '',
91            textinput      => '',
92            skipHours      => '',
93            skipDays       => '',
94        },
95        image => bless ({
96            title       => undef,
97            url         => undef,
98            'link'      => undef,
99            width       => undef,
100            height      => undef,
101            description => undef,
102        }, 'XML::RSS::LibXML::ElementSpec'),
103        skipDays  => bless ({
104            day => undef,
105        }, 'XML::RSS::LibXML::ElementSpec'),
106        skipHours => bless ({
107            hour => undef,
108        }, 'XML::RSS::LibXML::ElementSpec'),
109        textinput => bless ({
110            title       => undef,
111            description => undef,
112            name        => undef,
113            'link'      => undef,
114        }, 'XML::RSS::LibXML::ElementSpec'),
115    };
116}
117
118sub parse_dom
119{
120    my $self = shift;
121    my $c    = shift;
122    my $dom  = shift;
123
124    $c->reset;
125    $c->version('2.0');
126    $c->encoding($dom->encoding);
127    $self->parse_base($c, $dom);
128    $self->parse_namespaces($c, $dom);
129    $self->parse_channel($c, $dom);
130    $self->parse_items($c, $dom);
131    $self->parse_misc_simple($c, $dom);
132}
133
134
135sub parse_channel
136{
137    my ($self, $c, $dom) = @_;
138
139    my $xc = $c->create_xpath_context($c->{namespaces});
140
141    my ($root) = $xc->findnodes('/rss/channel', $dom);
142    my %h = $self->parse_children($c, $root, './*[name() != "item"]');
143
144    foreach my $type (qw(day hour)) {
145        my $field = 'skip' . ucfirst($type) . 's';
146        if (my $skip = delete $h{$field}) {
147            if (ref $skip ne 'HASH') {
148#                warn "field $field has invalid entry (does this RSS validate?)";
149            } elsif (! UNIVERSAL::isa($skip, 'XML::RSS::LibXML::ElementSpec')) {
150                $c->$field(UNIVERSAL::isa($skip, 'XML::RSS::LibXML::MagicElement') ? $skip : %$skip);
151            }
152        }
153    }
154
155    foreach my $field (qw(textinput image)) {
156        if (my $v = $h{$field}) {
157            if (ref $v ne 'HASH') {
158#                warn "field $field has invalid entry (does this RSS validate?)";
159            } elsif (! UNIVERSAL::isa($v, 'XML::RSS::LibXML::ElementSpec')) {
160                $c->$field(UNIVERSAL::isa($v, 'XML::RSS::LibXML::MagicElement') ? $v : %$v);
161            }
162        }
163    }
164    $c->channel(%h);
165}
166
167sub parse_items
168{
169    my ($self, $c, $dom) = @_;
170    my @items;
171    my $version = $c->version;
172    my $xc      = $c->create_xpath_context($c->{namespaces});
173    my $xpath   = '/rss/channel/item';
174    foreach my $item ($xc->findnodes($xpath, $dom)) {
175        my $i = $self->parse_children($c, $item);
176        $self->add_item($c, $i);
177    }
178}
179
180sub parse_misc_simple
181{
182    my ($self, $c, $dom) = @_;
183
184    my $xc = $c->create_xpath_context($c->{namespaces});
185    foreach my $node ($xc->findnodes('/rss/*[name() != "channel" and name() != "item"]', $dom)) {
186        my $h = $self->parse_children($c, $node);
187        my $name = $node->localname;
188        my $prefix = $node->getPrefix();
189
190        $name = 'textinput' if $name eq 'textInput';
191
192        if ($prefix) {
193            $c->{$prefix} ||= {};
194            $self->store_element($c->{$prefix}, $name, $h);
195
196            # XML::RSS requires us to allow access to elements both from
197            # the prefix and the namespace
198            $c->{$c->{namespaces}{$prefix}} ||= {};
199            $self->store_element($c->{$c->{namespaces}{$prefix}}, $name, $h);
200        } else {
201            $self->store_element($c, $name, $h);
202        }
203    }
204}
205
206sub create_dom
207{
208    my ($self, $c) = @_;
209
210    my $dom = $self->SUPER::create_dom($c);
211    my $root = $dom->getDocumentElement();
212    my $xc = $c->create_xpath_context(scalar $c->namespaces);
213    my($channel) = $xc->findnodes('/rss/channel', $dom);
214
215    if (my $image = $c->image) {
216        my $inode = $dom->createElement('image');
217        $self->create_element_from_spec($image, $dom, $inode, \%ImageElements);
218        $self->create_extra_modules($image, $dom, $inode, $c->namespaces);
219        $channel->appendChild($inode);
220    }
221
222    if (my $textinput = $c->textinput) {
223        my $inode = $dom->createElement('textInput');
224        $self->create_element_from_spec($textinput, $dom, $inode, \%TextInputElements);
225        $self->create_extra_modules($textinput, $dom, $inode, $c->namespaces);
226        $channel->appendChild($inode);
227    }
228
229    return $dom;
230}
231
232sub create_rootelement
233{
234    my ($self, $c, $dom) = @_;
235    my $root = $dom->createElement('rss');
236    $root->setAttribute(version => '2.0');
237    if (my $base = $c->base) {
238        $root->setAttribute('xml:base' => $base);
239    }
240    $dom->setDocumentElement($root);
241}
242
243sub create_channel
244{
245    my ($self, $c, $dom) = @_;
246
247    my $root = $dom->getDocumentElement();
248    my $channel = $dom->createElement('channel');
249
250    $self->create_element_from_spec($c->channel, $dom, $channel, \%ChannelElements);
251
252    foreach my $type (qw(day hour)) {
253        my $field = 'skip' . ucfirst($type) . 's';
254        my $skip = $c->$field;
255        if ($skip && defined $skip->{$type}) {
256            my $sd = $dom->createElement($field);
257            my $d  = $dom->createElement($type);
258            $d->appendChild($dom->createTextNode($skip->{$type}));
259            $sd->appendChild($d);
260            $channel->appendChild($sd);
261        }
262    }
263    $root->appendChild($channel);
264}
265
266sub create_items
267{
268    my ($self, $c, $dom) = @_;
269
270    my ($channel) = $dom->findnodes('/rss/channel');
271    foreach my $i ($c->items) {
272        my $item = $dom->createElement('item');
273        $self->create_element_from_spec($i, $dom, $item, \%ItemElements);
274        $self->create_extra_modules($i, $dom, $item, $c->namespaces);
275        my $guid = $i->{guid};
276        if (defined $guid) {
277            my $guid_element = $dom->createElement('guid');
278            if (eval { $guid->isa('XML::RSS::LibXML::MagicElement') }) {
279                my $isperma = 'true';
280                if (! $guid->{isPermaLink} || $guid->{isPermaLink} ne 'true') {
281                    $isperma = 'false';
282                }
283                $guid_element->setAttribute(isPermaLink => $isperma);
284                $guid_element->appendChild($dom->createTextNode($guid->toString));
285            } else {
286                $guid_element->setAttribute(isPermaLink => "false");
287                $guid_element->appendChild($dom->createTextNode($guid));
288            }
289            $item->appendChild($guid_element);
290        }
291
292        $channel->appendChild($item);
293    }
294}
295
2961;
297