File Coverage

blib/lib/XML/Generator/RSS10.pm
Criterion Covered Total %
statement 33 204 16.1
branch 0 42 0.0
condition 0 3 0.0
subroutine 11 30 36.6
pod 6 6 100.0
total 50 285 17.5


line stmt bran cond sub pod time code
1             package XML::Generator::RSS10;
2             {
3             $XML::Generator::RSS10::VERSION = '0.02';
4             }
5              
6 10     10   7519 use strict;
  10         19  
  10         830  
7              
8 10     10   76 use vars qw($VERSION);
  10         20  
  10         668  
9              
10 10     10   68 use base 'XML::SAX::Base';
  10         16  
  10         17983  
11              
12 10     10   357022 use Params::Validate qw( validate SCALAR ARRAYREF BOOLEAN OBJECT );
  10         146683  
  10         1194  
13              
14 10     10   7383 use XML::Generator::RSS10::dc;
  10         29  
  10         283  
15 10     10   12222 use XML::Generator::RSS10::sy;
  10         29  
  10         707  
16              
17 10         4252 use constant NEW_SPEC => {
18             modules => {
19             type => ARRAYREF,
20             default => [ 'dc', 'sy' ],
21             },
22             pretty => { type => BOOLEAN, default => 0 },
23             Handler => { type => OBJECT },
24 10     10   60 };
  10         19  
25              
26             sub new {
27 0     0 1   my $class = shift;
28 0           my %p = validate( @_, NEW_SPEC );
29              
30 0           my %mod;
31 0           foreach my $prefix ( @{ delete $p{modules} } ) {
  0            
32 0           my $package = __PACKAGE__ . "::$prefix";
33              
34 0 0         unless ( $package->can('Prefix') ) {
35 0           eval "require $package";
36 0 0         die $@ if $@;
37             }
38              
39 0           $mod{$prefix} = $package;
40             }
41              
42 0           my $self = bless {
43             %p,
44             state => {},
45             modules => \%mod,
46             };
47              
48 0           $self->{state}{indent} = 0;
49 0           $self->{state}{items} = [];
50              
51 0           $self->_start;
52              
53 0           return $self;
54             }
55              
56             sub parse {
57 0     0 1   die __PACKAGE__ . " does not implement RSS parsing\n";
58             }
59              
60             sub _start {
61 0     0     my $self = shift;
62              
63 0           $self->start_document;
64              
65 0           $self->processing_instruction(
66             { Target => 'xml', Data => 'version="1.0"' } );
67              
68 0           $self->_declare_namespaces;
69 0           $self->_newline_if_pretty;
70              
71 0           $self->_start_element( 'rdf', 'RDF' );
72 0           $self->_newline_if_pretty;
73             }
74              
75 10         3179 use constant ITEM_SPEC => (
76             title => { type => SCALAR },
77             link => { type => SCALAR },
78             description => { type => SCALAR, optional => 1 },
79 10     10   61 );
  10         14  
80              
81             sub item {
82 0     0 1   my $self = shift;
83 0           my %p = validate(
84             @_,
85             {
86             ITEM_SPEC,
87 0           map { $_ => { optional => 1 } }
88 0           keys %{ $self->{namespace_prefixes} },
89             },
90             );
91              
92 0           $self->_start_element(
93             '', 'item',
94             [ 'rdf', 'about' => $p{link} ],
95             );
96 0           $self->_newline_if_pretty;
97              
98 0           $self->_contents( \%p, qw( title link ) );
99              
100 0           $self->_call_modules( \%p );
101              
102 0 0         if ( defined $p{description} ) {
103 0           $self->_element_with_cdata( '', 'description', $p{description} );
104 0           $self->_newline_if_pretty;
105             }
106              
107 0           $self->_end_element( '', 'item' );
108 0           $self->_newline_if_pretty;
109              
110 0           push @{ $self->{state}{items} }, $p{link};
  0            
111              
112             }
113              
114 10         3029 use constant IMAGE_SPEC => (
115             title => { type => SCALAR },
116             link => { type => SCALAR },
117             url => { type => SCALAR },
118 10     10   60 );
  10         28  
119              
120             sub image {
121 0     0 1   my $self = shift;
122 0           my %p = validate(
123             @_,
124             {
125             IMAGE_SPEC,
126 0           map { $_ => { optional => 1 } }
127 0           keys %{ $self->{namespace_prefixes} },
128             },
129             );
130              
131 0 0         die "Cannot call image() more than once.\n"
132             if $self->{state}{image};
133              
134 0 0         die "Cannot call image() after calling channel().\n"
135             if $self->{state}{finished};
136              
137 0           $self->_start_element(
138             '', 'image',
139             [ 'rdf', 'about' => $p{url} ],
140             );
141 0           $self->_newline_if_pretty;
142              
143 0           $self->_contents( \%p, qw( title url link ) );
144              
145 0           $self->_call_modules( \%p );
146              
147 0           $self->{state}{image} = $p{url};
148              
149 0           $self->_end_element( '', 'image' );
150 0           $self->_newline_if_pretty;
151             }
152              
153 10         3078 use constant TEXTINPUT_SPEC => (
154             title => { type => SCALAR },
155             description => { type => SCALAR },
156             name => { type => SCALAR },
157             url => { type => SCALAR },
158 10     10   63 );
  10         19  
159              
160             sub textinput {
161 0     0 1   my $self = shift;
162 0           my %p = validate(
163             @_,
164             {
165             TEXTINPUT_SPEC,
166 0           map { $_ => { optional => 1 } }
167 0           keys %{ $self->{namespace_prefixes} },
168             },
169             );
170              
171 0 0         die "Cannot call textinput() more than once().\n"
172             if $self->{state}{textinput};
173              
174 0 0         die "Cannot call textinput() after calling channel().\n"
175             if $self->{state}{finished};
176              
177 0           $self->_start_element(
178             '', 'textinput',
179             [ 'rdf', 'about' => $p{url} ],
180             );
181 0           $self->_newline_if_pretty;
182              
183 0           $self->_contents( \%p, qw( title description name url ) );
184              
185 0           $self->_call_modules( \%p );
186              
187 0           $self->{state}{textinput} = $p{url};
188              
189 0           $self->_end_element( '', 'textinput' );
190 0           $self->_newline_if_pretty;
191             }
192              
193 10         23762 use constant CHANNEL_SPEC => (
194             title => { type => SCALAR },
195             link => { type => SCALAR },
196             description => { type => SCALAR },
197 10     10   59 );
  10         23  
198              
199             sub channel {
200 0     0 1   my $self = shift;
201 0           my %p = validate(
202             @_,
203             {
204             CHANNEL_SPEC,
205 0           map { $_ => { optional => 1 } }
206 0           keys %{ $self->{namespace_prefixes} },
207             },
208             );
209              
210 0           die "Cannot call channel() without any items.\n"
211 0 0         unless @{ $self->{state}{items} };
212              
213 0 0         die "Cannot call channel() more than once.\n"
214             if $self->{state}{finished};
215              
216 0           $self->_start_element(
217             '', 'channel',
218             [ 'rdf', 'about' => $p{link} ],
219             );
220 0           $self->_newline_if_pretty;
221              
222 0           $self->_contents( \%p, qw( title link ) );
223              
224 0           $self->_element_with_cdata( '', 'description', $p{description} );
225 0           $self->_newline_if_pretty;
226              
227 0           foreach my $elt ( grep { $self->{state}{$_} } qw( image textinput ) ) {
  0            
228 0           $self->_element(
229             '', $elt,
230             [ 'rdf', 'resource' => $self->{state}{$elt} ],
231             );
232 0           $self->_newline_if_pretty;
233             }
234              
235 0           $self->_start_element( '', 'items' );
236 0           $self->_newline_if_pretty;
237              
238 0           $self->_start_element( 'rdf', 'Seq' );
239 0           $self->_newline_if_pretty;
240              
241 0           foreach my $i ( @{ $self->{state}{items} } ) {
  0            
242 0           $self->_element(
243             'rdf', 'li',
244             [ 'rdf', 'resource' => $i ],
245             );
246 0           $self->_newline_if_pretty;
247             }
248              
249 0           $self->_end_element( 'rdf', 'Seq' );
250 0           $self->_newline_if_pretty;
251              
252 0           $self->_end_element( '', 'items' );
253 0           $self->_newline_if_pretty;
254              
255 0           $self->_call_modules( \%p );
256              
257 0           foreach my $mod ( values %{ $self->{modules} } ) {
  0            
258 0 0         $mod->channel_hook($self) if $mod->can('channel_hook');
259             }
260              
261 0           $self->_end_element( '', 'channel' );
262 0           $self->_newline_if_pretty;
263              
264 0           $self->_finish;
265              
266 0           $self->{state}{finished} = 1;
267             }
268              
269             sub _finish {
270 0     0     my $self = shift;
271              
272 0           $self->_end_element( 'rdf', 'RDF' );
273 0           $self->_newline_if_pretty;
274              
275 0           $self->end_document;
276             }
277              
278             sub _contents {
279 0     0     my $self = shift;
280 0           my $p = shift;
281 0           my @required = @_;
282              
283 0           for my $elt (@required) {
284 0           $self->_element_with_data( '', $elt, $p->{$elt} );
285 0           $self->_newline_if_pretty;
286             }
287             }
288              
289             sub _call_modules {
290 0     0     my $self = shift;
291 0           my $p = shift;
292              
293 0           foreach my $pre ( sort keys %{ $self->{modules} } ) {
  0            
294 0 0         next unless exists $p->{$pre};
295              
296 0           $self->{modules}{$pre}->contents( $self, $p->{$pre} );
297             }
298             }
299              
300             sub _element {
301 0     0     my $self = shift;
302              
303 0           $self->_start_element(@_);
304 0           $self->_end_element(@_);
305             }
306              
307             sub _element_with_data {
308 0     0     my $self = shift;
309 0           my $data = pop;
310              
311 0           $self->_start_element(@_);
312 0 0         $self->characters( { Data => $data } ) if length $data;
313 0           $self->_end_element(@_);
314             }
315              
316             sub _element_with_cdata {
317 0     0     my $self = shift;
318 0           my $data = pop;
319              
320 0           $self->_start_element(@_);
321 0 0         if ( length $data ) {
322 0           $self->start_cdata;
323 0           $self->characters( { Data => $data } );
324 0           $self->end_cdata;
325             }
326 0           $self->_end_element(@_);
327             }
328              
329             sub _start_element {
330 0     0     my $self = shift;
331 0           my ( $name, $prefix ) = ( shift, shift );
332              
333 0           my %att;
334 0           foreach my $a ( grep { @$_ } @_ ) {
  0            
335 0           my ( $k, $v ) = $self->_rss_att(@$a);
336              
337 0           $att{$k} = $v;
338             }
339              
340 0 0 0       $self->ignorable_whitespace( { Data => ' ' x $self->{state}{indent} } )
341             if $self->{pretty} && $self->{state}{indent};
342              
343 0           $self->start_element(
344             {
345             $self->_rss_name_and_prefix( $name, $prefix ),
346             Attributes => \%att,
347             }
348             );
349              
350 0           $self->{state}{indent}++;
351             }
352              
353             sub _end_element {
354 0     0     my $self = shift;
355              
356 0 0         if ( $self->{pretty} ) {
357 0 0         unless ( ( caller(1) )[3] =~ /(?:_element|_element_with_c?data)$/ ) {
358 0 0         $self->ignorable_whitespace(
359             { Data => ' ' x ( $self->{state}{indent} - 1 ) } )
360             if $self->{state}{indent} > 1;
361             }
362             }
363              
364 0           $self->end_element( { $self->_rss_name_and_prefix(@_) } );
365              
366 0           $self->{state}{indent}--;
367             }
368              
369             sub _newline_if_pretty {
370 0 0   0     $_[0]->ignorable_whitespace( { Data => "\n" } ) if $_[0]->{pretty};
371             }
372              
373             {
374             my %ns = (
375             '' => 'http://purl.org/rss/1.0/',
376             rdf => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
377             );
378              
379             sub _declare_namespaces {
380 0     0     my $self = shift;
381              
382 0           while ( my ( $p, $uri ) = each %ns ) {
383 0           $self->SUPER::start_prefix_mapping(
384             { Prefix => $p, NamespaceURI => $uri } );
385              
386 0           $self->{namespace_prefixes}{$p} = $uri;
387             }
388              
389 0           foreach my $package ( values %{ $self->{modules} } ) {
  0            
390 0           my $p = $package->Prefix;
391 0           my $uri = $package->NamespaceURI;
392              
393 0           $self->SUPER::start_prefix_mapping(
394             { Prefix => $p, NamespaceURI => $uri } );
395              
396 0           $self->{namespace_prefixes}{$p} = $uri;
397             }
398             }
399              
400             sub _rss_name_and_prefix {
401 0     0     my $self = shift;
402 0           my $prefix = shift;
403 0           my $local = shift;
404              
405 0 0         die "Invalid prefix ($prefix)"
406             unless exists $self->{namespace_prefixes}{$prefix};
407              
408 0 0         my $name = $prefix ? "$prefix:$local" : $local;
409              
410             return (
411 0           Name => $name,
412             LocalName => $local,
413             Prefix => $prefix,
414             NamespaceURI => $self->{namespace_prefixes}{$prefix}
415             );
416             }
417              
418             sub _rss_att {
419 0     0     my $self = shift;
420 0           my $prefix = shift;
421 0           my $att = shift;
422 0           my $value = shift;
423              
424 0 0         die "Invalid prefix ($prefix)"
425             unless exists $self->{namespace_prefixes}{$prefix};
426              
427             return (
428 0           "{$self->{namespace_prefixes}{$prefix}}$att" => {
429             $self->_rss_name_and_prefix( $prefix, $att ),
430             Value => $value,
431             },
432             );
433             }
434             }
435              
436             1;
437              
438             # ABSTRACT: Generate SAX events for RSS
439              
440              
441              
442             =pod
443              
444             =head1 NAME
445              
446             XML::Generator::RSS10 - Generate SAX events for RSS
447              
448             =head1 VERSION
449              
450             version 0.02
451              
452             =head1 SYNOPSIS
453              
454             use XML::Generator::RSS10;
455              
456             my $rss = XML::Generator::RSS10->new( Handler => $sax_handler );
457              
458             $rss->item( title => 'Exciting News About my Pants!',
459             link => 'http://pants.example.com/my/news.html',
460             description => 'My pants are full of ants!',
461             );
462              
463             $rss->channel( title => 'Pants',
464             link => 'http://pants.example.com/',
465             description => 'A fascinating pants site',
466             );
467              
468             =head1 DESCRIPTION
469              
470             This module generates SAX events which will create an RSS 1.0
471             document, based on easy to use RSS-related methods like C and
472             C.
473              
474             =head1 METHODS
475              
476             =head2 new
477              
478             This is the constructor for this class.
479              
480             It takes several parameters, though only one, "Handler", is required:
481              
482             =over 4
483              
484             =item * Handler
485              
486             This should be a SAX2 handler. If you are looking to write RSS to a
487             file or store it in a string, you probably want to use
488             C.
489              
490             This parameter is required.
491              
492             =item * pretty
493              
494             If this is true, the generated XML document will include extra spaces
495             and newlines in an effort to make it look pretty. This defaults to
496             false.
497              
498             =item * modules
499              
500             This parameter can be used to make additional RSS 1.0 modules
501             available when creating a feed. It should be an array reference to a
502             list of module prefixes.
503              
504             You can specify any prefix you like, and this module will try to load
505             a module named C<< XML::Generator::RSS10:: >>.
506              
507             This module comes with support for the core RSS 1.0 modules, which are
508             Content (content), Dublin Core (dc), and Syndication (sy). It also
509             include a module supporting the proposed Administrative (admin) and
510             Creative Commons (cc) modules. See the docs for
511             C, C,
512             C, C, and
513             C for details on how to use them.
514              
515             The Dublin Core and Syndication modules are loaded by default if this
516             parameter is not specified.
517              
518             =back
519              
520             The constructor begins the RSS document and returns a new
521             C object.
522              
523             =head2 item
524              
525             This method is used to add item elements to the document. It accepts
526             the following parameters:
527              
528             =over 4
529              
530             =item * title
531              
532             The item's title. Required.
533              
534             =item * link
535              
536             The item's link. Required.
537              
538             =item * description
539              
540             The item's link. Optional.
541              
542             This element will be formatted as CDATA since many people like to put
543             HTML in it.
544              
545             =back
546              
547             =head2 image
548              
549             This method is used to add an image element to the document. It may
550             only be called once. It accepts the following parameters:
551              
552             =over 4
553              
554             =item * title
555              
556             The image's title. Required.
557              
558             =item * link
559              
560             The image's link. Required.
561              
562             =item * url
563              
564             The image's URL. Required.
565              
566             =back
567              
568             =head2 textinput
569              
570             This method is used to add an textinput element to the document. It
571             may only be called once. It accepts the following parameters:
572              
573             =over 4
574              
575             =item * title
576              
577             The textinput's title. Required.
578              
579             =item * description
580              
581             The textinput's description. Required.
582              
583             =item * name
584              
585             The textinput's name. Required.
586              
587             =item * url
588              
589             The textinput's URL. Required.
590              
591             =back
592              
593             =head2 channel
594              
595             This method is used add the channel element to the document. It also
596             finishes the document. You must have added at least one item to the
597             document prior to calling this method.
598              
599             B.
600              
601             =over 4
602              
603             =item * title
604              
605             The channel's title. Required.
606              
607             =item * link
608              
609             The channel's link. Required.
610              
611             =item * description
612              
613             The channel's description. Required.
614              
615             This element will be formatted as CDATA since many people like to put
616             HTML in it.
617              
618             =back
619              
620             =head1 RSS 1.0 MODULES
621              
622             To add module output to a document, you can pass extra hash keys when
623             calling any of the output-generating methods. The extra keys should
624             be the module prefixes, and the values should be something expected by
625             the relevant module.
626              
627             For example, to add some Dublin Core elements to the channel element,
628             you can write this:
629              
630             $rss->channel( title => 'Pants',
631             link => 'http://pants.example.com/',
632             description => 'A fascinating pants site',
633             dc => { publisher => 'The Pants People',
634             rights => 'Mine, all mine!',
635             date => $date,
636             },
637             );
638              
639             The values for the "dc" key will be passed to
640             C, which will add them to the output
641             stream appropriately.
642              
643             =head1 XML::Generator::RSS10 VERSUS XML::RSS
644              
645             This module is less flexible than C in many ways. However,
646             it does have two features that C does not provide:
647              
648             =over 4
649              
650             =item *
651              
652             Because it generates SAX events, this module can be used to write a
653             document to a handle as a stream. C requires you to create
654             the entire document in memory first.
655              
656             =item *
657              
658             It has support for arbitrary RSS 1.0 modules, including ones you
659             create.
660              
661             =back
662              
663             However, if you don't need any of these features you may be better off
664             using C instead.
665              
666             =head1 BUGS
667              
668             Please report any bugs or feature requests to
669             C, or through the web interface
670             at L. I will be notified, and then you'll
671             automatically be notified of progress on your bug as I make changes.
672              
673             =head1 AUTHOR
674              
675             Dave Rolsky
676              
677             =head1 COPYRIGHT AND LICENSE
678              
679             This software is Copyright (c) 2011 by Dave Rolsky.
680              
681             This is free software, licensed under:
682              
683             The Artistic License 2.0 (GPL Compatible)
684              
685             =cut
686              
687              
688             __END__