File Coverage

blib/lib/XML/Builder.pm
Criterion Covered Total %
statement 262 290 90.3
branch 56 78 71.7
condition 25 50 50.0
subroutine 70 80 87.5
pod 0 32 0.0
total 413 530 77.9


line stmt bran cond sub pod time code
1 7     7   3278 use 5.008001; use strict; use warnings;
  7     7   51  
  7     7   29  
  7         10  
  7         109  
  7         43  
  7         11  
  7         275  
2              
3 7     7   49 use Scalar::Util ();
  7         24  
  7         107  
4 7     7   3573 use Encode ();
  7         66187  
  7         297  
5              
6             package XML::Builder;
7              
8             our $VERSION = '0.907';
9              
10 7     7   2496 use Object::Tiny::Lvalue qw( nsmap default_ns encoding );
  7         2120  
  7         29  
11              
12             # these aren't constants, they need to be overridable in subclasses
13             my %class = (
14             ns => 'XML::Builder::NS',
15             fragment => 'XML::Builder::Fragment',
16             qname => 'XML::Builder::Fragment::QName',
17             tag => 'XML::Builder::Fragment::Tag',
18             unsafe => 'XML::Builder::Fragment::Unsafe',
19             root => 'XML::Builder::Fragment::Root',
20             document => 'XML::Builder::Fragment::Document',
21             );
22              
23             my ( $name, $class );
24 1     1 0 7 eval XML::Builder::Util::factory_method( $name, $class )
  112     112 0 300  
  1     1 0 6  
  112     112 0 243  
  10     10 0 43  
  51     51 0 952  
  0     0 0 0  
  57     57 0 151  
  1     1 0 6  
  10     10 0 55  
  51     51 0 150  
  3     3 0 68  
  58     58 0 200  
  1     1 0 9  
25             while ( $name, $class ) = each %class;
26              
27             sub new {
28 8     8 0 536 my $class = shift;
29 8         26 my $self = bless { @_ }, $class;
30 8   50     144 $self->encoding ||= 'us-ascii';
31 8   50     223 $self->nsmap ||= {};
32 8         72 return $self;
33             }
34              
35             sub register_ns {
36 43     43 0 54 my $self = shift;
37 43         65 my ( $uri, $pfx ) = @_;
38              
39 43         645 my $nsmap = $self->nsmap;
40              
41 43         179 $uri = $self->stringify( $uri );
42              
43 43 100       97 if ( exists $nsmap->{ $uri } ) {
44 32         45 my $ns = $nsmap->{ $uri };
45 32         478 my $registered_pfx = $ns->prefix;
46              
47 32 50 33     128 XML::Builder::Util::croak( "Namespace '$uri' being bound to '$pfx' is already bound to '$registered_pfx'" )
48             if defined $pfx and $pfx ne $registered_pfx;
49              
50 32         83 return $ns;
51             }
52              
53 11 100       31 if ( not defined $pfx ) {
54 4         13 my %pfxmap = map {; $_->prefix => $_ } values %$nsmap;
  3         45  
55              
56 4 100 66     51 if ( $uri eq '' and not exists $pfxmap{ '' } ) {
57 1         7 return $self->register_ns( '', '' );
58             }
59              
60 3         11 my $counter;
61 3 50       11 my $letter = ( $uri =~ m!([[:alpha:]])[^/]*/?\z! ) ? lc $1 : 'ns';
62 3         11 do { $pfx = $letter . ++$counter } while exists $pfxmap{ $pfx };
  3         15  
63             }
64              
65             # FIXME needs proper validity check per XML TR
66 10 50 66     44 XML::Builder::Util::croak( "Invalid namespace prefix '$pfx'" )
67             if length $pfx and $pfx !~ /[\w-]/;
68              
69 10         172 my $ns = $self->new_ns(
70             uri => $uri,
71             prefix => $pfx,
72             );
73              
74 10 100       128 $self->default_ns = $uri if '' eq $pfx;
75 10         103 return $nsmap->{ $uri } = $ns;
76             }
77              
78             sub get_namespaces {
79 3     3 0 48 my $self = shift;
80 3         6 return values %{ $self->nsmap };
  3         40  
81             }
82              
83 7     7 0 45 sub ns { shift->register_ns( @_ )->factory }
84 3     3 0 674 sub null_ns { shift->ns( '', '' ) }
85              
86             sub qname {
87 35     35 0 44 my $self = shift;
88 35         40 my $ns_uri = shift;
89 35         66 return $self->register_ns( $ns_uri )->qname( @_ );
90             }
91              
92             sub parse_qname {
93 34     34 0 50 my $self = shift;
94 34         57 my ( $name ) = @_;
95              
96 34         50 my $ns_uri = '';
97 34 50       74 $ns_uri = $1 if $name =~ s/\A\{([^}]+)\}//;
98              
99 34         63 return $self->qname( $ns_uri, $name );
100             }
101              
102             sub root {
103 2     2 0 7 my $self = shift;
104 2         5 my ( $tag ) = @_;
105 2         6 return $tag->root;
106             }
107              
108             sub document {
109 1     1 0 5 my $self = shift;
110 1         29 return $self->new_document( content => [ @_ ] );
111             }
112              
113             sub unsafe {
114 1     1 0 3 my $self = shift;
115 1         2 my ( $string ) = @_;
116 1         25 return $self->new_unsafe( content => $string );
117             }
118              
119             sub comment {
120 0     0 0 0 my $self = shift;
121 0         0 my ( $comment ) = $self->stringify( @_ );
122 0 0       0 XML::Builder::Util::croak( "Comment contains double dashes '$1...'" )
123             if $comment =~ /(.*?--)/;
124 0         0 return $self->new_unsafe( "" );
125             }
126              
127             sub pi {
128 0     0 0 0 my $self = shift;
129 0         0 my ( $name, $content ) = map $self->stringify( $_ ), @_;
130 0 0       0 XML::Builder::Util::croak( "PI contains terminator '$1...'" )
131             if $content =~ /(.*\?>)/;
132 0         0 return $self->new_unsafe( "" );
133             }
134              
135             sub render {
136 106     106 0 174 my $self = shift;
137             return 'SCALAR' eq ref $_[0]
138 106 100       1594 ? $self->qname( ${$_[0]}, @_[ 1 .. $#_ ] )
  1         7  
139             : $self->new_fragment( content => [ @_ ] );
140             }
141              
142             sub test_fragment {
143 157     157 0 180 my $self = shift;
144 157         187 my ( $obj ) = @_;
145 157         480 return $obj->isa( 'XML::Builder::Fragment::Role' );
146             }
147              
148             {
149 7     7   8593 no warnings 'qw';
  7         15  
  7         2034  
150              
151             my %XML_NCR = map eval "qq[$_]", qw(
152             \xA \xD
153             & & < < > >
154             " " ' '
155             );
156              
157             my %type = (
158             encode => undef,
159             escape_text => qr/([<>&'"])/,
160             escape_attr => qr/([<>&'"\x0A\x0D])/,
161             );
162              
163             # using eval instead of closures to avoid __ANON__
164             while ( my ( $subname, $specials_rx ) = each %type ) {
165             my $esc = '';
166              
167             $esc = sprintf '$str =~ s{ %s }{ $XML_NCR{$1} }gex', $specials_rx
168             if defined $specials_rx;
169              
170 1     1 0 20 eval sprintf 'sub %s {
  1     35 0 4  
  1     65 0 16  
  35         1965  
  35         86  
  35         84  
  7         21  
  35         473  
  65         128  
  65         131  
  65         160  
  13         46  
  65         892  
171             my $self = shift;
172             my $str = $self->stringify( shift );
173             %s;
174             return Encode::encode $self->encoding, $str, Encode::HTMLCREF;
175             }', $subname, $esc;
176             }
177             }
178              
179             sub stringify {
180 149     149 0 1791 my $self = shift;
181 149         226 my ( $thing ) = @_;
182              
183 149 50       249 return if not defined $thing;
184              
185 149 100       1578 return $thing if not Scalar::Util::blessed $thing;
186              
187 4   100     32 my $conv = $thing->can( 'as_string' ) || overload::Method( $thing, '""' );
188 4 100       91 return $conv->( $thing ) if $conv;
189              
190 1         3 XML::Builder::Util::croak( 'Unstringifiable object ', $thing );
191             }
192              
193             #######################################################################
194              
195             package XML::Builder::NS;
196              
197             our $VERSION = '0.907';
198              
199 7     7   65 use Object::Tiny::Lvalue qw( builder uri prefix qname_for_localname );
  7         28  
  7         43  
200 7     7   11199 use overload '""' => 'uri', fallback => 1;
  7         15212  
  7         45  
201              
202             sub new {
203 10     10   54 my $class = shift;
204 10         62 my $self = bless { @_ }, $class;
205 10   50     166 $self->qname_for_localname ||= {};
206 10         374 Scalar::Util::weaken $self->builder;
207 10         64 return $self;
208             }
209              
210             sub qname {
211 87     87   121 my $self = shift;
212 87         103 my $name = shift;
213              
214 87   33     1259 my $builder = $self->builder
215             || XML::Builder::Util::croak( 'XML::Builder for this NS object has gone out of scope' );
216              
217             my $qname
218 87   66     1392 = $self->qname_for_localname->{ $name }
219             ||= $builder->new_qname( name => $name, ns => $self );
220              
221 87 100       1515 return @_ ? $qname->tag( @_ ) : $qname;
222             }
223              
224             sub xmlns {
225 3     3   20 my $self = shift;
226 3         41 my $pfx = $self->prefix;
227 3 50       79 return ( ( '' ne $pfx ? "xmlns:$pfx" : 'xmlns' ), $self->uri );
228             }
229              
230 7     7   37 sub factory { bless \shift, 'XML::Builder::NS::QNameFactory' }
231              
232             #######################################################################
233              
234             package XML::Builder::NS::QNameFactory;
235              
236             our $VERSION = '0.907';
237              
238 51     51   850 sub AUTOLOAD { my $self = shift; $$self->qname( ( our $AUTOLOAD =~ /.*::(.*)/ ), @_ ) }
  51         277  
239 1     1   2 sub _qname { my $self = shift; $$self->qname( @_ ) }
  1         6  
240       0     sub DESTROY {}
241              
242             #######################################################################
243              
244             package XML::Builder::Fragment::Role;
245              
246             our $VERSION = '0.907';
247              
248 0     0   0 sub depends_ns_scope { 1 }
249              
250             #######################################################################
251              
252             package XML::Builder::Fragment;
253              
254             our $VERSION = '0.907';
255              
256             our @ISA = 'XML::Builder::Fragment::Role';
257              
258 7     7   2849 use Object::Tiny::Lvalue qw( builder content );
  7         14  
  7         50  
259              
260 0     0   0 sub depends_ns_scope { 0 }
261              
262             sub new {
263 170     170   222 my $class = shift;
264 170         427 my $self = bless { @_ }, $class;
265 170         2369 my $builder = $self->builder;
266 170         2703 my $content = $self->content;
267              
268 170         482 my ( @gather, @take );
269              
270 170 100       382 for my $r ( 'ARRAY' eq ref $content ? @$content : $content ) {
271 223         331 @take = $r;
272              
273 223 100       490 if ( not Scalar::Util::blessed $r ) {
274 66 100       158 @take = $builder->render( @$r ) if 'ARRAY' eq ref $r;
275 66         88 next;
276             }
277              
278 157 50       257 if ( not $builder->test_fragment( $r ) ) {
279 0         0 @take = $builder->stringify( $r );
280 0         0 next;
281             }
282              
283 157 100       2198 next if $builder == $r->builder;
284              
285 1 50       71 XML::Builder::Util::croak( 'Cannot merge XML::Builder fragments built with different namespace maps' )
286             if $r->depends_ns_scope;
287              
288 1         5 @take = $r->flatten;
289              
290 1         21 my ( $self_enc, $r_enc ) = map { lc $_->encoding } $builder, $r->builder;
  2         35  
291             next
292             if $self_enc eq $r_enc
293             # be more permissive: ASCII is one-way compatible with UTF-8 and Latin-1
294 1 50 0     10 or 'us-ascii' eq $r_enc and grep { $_ eq $self_enc } 'utf-8', 'iso-8859-1';
  0   33     0  
295              
296 0         0 XML::Builder::Util::croak(
297             'Cannot merge XML::Builder fragments with incompatible encodings'
298             . " (have $self_enc, fragment has $r_enc)"
299             );
300             }
301             continue {
302 223         814 push @gather, @take;
303             }
304              
305 170         2394 $self->content = \@gather;
306              
307 170         1671 return $self;
308             }
309              
310             sub as_string {
311 166     166   418 my $self = shift;
312 166         2648 my $builder = $self->builder;
313 166 100       486 return join '', map { ref $_ ? $_->as_string : $builder->escape_text( $_ ) } @{ $self->content };
  223         2590  
  166         2099  
314             }
315              
316             sub flatten {
317 0     0   0 my $self = shift;
318 0         0 return @{ $self->content };
  0         0  
319             }
320              
321             #######################################################################
322              
323             package XML::Builder::Fragment::Unsafe;
324              
325             our $VERSION = '0.907';
326              
327             our @ISA = 'XML::Builder::Fragment';
328              
329 0     0   0 sub depends_ns_scope { 0 }
330              
331             sub new {
332 1     1   2 my $class = shift;
333 1         5 my $self = bless { @_ }, $class;
334 1         19 $self->content = $self->builder->stringify( $self->content );
335 1         9 return $self;
336             }
337              
338             sub as_string {
339 1     1   3 my $self = shift;
340 1         14 return $self->builder->encode( $self->content );
341             }
342              
343 0     0   0 sub flatten { shift }
344              
345             #######################################################################
346              
347             package XML::Builder::Fragment::QName;
348              
349             our $VERSION = '0.907';
350              
351 7     7   4945 use Object::Tiny::Lvalue qw( builder ns name as_qname as_attr_qname as_clarkname as_string );
  7         13  
  7         27  
352              
353             our @ISA = 'XML::Builder::Fragment';
354 7     7   2134 use overload '""' => 'as_clarkname', fallback => 1;
  7         21  
  7         45  
355              
356             sub new {
357 51     51   72 my $class = shift;
358 51         158 my $self = bless { @_ }, $class;
359              
360 51         724 my $uri = $self->ns->uri;
361 51         1635 my $pfx = $self->ns->prefix;
362 51         1413 Scalar::Util::weaken $self->ns; # really don't even need this any more
363 51         790 Scalar::Util::weaken $self->builder;
364              
365             # NB.: attributes without a prefix not in a namespace rather than in the
366             # default namespace, so attributes without a namespace never need a prefix
367              
368 51         812 my $name = $self->name;
369 51 100       862 $self->as_qname = ( '' eq $pfx ) ? $name : "$pfx:$name";
370 51 50 66     883 $self->as_attr_qname = ( '' eq $pfx or '' eq $uri ) ? $name : "$pfx:$name";
371 51 100       865 $self->as_clarkname = ( '' eq $uri ) ? $name : "{$uri}$name";
372 51         794 $self->as_string = '<' . $self->as_qname . '/>';
373              
374 51         956 return $self;
375             }
376              
377             sub tag {
378 40     40   56 my $self = shift;
379              
380 40 50 33     97 if ( 'SCALAR' eq ref $_[0] and 'foreach' eq ${$_[0]} ) {
  0         0  
381 0         0 shift @_; # throw away
382 0         0 return $self->foreach( @_ );
383             }
384              
385             # has to be written this way so it'll drop undef attributes
386 40         51 my $attr = {};
387 40         102 XML::Builder::Util::merge_param_hash( $attr, \@_ );
388              
389 40   33     602 my $builder = $self->builder
390             || XML::Builder::Util::croak( 'XML::Builder for this QName object has gone out of scope' );
391              
392 40         287 return $builder->new_tag(
393             qname => $self,
394             attr => $attr,
395             content => [ map $builder->render( $_ ), @_ ],
396             );
397             }
398              
399             sub foreach {
400 8     8   23 my $self = shift;
401              
402 8         14 my $attr = {};
403 8         12 my @out = ();
404              
405 8   33     118 my $builder = $self->builder
406             || XML::Builder::Util::croak( 'XML::Builder for this QName object has gone out of scope' );
407              
408 8         39 do {
409 17         47 XML::Builder::Util::merge_param_hash( $attr, \@_ );
410 17 50       42 my $content = 'HASH' eq ref $_[0] ? undef : shift;
411 17         45 push @out, $builder->new_tag(
412             qname => $self,
413             attr => {%$attr},
414             content => $builder->render( $content ),
415             );
416             } while @_;
417              
418 8 100 66     116 return $builder->new_fragment( content => \@out )
419             if @out > 1 and not wantarray;
420              
421 3         30 return @out[ 0 .. $#out ];
422             }
423              
424             #######################################################################
425              
426             package XML::Builder::Fragment::Tag;
427              
428             our $VERSION = '0.907';
429              
430             our @ISA = 'XML::Builder::Fragment';
431 7     7   3873 use Object::Tiny::Lvalue qw( qname attr );
  7         14  
  7         22  
432              
433 0     0   0 sub depends_ns_scope { 1 }
434              
435             sub as_string {
436 57     57   89 my $self = shift;
437              
438 57         798 my $builder = $self->builder;
439 57         859 my $qname = $self->qname->as_qname;
440 57   50     1607 my $attr = $self->attr || {};
441              
442             my $tag = join ' ', $qname,
443 57         338 map { sprintf '%s="%s"', $builder->parse_qname( $_ )->as_attr_qname, $builder->escape_attr( $attr->{ $_ } ) }
  34         590  
444             sort keys %$attr;
445              
446 57 100       2456 my $content = @{ $self->content } ? $self->SUPER::as_string : undef;
  57         827  
447 57 100       2921 return defined $content
448             ? "<$tag>$content"
449             : "<$tag/>";
450             }
451              
452             sub append {
453 2     2   10 my $self = shift;
454 2         40 return $self->builder->new_fragment( content => [ $self, $self->builder->render( @_ ) ] );
455             }
456              
457             sub root {
458 3     3   4 my $self = shift;
459 3         71 bless $self, $self->builder->root_class;
460             }
461              
462 1     1   3 sub flatten { shift }
463              
464             #######################################################################
465              
466             package XML::Builder::Fragment::Root;
467              
468             our $VERSION = '0.907';
469              
470             our @ISA = 'XML::Builder::Fragment::Tag';
471 7     7   3418 use overload '""' => 'as_string', fallback => 1;
  7         13  
  7         36  
472              
473 1     1   3 sub depends_ns_scope { 0 }
474              
475             sub as_string {
476 3     3   6 my $self = shift;
477              
478 3         59 my %decl = map $_->xmlns, $self->builder->get_namespaces;
479              
480             # make sure to always declare the default NS (if not bound to a URI, by
481             # explicitly undefining it) to allow embedding the XML easily without
482             # having to parse the fragment
483 3 50       36 $decl{'xmlns'} = '' if not defined $decl{'xmlns'};
484              
485 3         12 local @{ $self->attr }{ keys %decl } = values %decl;
  3         53  
486              
487 3         36 return $self->SUPER::as_string( @_ );
488             }
489              
490             #######################################################################
491              
492             package XML::Builder::Fragment::Document;
493              
494             our $VERSION = '0.907';
495              
496             our @ISA = 'XML::Builder::Fragment';
497 7     7   1528 use overload '""' => 'as_string', fallback => 1;
  7         12  
  7         25  
498              
499             sub new {
500 1     1   2 my $class = shift;
501 1         13 my $self = $class->SUPER::new( @_ );
502 1         6 $self->validate;
503 1         2 return $self;
504             }
505              
506             sub validate {
507 1     1   2 my $self = shift;
508 1         2 my @root;
509              
510 1         2 for ( @{ $self->content } ) {
  1         23  
511 1 50       11 if ( Scalar::Util::blessed $_ ) {
512 1 50       16 if ( $_->isa( $self->builder->tag_class ) ) { push @root, $_; next }
  1         3  
  1         2  
513 0 0       0 if ( $_->isa( $self->builder->unsafe_class ) ) { next }
  0         0  
514             }
515 0         0 XML::Builder::Util::croak( 'Junk at top level of document' );
516             }
517              
518 1 50       17 XML::Builder::Util::croak( 'Document must have exactly one document element, not ' . @root )
519             if @root != 1;
520              
521 1         5 $root[0]->root;
522              
523 1         3 return;
524             }
525              
526             sub as_string {
527 1     1   305 my $self = shift;
528 1         3 my $preamble = qq(builder->encoding}"?>\n);
  1         16  
529 1         29 return $preamble . $self->SUPER::as_string( @_ );
530             }
531              
532             #######################################################################
533              
534 0         0 BEGIN {
535             package XML::Builder::Util;
536              
537 7     7   2039 our $VERSION = '0.907';
538              
539 7     7   5942 use Carp::Clan '^XML::Builder(?:\z|::)';
  7         10761  
  7         49  
540              
541             sub merge_param_hash {
542 57     57   87 my ( $cur, $param ) = @_;
543              
544 57 100 66     214 return if not ( @$param and 'HASH' eq ref $param->[0] );
545              
546 17         31 my $new = shift @$param;
547              
548 17         59 @{ $cur }{ keys %$new } = values %$new;
  17         39  
549 17         61 while ( my ( $k, $v ) = each %$cur ) {
550 33 100       106 delete $cur->{ $k } if not defined $v;
551             }
552             }
553              
554             sub factory_method {
555 49     49   122 my ( $name, $class ) = @_;
556 49         1451 my ( $class_method, $new_method ) = ( "$name\_class", "new_$name" );
557 49         2963 return <<";";
558             sub $class_method { "\Q$class\E" }
559             sub $new_method { \$_[0]->$class_method->new( builder => \@_ ) }
560             ;
561             }
562             }
563              
564             1;
565              
566             __END__