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 |