File Coverage

blib/lib/TUWF/XML.pm
Criterion Covered Total %
statement 86 107 80.3
branch 26 54 48.1
condition 6 19 31.5
subroutine 15 21 71.4
pod 11 12 91.6
total 144 213 67.6


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3              
4             package TUWF::XML;
5              
6              
7 1     1   69627 use strict;
  1         8  
  1         29  
8 1     1   5 use warnings;
  1         2  
  1         25  
9 1     1   4 use Exporter 'import';
  1         2  
  1         40  
10 1     1   5 use Carp 'carp', 'croak';
  1         2  
  1         348  
11              
12              
13             our $VERSION = '1.5';
14             our(@EXPORT_OK, %EXPORT_TAGS, $OBJ);
15              
16             # List::Util provides a uniq() since 1.45, but for some reason my Perl comes
17             # with an even more ancient version.
18 1     1 0 362 sub uniq { my %h = map +($_,1), @_; keys %h }
  1         85  
19              
20              
21             BEGIN {
22 1     1   24 my @htmltags = qw|
23             a abbr acronym address area b base bdo big blockquote body br button caption
24             cite code col colgroup dd del dfn div dl dt em fieldset form h1 h2 h3 h4 h5 h6
25             head i img input ins kbd label legend li Link Map meta noscript object ol
26             optgroup option p param pre Q samp script Select small span strong style Sub
27             sup table tbody td textarea tfoot th thead title Tr tt ul var
28             |;
29 1         21 my @html5tags = qw|
30             a abbr address area article aside audio b base bb bdo blockquote body br
31             button canvas caption cite code col colgroup command datagrid datalist dd
32             del details dfn dialog div dl dt em embed fieldset figure footer form h1 h2
33             h3 h4 h5 h6 head header hr i iframe img input ins kbd label legend li Link
34             main Map mark meta meter nav noscript object ol optgroup option output p
35             param pre progress Q rp rt ruby samp script section Select small source
36             span strong style Sub summary sup table tbody td textarea tfoot th thead
37             Time title Tr ul var video
38             |;
39 1         34 my @Htmltags = map ucfirst, @htmltags;
40 1         41 my @Html5tags = map ucfirst, @html5tags;
41 1         42 my @html_tags = map lc($_).'_', @htmltags;
42 1         48 my @html5_tags = map lc($_).'_', @html5tags;
43 1         16 my @all = uniq @htmltags, @html5tags, @Htmltags, @Html5tags, @html_tags, @html5_tags;
44              
45             # boolean/empty/self-closing tags
46 1         13 my %htmlbool = map +($_,1), qw{
47             area base br col command embed hr img input link meta param source
48             };
49              
50             # create the subroutines to map to the html tags
51 1     1   7 no strict 'refs';
  1         2  
  1         284  
52 1         3 for my $e (@all) {
53 308         757 (my $le = lc $e) =~ s/_$//;
54 308         1174 *{__PACKAGE__."::$e"} = sub {
55 5 50   5   28 my $s = ref($_[0]) eq __PACKAGE__ ? shift : $OBJ;
56 5 100 66     25 $s->tag($le, @_, $htmlbool{$le} && $#_%2 ? undef : ());
57             }
58 308         1048 }
59              
60             # functions to export
61 1         45 @EXPORT_OK = (@all, qw(
62             xml mkclass xml_escape html_escape xml_string
63             tag html lit txt end
64             Tag Html Lit Txt End
65             tag_ html_ lit_ txt_ end_
66             ));
67 1         1669 %EXPORT_TAGS = (
68             html => [ @htmltags, qw(tag html lit txt end ) ],
69             html5 => [ @html5tags, qw(tag html lit txt end ) ],
70             Html => [ @Htmltags, qw(Tag Html Lit Txt End ) ],
71             Html5 => [ @Html5tags, qw(Tag Html Lit Txt End ) ],
72             html_ => [ @html_tags, qw(tag_ html_ lit_ txt_ end_) ],
73             html5_=> [ @html5_tags, qw(tag_ html_ lit_ txt_ end_) ],
74             xml => [ qw(xml tag lit txt end) ],
75             );
76             };
77              
78              
79             # the common (X)HTML doctypes, from http://www.w3.org/QA/2002/04/valid-dtd-list.html
80             my %doctypes = split /\r?\n/, <<__;
81             xhtml1-strict
82            
83             xhtml1-transitional
84            
85             xhtml1-frameset
86            
87             xhtml11
88            
89             xhtml-basic11
90            
91             xhtml-math-svg
92            
93             html5
94            
95             __
96              
97              
98             sub new {
99 1     1 1 5 my($pack, %o) = @_;
100 1   50 0   5 $o{write} ||= sub { print @_ };
  0         0  
101 1         7 my $self = bless {
102             %o,
103             nesting => 0,
104             stack => [],
105             }, $pack;
106 1 50       4 $OBJ = $self if $o{default};
107 1         3 return $self;
108             };
109              
110              
111             # Convenient function to generate a dynamic class attribute.
112             sub mkclass {
113 0     0 1 0 my %c = @_;
114 0         0 my $c = join ' ', grep $c{$_}, keys %c;
115 0 0       0 return $c ? (class => $c) : ();
116             }
117              
118              
119             # XML escape (not a method)
120             my %XML = qw/& & < < " "/;
121             sub xml_escape {
122 7     7 1 12 local $_ = $_[0];
123 7 50       12 if(!defined $_) {
124 0         0 carp "Attempting to XML-escape an undefined value";
125 0         0 return '';
126             }
127 7         27 s/([&<"])/$XML{$1}/g;
128 7         37 $_;
129             }
130              
131             # HTML escape, also does \n to
conversion
132             # (not a method)
133             sub html_escape {
134 0     0 1 0 local $_ = xml_escape shift;
135 0         0 s/\r?\n/
/g;
136 0         0 return $_;
137             }
138              
139             # Evaluate a function and return XML as a string
140             sub xml_string {
141 1     1 1 86 my $f = pop;
142 1         3 my $buf = '';
143 1     8   8 local $OBJ = TUWF::XML->new(@_, write => sub { $buf .= shift });
  8         24  
144 1         6 $f->();
145 1         13 $buf
146             }
147              
148              
149             # output literal data (not HTML escaped)
150             sub lit {
151 8 50   8 1 21 my $s = ref($_[0]) eq __PACKAGE__ ? shift : $OBJ;
152 8         15 $s->{write}->($_) for @_;
153             }
154              
155             *Lit = \&lit;
156             *lit_ = \&lit;
157              
158              
159             # output text (HTML escaped)
160             sub txt {
161 0 0   0 1 0 my $s = ref($_[0]) eq __PACKAGE__ ? shift : $OBJ;
162 0         0 $s->lit(xml_escape $_) for @_;
163             }
164              
165             *Txt = \&txt;
166             *txt_ = \&txt;
167              
168              
169             # Output any XML or HTML tag.
170             # Arguments Output
171             # 'tagname'
172             # 'tagname', id => "main"
173             # 'tagname', '' <bar>
174             # 'tagname', sub { .. } ..
175             # 'tagname', class => undef
176             # 'tagname', '+a' => 1, '+a' => 2
177             # 'tagname', id => 'main', '' <bar>
178             # 'tagname', id => 'main', sub { .. } ..
179             # 'tagname', id => 'main', undef
180             # 'tagname', undef
181             sub tag {
182 5 50   5 1 13 my $s = ref($_[0]) eq __PACKAGE__ ? shift : $OBJ;
183 5         7 my $name = shift;
184 5 50 33     32 croak "Invalid XML tag name" if !$name || $name =~ /^[^a-z]/i || $name =~ / /;
      33        
185              
186 5 50       23 my $indent = $s->{pretty} ? "\n".(' 'x($s->{nesting}*$s->{pretty})) : '';
187 5         9 my $t = $indent.'<'.$name;
188 5         5 my %concat;
189 5         13 while(@_ > 1) {
190 8         12 my $attr = shift;
191 8         12 my $val = shift;
192 8 100       14 next if !defined $val;
193 6 50       14 croak "Invalid XML attribute name" if $attr =~ /[\s'"&<>=]/; # Not comprehensive, just enough to prevent XSS-by-fucking-up-XML-structure
194 6 100       13 if($attr =~ /^\+(.+)/) {
195 2 100       11 $concat{$1} .= (length $concat{$1} ? ' ' : '') . $val;
196             } else {
197 4         10 $t .= qq{ $attr="}.xml_escape($val).'"';
198             }
199             }
200 5         14 $t .= qq{ $_="}.xml_escape($concat{$_}).'"' for sort keys %concat;
201              
202 5 100       17 if(!@_) {
    100          
    100          
203 1         10 $s->lit($t.'>');
204 1         2 push @{$s->{stack}}, $name;
  1         3  
205 1         3 $s->{nesting}++;
206             } elsif(!defined $_[0]) {
207 1         3 $s->lit($t.' />');
208             } elsif(ref $_[0] eq 'CODE') {
209 1         4 $s->lit($t.'>');
210 1         3 local $s->{nesting} = $s->{nesting}+1;
211 1         3 local $s->{stack} = []; # Call the sub with an empty stack, there's nothing to end() now.
212 1         4 $_[0]->();
213 1         3 $s->lit($indent.'');
214             } else {
215 2         4 $s->lit($t.'>'.xml_escape(shift).'');
216             }
217             }
218              
219             *Tag = \&tag;
220             *tag_ = \&tag;
221              
222              
223             # Ends the last opened tag
224             sub end {
225 1 50   1 1 6 my $s = ref($_[0]) eq __PACKAGE__ ? shift : $OBJ;
226 1         2 my $w = shift;
227 1         2 my $l = pop @{$s->{stack}};
  1         3  
228 1         2 $s->{nesting}--;
229 1 50       2 croak "No more tags to close" if !$l;
230 1 50 33     4 croak "Specified tag to end ($w) is not equal to the last opened tag ($l)" if $w && $w ne $l;
231 1 50       5 $s->lit("\n".(' 'x($s->{nesting}*$s->{pretty}))) if $s->{pretty};
232 1         5 $s->lit('');
233             }
234              
235             *End = \&end;
236             *end_ = \&end;
237              
238              
239             sub html {
240 0 0   0 1   my $s = ref($_[0]) eq __PACKAGE__ ? shift : $OBJ;
241 0           my $hascontent = @_ % 2 == 1;
242 0   0       my $c = $hascontent && pop;
243 0           my %o = @_;
244              
245 0   0       my $doctype = delete $o{doctype} || 'html5';
246              
247 0           $s->lit($doctypes{$doctype}."\n");
248 0           my $lang = delete $o{lang};
249 0 0         $s->tag('html',
    0          
    0          
    0          
250             # html5 has no 'xmlns' or 'xml:lang'
251             $doctype eq 'html5' ? (
252             $lang ? (lang => $lang) : (),
253             ) : (
254             xmlns => 'http://www.w3.org/1999/xhtml',
255             $lang ? ('xml:lang' => $lang, lang => $lang) : (),
256             ),
257             %o,
258             $hascontent ? ($c) : ()
259             );
260             }
261              
262             *Html = \&html;
263             *html_ = \&html;
264              
265              
266             # Writes an xml header, doesn't open an tag, and doesn't need an
267             # end() either.
268             sub xml() {
269 0 0   0 1   my $s = ref($_[0]) eq __PACKAGE__ ? shift : $OBJ;
270 0           $s->lit(qq||);
271             }
272              
273              
274             1;
275