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" => '&#10;',
34	"\xD" => '&#13;',
35	'"'   => '&#34;',
36	'&'   => '&#38;',
37	"'"   => '&#39;',
38	'<'   => '&lt;',
39	'>'   => '&gt;',
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