1use 5.008001; # no good Unicode support? you lose 2use strict; 3use warnings; 4 5package XML::Atom::SimpleFeed; 6 7our $VERSION = '0.904'; 8 9use Carp; 10use Encode (); 11use POSIX (); 12 13my @XML_ENC = 'us-ascii'; # use array because local($myvar) error but local($myvar[0]) OK 14 # and use a lexical because not a public interface 15 16sub ATOM_NS () { 'http://www.w3.org/2005/Atom' } 17sub XHTML_NS () { 'http://www.w3.org/1999/xhtml' } 18sub PREAMBLE () { qq(<?xml version="1.0" encoding="$XML_ENC[0]"?>\n) } 19sub W3C_DATETIME () { '%Y-%m-%dT%H:%M:%S' } 20sub DEFAULT_GENERATOR () { { 21 uri => 'https://metacpan.org/pod/' . __PACKAGE__, 22 version => __PACKAGE__->VERSION || 'git', 23 name => __PACKAGE__, 24} } 25 26#################################################################### 27# superminimal XML writer 28# 29 30sub xml_encoding { local $XML_ENC[0] = shift; &{(shift)} } 31 32my %XML_ESC = ( 33 "\xA" => ' ', 34 "\xD" => ' ', 35 '"' => '"', 36 '&' => '&', 37 "'" => ''', 38 '<' => '<', 39 '>' => '>', 40); 41 42sub xml_cref { Encode::encode $XML_ENC[0], $_[0], Encode::HTMLCREF } 43 44sub xml_escape { 45 $_[0] =~ s{ ( [<>&'"] ) }{ $XML_ESC{ $1 } }gex; 46 &xml_cref; 47} 48 49sub xml_attr_escape { 50 $_[0] =~ s{ ( [\x0A\x0D<>&'"] ) }{ $XML_ESC{ $1 } }gex; 51 &xml_cref; 52} 53 54sub xml_cdata_flatten { 55 for ( $_[0] ) { 56 my $cdata_content; 57 s{<!\[CDATA\[(.*?)]]>}{ xml_escape $cdata_content = $1 }gse; 58 croak 'Incomplete CDATA section' if -1 < index $_, '<![CDATA['; 59 return $_; 60 } 61} 62 63sub xml_string { xml_cref xml_cdata_flatten $_[ 0 ] } 64 65sub xml_tag { 66 my $name = shift; 67 my $attr = ''; 68 if( ref $name eq 'ARRAY' ) { 69 my $i = 1; 70 while( $i < @$name ) { 71 $attr .= ' ' . $name->[ $i ] . '="' . xml_attr_escape( $name->[ $i + 1 ] ) . '"'; 72 $i += 2; 73 } 74 $name = $name->[ 0 ]; 75 } 76 @_ ? join( '', "<$name$attr>", @_, "</$name>" ) : "<$name$attr/>"; 77} 78 79#################################################################### 80# misc utility functions 81# 82 83sub natural_enum { 84 my @and; 85 unshift @and, pop @_ if @_; 86 unshift @and, join ', ', @_ if @_; 87 join ' and ', @and; 88} 89 90sub permalink { 91 my ( $link_arg ) = ( @_ ); 92 if( ref $link_arg ne 'HASH' ) { 93 return $link_arg; 94 } 95 elsif( not exists $link_arg->{ rel } or $link_arg->{ rel } eq 'alternate' ) { 96 return $link_arg->{ href }; 97 } 98 return; 99} 100 101#################################################################### 102# actual implementation of RFC 4287 103# 104 105sub simple_construct { 106 my ( $name, $content ) = @_; 107 xml_tag $name, xml_escape $content; 108} 109 110sub date_construct { 111 my ( $name, $dt ) = @_; 112 eval { $dt = $dt->epoch }; # convert to epoch to avoid dealing with everyone's TZ crap 113 $dt = POSIX::strftime( W3C_DATETIME . 'Z', gmtime $dt ) unless $dt =~ /[^0-9]/; 114 xml_tag $name, xml_escape $dt; 115} 116 117sub person_construct { 118 my ( $name, $arg ) = @_; 119 120 my $prop = 'HASH' ne ref $arg ? { name => $arg } : $arg; 121 122 croak "name required for $name element" if not exists $prop->{ name }; 123 124 return xml_tag $name => ( 125 map { xml_tag $_ => xml_escape $prop->{ $_ } } 126 grep { exists $prop->{ $_ } } 127 qw( name email uri ) 128 ); 129} 130 131sub text_construct { 132 my ( $name, $arg ) = @_; 133 134 my ( $type, $content ); 135 136 if( ref $arg eq 'HASH' ) { 137 # FIXME doesn't support @src attribute for $name eq 'content' yet 138 139 $type = exists $arg->{ type } ? $arg->{ type } : 'html'; 140 141 croak "content required for $name element" unless exists $arg->{ content }; 142 143 # a lof of the effort that follows is to omit the type attribute whenever possible 144 # 145 if( $type eq 'xhtml' ) { 146 $content = xml_string $arg->{ content }; 147 148 if( $content !~ /</ ) { # FIXME does this cover all cases correctly? 149 $type = 'text'; 150 $content =~ s/[\n\t]+/ /g; 151 } 152 else { 153 $content = xml_tag [ div => xmlns => XHTML_NS ], $content; 154 } 155 } 156 elsif( $type eq 'html' or $type eq 'text' ) { 157 $content = xml_escape $arg->{ content }; 158 } 159 else { 160 croak "type '$type' not allowed in $name element" 161 if $name ne 'content'; 162 163 # FIXME non-XML/text media types must be base64 encoded! 164 $content = xml_string $arg->{ content }; 165 } 166 } 167 else { 168 $type = 'html'; 169 $content = xml_escape $arg; 170 } 171 172 if( $type eq 'html' and $content !~ /&/ ) { 173 $type = 'text'; 174 $content =~ s/[\n\t]+/ /g; 175 } 176 177 return xml_tag [ $name => $type ne 'text' ? ( type => $type ) : () ], $content; 178} 179 180sub link_element { 181 my ( $name, $arg ) = @_; 182 183 # omit atom:link/@rel value when possible 184 delete $arg->{'rel'} 185 if 'HASH' eq ref $arg 186 and exists $arg->{'rel'} 187 and 'alternate' eq $arg->{'rel'}; 188 189 my @attr = 'HASH' eq ref $arg 190 ? do { 191 croak "href required for link element" if not exists $arg->{'href'}; 192 map { $_ => $arg->{ $_ } } grep exists $arg->{ $_ }, qw( href rel type title hreflang length ); 193 } 194 : ( href => $arg ); 195 196 # croak "link '$attr[1]' is not a valid URI" 197 # if $attr[1] XXX TODO 198 199 xml_tag [ link => @attr ]; 200} 201 202sub category_element { 203 my ( $name, $arg ) = @_; 204 205 my @attr = 'HASH' eq ref $arg 206 ? do { 207 croak "term required for category element" if not exists $arg->{'term'}; 208 map { $_ => $arg->{ $_ } } grep exists $arg->{ $_ }, qw( term scheme label ); 209 } 210 : ( term => $arg ); 211 212 xml_tag [ category => @attr ]; 213} 214 215sub generator_element { 216 my ( $name, $arg ) = @_; 217 if( ref $arg eq 'HASH' ) { 218 croak 'name required for generator element' if not exists $arg->{ name }; 219 my $content = delete $arg->{ name }; 220 xml_tag [ generator => map +( $_ => $arg->{ $_ } ), grep exists $arg->{ $_ }, qw( uri version ) ], xml_escape( $content ); 221 } 222 elsif( defined $arg ) { 223 xml_tag generator => xml_escape( $arg ); 224 } 225 else { '' } 226} 227 228# tag makers are called with the name of the tag they're supposed to handle as the first parameter 229my %make_tag = ( 230 icon => \&simple_construct, 231 id => \&simple_construct, 232 logo => \&simple_construct, 233 published => \&date_construct, 234 updated => \&date_construct, 235 author => \&person_construct, 236 contributor => \&person_construct, 237 title => \&text_construct, 238 subtitle => \&text_construct, 239 rights => \&text_construct, 240 summary => \&text_construct, 241 content => \&text_construct, 242 link => \&link_element, 243 category => \&category_element, 244 generator => \&generator_element, 245); 246 247sub container_content { 248 my ( $name, %arg ) = @_; 249 250 my ( $elements, $required, $optional, $singular, $deprecation, $callback ) = 251 @arg{ qw( elements required optional singular deprecate callback ) }; 252 253 my ( $content, %permission, %count, $permalink ); 254 255 undef @permission{ @$required, @$optional }; # populate 256 257 while( my ( $elem, $arg ) = splice @$elements, 0, 2 ) { 258 if( exists $permission{ $elem } ) { 259 $content .= $make_tag{ $elem }->( $elem, $arg ); 260 ++$count{ $elem }; 261 } 262 else { 263 croak "Unknown element $elem"; 264 } 265 266 if( $elem eq 'link' and defined ( my $alt = permalink $arg ) ) { 267 $permalink = $alt unless $count{ 'alternate link' }++; 268 } 269 270 if( exists $callback->{ $elem } ) { $callback->{ $elem }->( $arg ) } 271 272 if( not @$elements ) { # end of input? 273 # we would normally fall off the bottom of the loop now; 274 # before that happens, it's time to defaultify stuff and 275 # put it in the input so we will keep going for a little longer 276 if( not $count{ id } and defined $permalink ) { 277 carp 'Falling back to alternate link as id'; 278 push @$elements, id => $permalink; 279 } 280 if( not $count{ updated } ) { 281 push @$elements, updated => $arg{ default_upd }; 282 } 283 } 284 } 285 286 my @error; 287 288 my @missing = grep { not exists $count{ $_ } } @$required; 289 my @toomany = grep { ( $count{ $_ } || 0 ) > 1 } 'alternate link', @$singular; 290 291 push @error, 'requires at least one ' . natural_enum( @missing ) . ' element' if @missing; 292 push @error, 'must have no more than one ' . natural_enum( @toomany ) . ' element' if @toomany; 293 294 croak $name, ' ', join ' and ', @error if @error; 295 296 return $content; 297} 298 299#################################################################### 300# implementation of published interface and rest of RFC 4287 301# 302 303sub XML::Atom::SimpleFeed::new { 304 my $self = bless { xml_encoding => $XML_ENC[0] }, shift; 305 306 if ( my @i = grep { '-encoding' eq $_[$_] } grep { not $_ % 2 } 0 .. $#_ ) { 307 croak 'multiple encodings requested' if @i > 1; 308 ( undef, my $encoding ) = splice @_, $i[0], 2; 309 $self->{ xml_encoding } = $encoding; 310 } 311 312 @_ ? $self->feed( @_ ) : $self; 313} 314 315sub XML::Atom::SimpleFeed::feed { 316 my $self = shift; 317 318 my $have_generator; 319 320 local $XML_ENC[0] = $self->{ xml_encoding }; 321 $self->{ meta } = container_content feed => ( 322 elements => \@_, 323 required => [ qw( id title updated ) ], 324 optional => [ qw( author category contributor generator icon logo link rights subtitle ) ], 325 singular => [ qw( generator icon logo id rights subtitle title updated ) ], 326 callback => { 327 author => sub { $self->{ have_default_author } = 1 }, 328 updated => sub { $self->{ global_updated } = $_[ 0 ] }, 329 generator => sub { $have_generator = 1 }, 330 }, 331 default_upd => time, 332 ); 333 334 $self->{ meta } .= $make_tag{ generator }->( generator => DEFAULT_GENERATOR ) 335 unless $have_generator; 336 337 return $self; 338} 339 340sub XML::Atom::SimpleFeed::add_entry { 341 my $self = shift; 342 343 my @required = qw( id title updated ); 344 my @optional = qw( category content contributor link published rights summary ); 345 346 push @{ $self->{ have_default_author } ? \@optional : \@required }, 'author'; 347 348 # FIXME 349 # 350 # o atom:entry elements that contain no child atom:content element 351 # MUST contain at least one atom:link element with a rel attribute 352 # value of "alternate". 353 # 354 # o atom:entry elements MUST contain an atom:summary element in either 355 # of the following cases: 356 # * the atom:entry contains an atom:content that has a "src" 357 # attribute (and is thus empty). 358 # * the atom:entry contains content that is encoded in Base64; 359 # i.e., the "type" attribute of atom:content is a MIME media type 360 # [MIMEREG], but is not an XML media type [RFC3023], does not 361 # begin with "text/", and does not end with "/xml" or "+xml". 362 363 local $XML_ENC[0] = $self->{ xml_encoding }; 364 push @{ $self->{ entries } }, xml_tag entry => container_content entry => ( 365 elements => \@_, 366 required => \@required, 367 optional => \@optional, 368 singular => [ qw( content id published rights summary ) ], 369 default_upd => $self->{ global_updated }, 370 ); 371 372 return $self; 373} 374 375sub XML::Atom::SimpleFeed::as_string { 376 my $self = shift; 377 local $XML_ENC[0] = $self->{ xml_encoding }; 378 PREAMBLE . xml_tag [ feed => xmlns => ATOM_NS ], $self->{ meta }, @{ $self->{ entries } }; 379} 380 381sub XML::Atom::SimpleFeed::print { 382 my $self = shift; 383 my ( $handle ) = @_; 384 local $, = local $\ = ''; 385 defined $handle ? print $handle $self->as_string : print $self->as_string; 386} 387 388sub XML::Atom::SimpleFeed::save_file { croak q{no longer supported, use 'print' instead and pass in a filehandle} } 389 390!!'Funky and proud of it.'; 391 392__END__ 393 394=pod 395 396=encoding UTF-8 397 398=head1 NAME 399 400XML::Atom::SimpleFeed - No-fuss generation of Atom syndication feeds 401 402=head1 SYNOPSIS 403 404 use XML::Atom::SimpleFeed; 405 406 my $feed = XML::Atom::SimpleFeed->new( 407 title => 'Example Feed', 408 link => 'http://example.org/', 409 link => { rel => 'self', href => 'http://example.org/atom', }, 410 updated => '2003-12-13T18:30:02Z', 411 author => 'John Doe', 412 id => 'urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6', 413 ); 414 415 $feed->add_entry( 416 title => 'Atom-Powered Robots Run Amok', 417 link => 'http://example.org/2003/12/13/atom03', 418 id => 'urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a', 419 summary => 'Some text.', 420 updated => '2003-12-13T18:30:02Z', 421 category => 'Atom', 422 category => 'Miscellaneous', 423 ); 424 425 $feed->print; 426 427=head1 DESCRIPTION 428 429This is a minimal API for generating Atom syndication feeds quickly and easily. 430It supports all aspects of the Atom format itself but has no mechanism for the 431inclusion of extension elements. 432 433You can supply strings for most things, and the module will provide useful 434defaults. When you want more control, you can provide data structures, as 435documented, to specify more particulars. 436 437=head1 INTERFACE 438 439=head2 C<new> 440 441Takes a list of key-value pairs. 442 443Most keys are used to create corresponding L<"Atom elements"|/ATOM ELEMENTS>. 444To specify multiple instances of an element that may be given multiple times, 445pass multiple key-value pairs with the same key. 446 447Keys that start with a dash specify how the XML document will be generated. 448 449The following keys are supported: 450 451=over 452 453=item * C<-encoding> (I<omissible>, default C<us-ascii>) 454 455=item * L</C<id>> (I<omissible>) 456 457=item * L</C<link>> (I<omissible>, multiple) 458 459=item * L</C<title>> (B<required>) 460 461=item * L</C<author>> (optional, multiple) 462 463=item * L</C<category>> (optional, multiple) 464 465=item * L</C<contributor>> (optional, multiple) 466 467=item * L</C<generator>> (optional) 468 469=item * L</C<icon>> (optional) 470 471=item * L</C<logo>> (optional) 472 473=item * L</C<rights>> (optional) 474 475=item * L</C<subtitle>> (optional) 476 477=item * L</C<updated>> (optional) 478 479=back 480 481=head2 C<add_entry> 482 483Takes a list of key-value pairs, 484used to create corresponding L<"Atom elements"|/ATOM ELEMENTS>. 485To specify multiple instances of an element that may be given multiple times, 486pass multiple key-value pairs with the same key. 487 488The following keys are supported: 489 490=over 491 492=item * L</C<author>> (B<required> unless there is a feed-level author, multiple) 493 494=item * L</C<id>> (I<omissible>) 495 496=item * L</C<link>> (B<required>, multiple) 497 498=item * L</C<title>> (B<required>) 499 500=item * L</C<category>> (optional, multiple) 501 502=item * L</C<content>> (optional) 503 504=item * L</C<contributor>> (optional, multiple) 505 506=item * L</C<published>> (optional) 507 508=item * L</C<rights>> (optional) 509 510=item * L</C<summary>> (optional) 511 512=item * L</C<updated>> (optional) 513 514=back 515 516=head2 C<as_string> 517 518Returns the XML representation of the feed as a string. 519 520=head2 C<print> 521 522Outputs the XML representation of the feed to a handle which should be passed 523as a parameter. Defaults to C<STDOUT> if you do not pass a handle. 524 525=head1 ATOM ELEMENTS 526 527=head2 C<author> 528 529A L</Person Construct> denoting the author of the feed or entry. 530 531If you supply at least one author for the feed, you can omit this information 532from entries; the feed's author(s) will be assumed as the author(s) for those 533entries. If you do not supply any author for the feed, you B<must> supply one 534for each entry. 535 536=head2 C<category> 537 538One or more categories that apply to the feed or entry. You can supply a string 539which will be used as the category term. The full range of details that can be 540provided by passing a hash instead of a string is as follows: 541 542=over 543 544=item C<term> (B<required>) 545 546The category term. 547 548=item C<scheme> (optional) 549 550A URI that identifies a categorization scheme. 551 552It is common to provide the base of some kind of by-category URL here. F.ex., 553if the weblog C<http://www.example.com/blog/> can be browsed by category using 554URLs such as C<http://www.example.com/blog/category/personal>, you would supply 555C<http://www.example.com/blog/category/> as the scheme and, in that case, 556C<personal> as the term. 557 558=item C<label> (optional) 559 560A human-readable version of the term. 561 562=back 563 564=head2 C<content> 565 566The actual, honest-to-goodness, body of the entry. This is like a 567L</Text Construct>, with a couple of extras. 568 569In addition to the C<type> values of a L</Text Construct>, you can also supply 570any MIME Type (except multipart types, which the Atom format specification 571forbids). If you specify a C<text/*> type, the same rules apply as for C<text>. 572If you pass a C<*/xml> or C<*/*+xml> type, the same rules apply as for C<xhtml> 573(except in that case there is no wrapper C<< <div> >> element). Any other type 574will be transported as Base64-encoded binary. 575 576XXX Furthermore, you can supply a C<src> key in place of the C<content> key. In 577that case, the value of the C<src> key should be a URL denoting the actual 578location of the content. FIXME This is not currently supported. XXX 579 580=head2 C<contributor> 581 582A L</Person Construct> denoting a contributor to the feed or entry. 583 584=head2 C<generator> 585 586The software used to generate the feed. Can be supplied as a string 587or as a hash with C<uri>, C<version> and C<name> keys. Can also be undef to 588suppress the element entirely. If nothing is passed, defaults to reporting 589XML::Atom::SimpleFeed as the generator. 590 591=head2 C<icon> 592 593The URI of a small image whose width and height should be identical. 594 595=head2 C<id> 596 597A URI that is a permanent, globally unique identifier for the feed or entry 598that B<MUST NEVER CHANGE>. 599 600You are encouraged to generate a UUID using L<Data::UUID> for the purpose of 601identifying entries/feeds. It should be stored alongside the resource 602corresponding to the entry/feed, f.ex. in a column of the article table of your 603weblog database. To use it as an identifier in the entry/feed, use the 604C<urn:uuid:########-####-####-####-############> URI form. 605 606If you do not specify an ID, the permalink will be used instead. This is 607unwise, as permalinks do unfortunately occasionally change. 608B<It is your responsibility to ensure that the permalink NEVER CHANGES.> 609 610=head2 C<link> 611 612A link element. You can either supply a bare string as the parameter, which 613will be used as the permalink URI, or a hash. The permalink for a feed is 614generally a browser-viewable weblog, upload browser, search engine results page 615or similar web page; for an entry, it is generally a browser-viewable article, 616upload details page, search result or similar web page. This URI I<should> be 617unique. If you supply a hash, you can provide the following range of details in 618the given hash keys: 619 620=over 621 622=item C<rel> (optional) 623 624The link relationship. If omitted, defaults to C<alternate> (note that you can 625only have one alternate link per feed/entry). Other permissible values are 626C<related>, C<self>, C<enclosure> and C<via>, as well as any URI. 627 628=item C<href> (B<required> URL) 629 630Where the link points to. 631 632=item C<type> (optional) 633 634An advisory media type that provides a hint about the type of the resource 635pointed to by the link. 636 637=item C<hreflang> (optional) 638 639The language of the resource pointed to by the link, an an RFC3066 language tag. 640 641=item C<title> (optional) 642 643Human-readable information about the link. 644 645=item C<length> (optional) 646 647A hint about the content length in bytes of the resource pointed to by the link. 648 649=back 650 651=head2 C<logo> 652 653The URI of an image that should be twice as wide as it is high. 654 655=head2 C<published> 656 657A L</Date Construct> denoting the moment in time when the entry was first 658published. This should never change. 659 660=head2 C<rights> 661 662A L</Text Construct> containing a human-readable statement of legal rights for 663the content of the feed or entry. This is not intended for machine processing. 664 665=head2 C<subtitle> 666 667A L</Text Construct> containing an optional additional description of the feed. 668 669=head2 C<summary> 670 671A L</Text Construct> giving a short summary of the entry. 672 673=head2 C<title> 674 675A L</Text Construct> containing the title of the feed or entry. 676 677=head2 C<updated> 678 679A L</Date Construct> denoting the moment in time when the feed or entry was 680last updated. Defaults to the current date and time if omitted. 681 682In entries, you can use this element to signal I<significant> changes at your 683discretion. 684 685=head1 COMMON ATOM CONSTRUCTS 686 687A number of Atom elements share a common structure. The following sections 688outline the data you can (or must) pass in each case. 689 690=head2 Date Construct 691 692A string denoting a date and time in W3CDTF format. You can generate those 693using something like 694 695 use POSIX 'strftime'; 696 my $now = strftime '%Y-%m-%dT%H:%M:%SZ', gmtime; 697 698However, you can also simply pass a Unix timestamp (a positive integer) or an 699object that responds to an C<epoch> method call. (Make sure that the timezone 700reported by such objects is correct!) 701 702The following datetime classes from CPAN are compatible with this interface: 703 704=over 4 705 706=item * L<Time::Piece|Time::Piece> 707 708=item * L<DateTime|DateTime> 709 710=item * L<Time::Moment|Time::Moment> 711 712=item * L<Panda::Date|Panda::Date> 713 714=item * L<Class::Date|Class::Date> 715 716=item * L<Time::Object|Time::Object> (an obsolete precursor to L<Time::Piece|Time::Piece>) 717 718=item * L<Time::Date|Time::Date> (version 0.05 or newer) 719 720=back 721 722The following are not: 723 724=over 4 725 726=item * L<DateTime::Tiny|DateTime::Tiny> 727 728This class lacks both an C<epoch> method or any way to emulate one E<ndash> as 729well as any timezone support in the first place. 730That makes it unsuitable in principle for use in Atom feeds E<ndash> unless you 731have separate information about the timezone. 732 733=item * L<Date::Handler|Date::Handler> 734 735This class has a suitable methodE<hellip> but sadly, calls it C<Epoch>. 736So it is left up to you to call C<< $dh->Epoch >> to pass such values. 737 738=back 739 740=head2 Person Construct 741 742You can supply a string to Person Construct parameters, which will be used as 743the name of the person. The full range of details that can be provided by 744passing a hash instead of a string is as follows: 745 746=over 747 748=item C<name> (B<required>) 749 750The name of the person. 751 752=item C<email> (optional) 753 754The person's email address. 755 756=item C<uri> (optional) 757 758A URI to distinguish this person. This would usually be a homepage, but need 759not actually be a dereferencable URL. 760 761=back 762 763=head2 Text Construct 764 765You can supply a string to Text Construct parameters, which will be used as the 766HTML content of the element. 767 768FIXME details, text/html/xhtml 769 770=head1 SEE ALSO 771 772=over 773 774=item * Atom Enabled (L<http://www.atomenabled.org/>) 775 776=item * W3CDTF Spec (L<http://www.w3.org/TR/NOTE-datetime>) 777 778=item * RFC 3066 (L<http://rfc.net/rfc3066.html>) 779 780=item * L<XML::Atom::Syndication> 781 782=item * L<XML::Feed> 783 784=back 785 786=head1 BUGS AND LIMITATIONS 787 788In C<content> elements, the C<src> attribute cannot be used, and non-XML or 789non-text media types do not get Base64-encoded automatically. This is a bug. 790 791There are practically no tests. This is a bug. 792 793Support for C<xml:lang> and C<xml:base> is completely absent. This is a bug and 794should be partially addressed in a future version. There are however no plans 795to allow these attributes on arbitrary elements. 796 797There are no plans to ever support generating feeds with arbitrary extensions, 798although support for specific extensions may or may not be added in the future. 799 800The C<source> element is not and may never be supported. 801 802Nothing is done to ensure that text constructs with type C<xhtml> and entry 803contents using either that or an XML media type are well-formed. So far, this 804is by design. You should strongly consider using an XML writer if you want to 805include content with such types in your feed. 806 807=head1 AUTHOR 808 809Aristotle Pagaltzis <pagaltzis@gmx.de> 810 811=head1 COPYRIGHT AND LICENSE 812 813This software is copyright (c) 2020 by Aristotle Pagaltzis. 814 815This is free software; you can redistribute it and/or modify it under 816the same terms as the Perl 5 programming language system itself. 817 818=cut 819