package XML::Struct::Writer;
use strict;
use Moo;
use XML::LibXML::SAX::Builder;
use XML::Struct::Writer::Stream;
use Scalar::Util qw(blessed reftype);
use Carp;
our $VERSION = '0.27';
has attributes => (is => 'rw', default => sub { 1 });
has encoding => (is => 'rw', default => sub { 'UTF-8' });
has version => (is => 'rw', default => sub { '1.0' });
has standalone => (is => 'rw');
has pretty => (is => 'rw', default => sub { 0 }); # 0|1|2
has xmldecl => (is => 'rw', default => sub { 1 });
has handler => (is => 'lazy', builder => 1);
has to => (
is => 'rw',
coerce => sub {
if (!ref $_[0]) {
return IO::File->new($_[0], 'w');
} elsif (reftype($_[0]) eq 'SCALAR') {
open my $io,">:utf8",$_[0];
return $io;
} else { # IO::Handle, GLOB, ...
return $_[0];
}
},
trigger => sub { delete $_[0]->{handler} }
);
sub _build_handler {
$_[0]->to ? XML::Struct::Writer::Stream->new(
fh => $_[0]->to,
encoding => $_[0]->encoding,
version => $_[0]->version,
pretty => $_[0]->pretty,
) : XML::LibXML::SAX::Builder->new( handler => $_[0] );
}
sub write {
my ($self, $element, $name) = @_;
$self->writeStart;
$self->writeElement(
$self->microXML($element, $name // 'root')
);
$self->writeEnd;
$self->handler->can('result') ? $self->handler->result : 1;
}
*writeDocument = \&write;
# TODO: Make available as function in XML::Struct or XML::Struct::Simple
sub microXML {
my ($self, $element, $name) = @_;
my $type = reftype($element);
if ($type) {
# MicroXML
if ($type eq 'ARRAY') {
if (@$element == 1) {
return $element;
} elsif (@$element == 2) {
if ( (reftype($element->[1]) // '') eq 'ARRAY') {
return [ $element->[0], {}, $element->[1] ];
} elsif (!$self->attributes and %{$element->[1]}) {
return [ $element->[0] ];
} else {
return $element;
}
} else {
if (!$self->attributes and %{$element->[1]}) {
return [ $element->[0], {}, $element->[2] ];
} else {
return $element;
}
}
# SimpleXML
} elsif ($type eq 'HASH') {
my $children = [
map {
my ($tag, $content) = ($_, $element->{$_});
# text
if (!ref $content) {
[ $tag, {}, [$content] ]
} elsif (reftype($content) eq 'ARRAY') {
@$content
? map { [ $tag, {}, [$_] ] } @$content
: [ $tag ];
} elsif (reftype $content eq 'HASH' ) {
[ $tag, {}, [ $content ] ];
} else {
();
}
}
grep { defined $element->{$_} }
sort keys %$element
];
return $name ? [ $name, {}, $children ] : @$children;
}
}
croak "expected XML as ARRAY or HASH reference";
}
sub writeElement {
my $self = shift;
foreach my $element (@_) {
$self->writeStartElement($element);
foreach my $child ( @{ $element->[2] // [] } ) {
if (ref $child) {
$self->writeElement( $self->microXML($child) );
} else {
$self->writeCharacters($child);
}
}
$self->writeEndElement($element);
}
}
sub writeStartElement {
my ($self, $element) = @_;
my $args = { Name => $element->[0] };
$args->{Attributes} = $element->[1] if $element->[1];
$self->handler->start_element($args);
}
sub writeEndElement {
my ($self, $element) = @_;
$self->handler->end_element({ Name => $element->[0] });
}
sub writeCharacters {
$_[0]->handler->characters({ Data => $_[1] });
}
sub writeStart {
my $self = shift;
$self->handler->start_document;
if ($self->handler->can('xml_decl') && $self->xmldecl) {
$self->handler->xml_decl({
Version => $self->version,
Encoding => $self->encoding,
Standalone => $self->standalone,
});
}
$self->writeStartElement(@_) if @_;
}
sub writeEnd {
my $self = shift;
$self->writeEndElement(@_) if @_;
$self->handler->end_document;
}
1;
__END__
=encoding UTF-8
=head1 NAME
XML::Struct::Writer - Write XML data structures to XML streams
=head1 SYNOPSIS
use XML::Struct::Writer;
# serialize
XML::Struct::Writer->new(
to => \*STDOUT,
attributes => 0,
pretty => 1,
)->write( [
doc => [
[ name => [ "alice" ] ],
[ name => [ "bob" ] ],
]
] );
#
#
# alice
# bob
#
# create DOM
my $xml = XML::Struct::Writer->new->write( [
greet => { }, [
"Hello, ",
[ emph => { color => "blue" } , [ "World" ] ],
"!"
]
] );
$xml->toFile("greet.xml");
#
# Hello, World!
=head1 DESCRIPTION
This module writes an XML document, given as L data structure, as
stream of L"SAX EVENTS">. The default handler receives these events with
L to build a DOM tree which can then be used to
serialize the XML document as string. The writer can also be used to directly
serialize XML with L.
L provides the shortcut function C to this module.
XML elements can be passed in any of these forms and its combinations:
# MicroXML:
[ $name => \%attributes, \@children ]
[ $name => \%attributes ]
[ $name ]
# lax MicroXML also:
[ $name => \@children ]
# SimpleXML:
{ $name => \@children, $name => $content, ... }
=head1 CONFIGURATION
A XML::Struct::Writer can be configured with the following options:
=over
=item to
Filename, L, string reference, or other kind of stream to directly
serialize XML to with L. This option is ignored
if C is explicitly set.
=item handler
A SAX handler to send L"SAX EVENTS"> to. If neither this option nor C is
explicitly set, an instance of L is used to build a
DOM.
=item attributes
Ignore XML attributes if set to false. Set to true by default.
=item xmldecl
Include XML declaration on serialization. Enabled by default.
=item encoding
An encoding (for handlers that support an explicit encoding). Set to UTF-8
by default.
=item version
The XML version. Set to C<1.0> by default.
=item standalone
Add standalone flag in the XML declaration.
=item pretty
Pretty-print XML. Disabled by default.
=back
=head1 METHODS
=head2 write( $root [, $name ] ) == writeDocument( $root [, $name ] )
Write an XML document, given as array reference (lax MicroXML), hash reference
(SimpleXML), or both mixed. If given as hash reference, the name of a root tag
can be chosen or it is set to C. This method is basically equivalent to:
$writer->writeStart;
$writer->writeElement(
$writer->microXML($root, $name // 'root')
);
$writer->writeEnd;
$writer->result if $writer->can('result');
The remaining methods expect XML in MicroXML format only.
=head2 writeElement( $element [, @more_elements ] )
Write one or more XML elements and their child elements to the handler.
=head2 writeStart( [ $root [, $name ] ] )
Call the handler's C and C methods. An optional root
element can be passed, so C<< $writer->writeStart($root) >> is equivalent to:
$writer->writeStart;
$writer->writeStartElement($root);
=head2 writeStartElement( $element )
Directly call the handler's C method.
=head2 writeEndElement( $element )
Directly call the handler's C method.
=head2 writeCharacters( $string )
Directy call the handler's C method.
=head2 writeEnd( [ $root ] )
Directly call the handler's C method. An optional root element
can be passed, so C<< $writer->writeEnd($root) >> is equivalent to:
$writer->writeEndElement($root);
$writer->writeEnd;
=head2 microXML( $element [, $name ] )
Convert an XML element, given as array reference (lax MicroXML) or as hash
reference (SimpleXML) to a list of MicroXML elements and optionally remove
attributes. Does not affect child elements.
=head1 SAX EVENTS
A SAX handler, set with option C, is expected to implement the
following methods (two of them are optional):
=over
=item xml_decl( { Version => $version, Encoding => $encoding } )
Optionally called once at the start of an XML document, if the handler supports
this method.
=item start_document()
Called once at the start of an XML document.
=item start_element( { Name => $name, Attributes => \%attributes } )
Called at the start of an XML element to emit an XML start tag.
=item end_element( { Name => $name } )
Called at the end of an XML element to emit an XML end tag.
=item characters( { Data => $characters } )
Called for character data. Character entities and CDATA section are expanded to
strings.
=item end_document()
Called once at the end of an XML document.
=item result()
Optionally called at the end of C/C to return a value
from this methods. Handlers do not need to implement this method.
=back
=head1 SEE ALSO
Using a streaming SAX handler, such as L,
L, L, and possibly L
should be more performant for serialization. Examples of other modules that
receive SAX events include L, L, and
L,
=cut