| 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', ' |
||||||
| 174 | # 'tagname', sub { .. } |
||||||
| 175 | # 'tagname', class => undef |
||||||
| 176 | # 'tagname', '+a' => 1, '+a' => 2 |
||||||
| 177 | # 'tagname', id => 'main', ' |
||||||
| 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.''.$name.'>'); | ||||
| 214 | } else { | ||||||
| 215 | 2 | 4 | $s->lit($t.'>'.xml_escape(shift).''.$name.'>'); | ||||
| 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(''.$l.'>'); | ||||
| 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 |
||||||
| 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 |