1 package XML::RSS::Private::Output::Base;
8 use HTML::Entities qw(encode_entities_numeric encode_entities);
9 use DateTime::Format::Mail;
10 use DateTime::Format::W3CDTF;
21 $self->_initialize(@_);
26 # _main() is a reference to the main XML::RSS module
31 $self->{_main} = shift;
34 return $self->{_main};
41 $self->{_encode_cb} = shift;
44 return $self->{_encode_cb};
51 $self->{_output} = "";
52 $self->_main($args->{main});
53 # TODO : Remove once we have inheritance proper.
54 $self->_rss_out_version($args->{version});
55 if (defined($args->{encode_cb})) {
56 $self->_encode_cb($args->{encode_cb});
59 $self->_encode_cb(\&_default_encode);
65 sub _rss_out_version {
69 $self->{_rss_out_version} = shift;
71 return $self->{_rss_out_version};
75 my ($self, $text) = @_;
76 return $self->_encode_cb()->($self, $text);
80 my ($self, $text) = @_;
82 #return "" unless defined $text;
83 if (!defined($text)) {
84 confess "\$text is undefined in XML::RSS::_encode(). We don't know how " . "to handle it!";
87 return $text if (!$self->_main->_encode_output);
89 my $encoded_text = '';
91 while ($text =~ s/(.*?)(\<\!\[CDATA\[.*?\]\]\>)//s) {
93 # we use &named; entities here because it's HTML
94 $encoded_text .= encode_entities($1) . $2;
97 # we use numeric entities here because it's XML
98 $encoded_text .= encode_entities_numeric($text);
100 return $encoded_text;
104 my ($self, $string) = @_;
105 $self->{_output} .= $string;
110 my ($self, $tag, $inner) = @_;
111 my $content = $inner;
113 if (ref($inner) eq 'HASH') {
114 my %inner_copy = %$inner;
115 $content = delete $inner_copy{content};
116 foreach my $key (keys %inner_copy) {
117 my $value = $inner->{$key};
118 if (defined($value)) {
119 $attr .= " " . $self->_encode($key) . qq{="}
120 . $self->_encode($value) . '"'
125 return $self->_out("<$tag$attr>" . $self->_encode($content) . "</$tag>\n");
128 # Remove non-alphanumeric elements and return the modified string.
129 # Useful for user-specified tags' attributes.
132 my ($self, $string) = @_;
134 $string =~ s{[^a-zA-Z_\-0-9]}{}g;
139 my ($self, $prefix, $tag, $inner) = @_;
141 if (ref($inner) eq "HASH")
143 $self->_out("<${prefix}:${tag}");
144 foreach my $attr (sort { $a cmp $b } keys(%{$inner}))
148 . $self->_sanitize($attr)
150 . $self->_encode($inner->{$attr})
158 return $self->_out_tag("${prefix}:${tag}", $inner);
162 sub _out_defined_tag {
163 my ($self, $tag, $inner) = @_;
165 if (defined($inner)) {
166 $self->_out_tag($tag, $inner);
173 my ($self, $tag, $inner) = @_;
175 if (ref($inner) eq "ARRAY") {
176 foreach my $elem (@$inner)
178 $self->_out_defined_tag($tag, $elem);
182 $self->_out_defined_tag($tag, $inner);
189 my ($self, $params, $tag) = @_;
191 if (ref($params) eq "") {
192 $params = {'ext' => $params, 'defined' => 0,};
195 my $ext_tag = $params->{ext};
197 if (ref($ext_tag) eq "") {
198 $ext_tag = $self->$ext_tag();
201 my $value = $ext_tag->{$tag};
203 if ($params->{defined} ? defined($value) : 1) {
204 $self->_out_tag($tag, $value);
210 sub _output_item_tag {
211 my ($self, $item, $tag) = @_;
213 return $self->_out_tag($tag, $item->{$tag});
216 sub _output_def_image_tag {
217 my ($self, $tag) = @_;
219 return $self->_out_inner_tag({ext => "image", 'defined' => 1}, $tag);
222 sub _output_multiple_tags {
223 my ($self, $ext_tag, $tags_ref) = @_;
225 foreach my $tag (@$tags_ref) {
226 $self->_out_inner_tag($ext_tag, $tag);
232 sub _output_common_textinput_sub_elements {
235 $self->_output_multiple_tags("textinput", [qw(title description name link)],);
239 sub _get_top_elem_about {
243 sub _start_top_elem {
244 my ($self, $tag, $about_sub) = @_;
246 my $about = $self->_get_top_elem_about($tag, $about_sub);
248 return $self->_out("<$tag$about>\n");
251 sub _out_textinput_rss_1_0_elems {
254 sub _get_textinput_tag {
258 sub _output_complete_textinput {
261 my $master_tag = $self->_get_textinput_tag();
263 if (defined(my $link = $self->textinput('link'))) {
264 $self->_start_top_elem($master_tag,
268 $self->_output_common_textinput_sub_elements();
270 $self->_out_textinput_rss_1_0_elems();
272 $self->_end_top_level_elem($master_tag);
281 my $ret = $self->{_output};
282 $self->{_output} = "";
284 # Detach _main to avoid referencing loops.
294 sub _date_from_dc_date {
295 my ($self, $string) = @_;
296 my $f = DateTime::Format::W3CDTF->new();
297 return $f->parse_datetime($string);
300 sub _date_from_rss2 {
301 my ($self, $string) = @_;
302 my $f = DateTime::Format::Mail->new();
303 return $f->parse_datetime($string);
307 my ($self, $date) = @_;
309 my $pf = DateTime::Format::Mail->new();
310 return $pf->format_datetime($date);
313 sub _date_to_dc_date {
314 my ($self, $date) = @_;
316 my $pf = DateTime::Format::W3CDTF->new();
317 return $pf->format_datetime($date);
322 my ($self, $key) = @_;
324 if ($self->channel('dc')) {
325 return $self->channel('dc')->{$key};
334 my ($self, $key) = @_;
336 if ($self->channel('syn')) {
337 return $self->channel('syn')->{$key};
344 sub _calc_lastBuildDate {
346 if (defined(my $d = $self->_channel_dc('date'))) {
347 return $self->_date_to_rss2($self->_date_from_dc_date($d));
351 # If lastBuildDate is undef we can still return it because we
352 # need to return undef.
353 return $self->channel("lastBuildDate");
360 if (defined(my $d = $self->channel('pubDate'))) {
363 elsif (defined(my $d2 = $self->_channel_dc('date'))) {
364 return $self->_date_to_rss2($self->_date_from_dc_date($d2));
371 sub _get_other_dc_date {
374 if (defined(my $d1 = $self->channel('pubDate'))) {
377 elsif (defined(my $d2 = $self->channel('lastBuildDate'))) {
388 if (defined(my $d1 = $self->_channel_dc('date'))) {
392 my $date = $self->_get_other_dc_date();
394 if (!defined($date)) {
398 return $self->_date_to_dc_date($self->_date_from_rss2($date));
403 sub _output_xml_declaration {
406 my $encoding = (defined $self->_main->_encoding())? ' encoding="' . $self->_main->_encoding() . '"' : "";
407 $self->_out('<?xml version="1.0"' . $encoding . '?>' . "\n");
408 if (defined(my $stylesheet = $self->_main->_stylesheet)) {
409 my $style_url = $self->_encode($stylesheet);
410 $self->_out(qq{<?xml-stylesheet type="text/xsl" href="$style_url"?>\n});
418 sub _out_image_title_and_url {
421 return $self->_output_multiple_tags({ext => "image"}, [qw(title url)]);
427 $self->_start_top_elem("image", sub { $self->image('url') });
429 $self->_out_image_title_and_url();
431 $self->_output_def_image_tag("link");
437 my ($self, $item) = @_;
440 my $base = $item->{'xml:base'};
441 $tag .= qq{ xml:base="$base"} if defined $base;
442 $self->_start_top_elem($tag, sub { $self->_get_item_about($item)});
444 $self->_output_common_item_tags($item);
449 sub _end_top_level_elem {
450 my ($self, $elem) = @_;
452 $self->_out("</$elem>\n");
456 shift->_end_top_level_elem("item");
460 shift->_end_top_level_elem("image");
464 shift->_end_top_level_elem("channel");
467 sub _output_array_item_tag {
468 my ($self, $item, $tag) = @_;
470 if (defined($item->{$tag})) {
471 $self->_out_array_tag($tag, $item->{$tag});
477 sub _output_def_item_tag {
478 my ($self, $item, $tag) = @_;
480 if (defined($item->{$tag})) {
481 $self->_output_item_tag($item, $tag);
487 sub _get_item_defined {
492 my ($self, $item) = @_;
493 return $self->_output_def_item_tag($item, "description");
496 # Outputs the common item tags for RSS 0.9.1 and above.
497 sub _output_common_item_tags {
498 my ($self, $item) = @_;
500 $self->_output_multiple_tags(
501 {ext => $item, 'defined' => $self->_get_item_defined},
504 $self->_out_item_desc($item);
509 sub _output_common_channel_elements {
512 $self->_output_multiple_tags("channel", [qw(title link description)],);
519 return $self->_out_channel_self_dc_field("language");
525 $self->_start_top_elem("channel", sub { $self->_get_channel_rdf_about });
527 $self->_output_common_channel_elements();
529 $self->_out_language();
534 # Calculates a channel field that has a dc: and non-dc alternative,
535 # prefering the dc: one.
536 sub _calc_channel_dc_field {
537 my ($self, $dc_key, $non_dc_key) = @_;
539 my $dc_value = $self->_channel_dc($dc_key);
541 return defined($dc_value) ? $dc_value : $self->channel($non_dc_key);
548 $self->{_prefer_dc} = shift;
550 return $self->{_prefer_dc};
553 sub _calc_channel_dc_field_params {
554 my ($self, $dc_key, $non_dc_key) = @_;
558 $self->_prefer_dc() ? "dc:$dc_key" : $non_dc_key,
559 $self->_calc_channel_dc_field($dc_key, $non_dc_key)
563 sub _out_channel_dc_field {
564 my ($self, $dc_key, $non_dc_key) = @_;
566 return $self->_out_defined_tag(
567 $self->_calc_channel_dc_field_params($dc_key, $non_dc_key),
571 sub _out_channel_array_self_dc_field {
572 my ($self, $key) = @_;
574 $self->_out_array_tag(
575 $self->_calc_channel_dc_field_params($key, $key),
579 sub _out_channel_self_dc_field {
580 my ($self, $key) = @_;
582 return $self->_out_channel_dc_field($key, $key);
585 sub _out_managing_editor {
588 return $self->_out_channel_dc_field("publisher", "managingEditor");
594 return $self->_out_channel_dc_field("creator", "webMaster");
600 return $self->_out_channel_dc_field("rights", "copyright");
606 $self->_out_managing_editor;
607 $self->_out_webmaster;
610 sub _get_channel_rdf_about {
613 if (defined(my $about = $self->channel('about'))) {
617 return $self->channel('link');
621 sub _output_taxo_topics {
622 my ($self, $elem) = @_;
624 if (my $list = $elem->{'taxo'}) {
625 $self->_out("<taxo:topics>\n <rdf:Bag>\n");
626 foreach my $taxo (@{$list}) {
627 $self->_out(" <rdf:li resource=\"" . $self->_encode($taxo) . "\" />\n");
629 $self->_out(" </rdf:Bag>\n</taxo:topics>\n");
635 # Output the Dublin core properties of a certain elements (channel, image,
638 sub _get_dc_ok_fields {
641 return $self->_main->_get_dc_ok_fields();
644 sub _out_dc_elements {
647 my $skip_hash = shift || {};
649 foreach my $dc (@{$self->_get_dc_ok_fields()}) {
650 next if $skip_hash->{$dc};
652 $self->_out_array_tag("dc:$dc", $elem->{dc}->{$dc});
658 sub _out_module_prefix_elements_hash
660 my ($self, $args) = @_;
662 my $prefix = $args->{prefix};
663 my $data = $args->{data};
664 my $url = $args->{url};
666 while (my ($el, $value) = each(%$data)) {
667 $self->_out_module_prefix_pair(
679 sub _out_module_prefix_pair
681 my ($self, $args) = @_;
683 my $prefix = $args->{prefix};
684 my $url = $args->{url};
686 my $el = $args->{el};
687 my $value = $args->{val};
689 if ($self->_main->_is_rdf_resource($el,$url)) {
691 qq{<${prefix}:${el} rdf:resource="} . $self->_encode($value) . qq{" />\n});
694 $self->_out_ns_tag($prefix, $el, $value);
700 sub _out_module_prefix_elements_array
702 my ($self, $args) = @_;
704 my $prefix = $args->{prefix};
705 my $data = $args->{data};
706 my $url = $args->{url};
708 foreach my $element (@$data)
710 $self->_out_module_prefix_pair(
713 el => $element->{'el'},
714 val => $element->{'val'},
722 sub _out_module_prefix_elements
724 my ($self, $args) = @_;
726 my $data = $args->{'data'};
729 # Do nothing - empty data
732 elsif (ref($data) eq "HASH") {
733 return $self->_out_module_prefix_elements_hash($args);
735 elsif (ref($data) eq "ARRAY") {
736 return $self->_out_module_prefix_elements_array($args);
739 die "Don't know how to handle module data of type " . ref($data) . "!";
743 # Output the Ad-hoc modules
744 sub _out_modules_elements {
745 my ($self, $super_elem) = @_;
748 while (my ($url, $prefix) = each %{$self->_modules}) {
749 next if $prefix =~ /^(dc|syn|taxo)$/;
751 $self->_out_module_prefix_elements(
755 data => $super_elem->{$prefix},
764 sub _out_complete_outer_tag {
765 my ($self, $outer, $inner) = @_;
767 my $value = $self->_main->{$outer}->{$inner};
769 if (defined($value)) {
770 $self->_out("<$outer>\n");
771 $self->_out_array_tag($inner, $value);
772 $self->_end_top_level_elem($outer);
777 my ($self, $what) = @_;
779 return $self->_out_complete_outer_tag("skip\u${what}s", $what);
782 sub _out_skip_hours {
783 return shift->_out_skip_tag("hour");
787 return shift->_out_skip_tag("day");
792 my ($self, $item) = @_;
793 return defined($item->{'about'}) ? $item->{'about'} : $item->{'link'};
796 sub _out_image_dc_elements {
799 sub _out_modules_elements_if_supported {
802 sub _out_image_dims {
805 sub _output_defined_image {
808 $self->_start_image();
810 $self->_out_image_dims;
813 #$output .= '<rss091:width>'.$self->{image}->{width}.'</rss091:width>'."\n"
814 # if $self->{image}->{width};
817 #$output .= '<rss091:height>'.$self->{image}->{height}.'</rss091:height>'."\n"
818 # if $self->{image}->{height};
821 #$output .= '<rss091:description>'.$self->{image}->{description}.'</rss091:description>'."\n"
822 # if $self->{image}->{description};
824 $self->_out_image_dc_elements;
826 $self->_out_modules_elements_if_supported($self->image());
831 sub _is_image_defined {
834 return defined ($self->image('url'));
837 sub _output_complete_image {
840 if ($self->_is_image_defined())
842 $self->_output_defined_image();
850 $self->_out("<items>\n <rdf:Seq>\n");
852 foreach my $item (@{$self->_main->_get_items()}) {
853 $self->_out(' <rdf:li rdf:resource="' .
854 $self->_encode($self->_get_item_about($item)) .
858 $self->_out(" </rdf:Seq>\n</items>\n");
861 sub _get_first_rdf_decl_mappings {
865 sub _get_rdf_decl_mappings
869 my $modules = $self->_modules();
873 $self->_get_first_rdf_decl_mappings(),
874 map { [$modules->{$_}, $_] } keys(%$modules)
879 my ($self, $prefix, $url) = @_;
881 my $pp = defined($prefix) ? ":$prefix" : "";
883 return qq{ xmlns$pp="$url"\n};
886 sub _get_rdf_xmlnses {
891 map { $self->_render_xmlns(@$_) }
892 @{$self->_get_rdf_decl_mappings}
896 sub _get_rdf_decl_open_tag {
897 return qq{<rss version="2.0"\n};
904 my $base = $self->_main()->{'xml:base'};
905 my $base_decl = (defined $base)? qq{ xml:base="$base"\n} : "";
906 return $self->_get_rdf_decl_open_tag() . $base_decl .
907 $self->_get_rdf_xmlnses() . ">\n\n";
914 return $self->_out($self->_get_rdf_decl);
918 my ($self, $item) = @_;
920 # The unique identifier. Use 'permaLink' for an external
921 # identifier, or 'guid' for a internal string.
922 # (I call it permaLink in the hash for purposes of clarity.)
924 for my $guid (qw(permaLink guid)) {
925 if (defined $item->{$guid}) {
926 $self->_out('<guid isPermaLink="'
927 . ($guid eq 'permaLink' ? 'true' : 'false') . '">'
928 . $self->_encode($item->{$guid})
935 sub _out_item_source {
936 my ($self, $item) = @_;
938 if (defined $item->{source} && defined $item->{sourceUrl}) {
939 $self->_out('<source url="'
940 . $self->_encode($item->{sourceUrl}) . '">'
941 . $self->_encode($item->{source})
946 sub _out_single_item_enclosure {
947 my ($self, $item, $enc) = @_;
953 map { "$_=\"" . $self->_encode($enc->{$_}) . '"' } keys(%$enc)
959 sub _out_item_enclosure {
960 my ($self, $item) = @_;
962 if (my $enc = $item->{enclosure}) {
964 (ref($enc) eq "ARRAY") ? @$enc : ($enc)
967 $self->_out_single_item_enclosure($item, $sub)
973 return shift->_main->{items};
976 sub _get_filtered_items {
977 return shift->_get_items;
980 sub _out_item_2_0_tags {
983 sub _out_item_1_0_tags {
986 sub _output_single_item {
987 my ($self, $item) = @_;
989 $self->_start_item($item);
991 $self->_out_item_2_0_tags($item);
993 $self->_out_item_1_0_tags($item);
995 $self->_out_modules_elements_if_supported($item);
997 $self->_end_item($item);
1003 foreach my $item (@{$self->_get_filtered_items}) {
1004 $self->_output_single_item($item);
1008 sub _output_main_elements {
1011 $self->_output_complete_image();
1013 $self->_output_items;
1015 $self->_output_complete_textinput();
1018 # Outputs the last elements - for RSS versions 0.9.1 and 2.0 .
1019 sub _out_last_elements {
1024 $self->_output_main_elements;
1026 $self->_out_skip_hours();
1028 $self->_out_skip_days();
1030 $self->_end_channel;
1033 sub _calc_prefer_dc {
1037 sub _output_xml_start {
1040 $self->_prefer_dc($self->_calc_prefer_dc());
1042 $self->_output_xml_declaration();
1044 $self->_out_rdf_decl;
1046 $self->_start_channel();
1056 return $self->_out("</" . $self->_get_end_tag() . ">");
1059 sub _out_all_modules_elems {
1062 # Dublin Core module
1063 $self->_out_dc_elements($self->channel(),
1064 {map { $_ => 1 } qw(language creator publisher rights date)},
1067 # Syndication module
1068 foreach my $syn (@{$self->_main->_get_syn_ok_fields}) {
1069 if (defined(my $value = $self->_channel_syn($syn))) {
1070 $self->_out_ns_tag("syn", $syn, $value);
1075 $self->_output_taxo_topics($self->channel());
1077 $self->_out_modules_elements($self->channel());
1083 $self->_out_defined_tag("pubDate", $self->_calc_pubDate());
1084 $self->_out_defined_tag("lastBuildDate", $self->_calc_lastBuildDate());
1087 sub _out_def_chan_tag {
1088 my ($self, $tag) = @_;
1089 return $self->_output_multiple_tags(
1090 {ext => "channel", 'defined' => 1},
1095 # $self->_render_complete_rss_output($xml_version)
1097 # This function is the workhorse of the XML output and does all the work of
1098 # rendering the RSS, delegating the work to specialised functions.
1100 # It accepts the requested version number as its argument.
1102 sub _render_complete_rss_output {
1105 $self->_output_xml_start();
1107 $self->_output_rss_middle;
1109 $self->_out_end_tag;
1111 return $self->_flush_output();
1115 ### Delegate the XML::RSS accessors to _main
1119 return shift->_main->channel(@_);
1123 return shift->_main->image(@_);
1127 return shift->_main->textinput(@_);
1131 return shift->_main->_modules();