File Coverage

blib/lib/HTML/HTML5/Builder.pm
Criterion Covered Total %
statement 30 32 93.7
branch n/a
condition n/a
subroutine 12 12 100.0
pod n/a
total 42 44 95.4


line stmt bran cond sub pod time code
1             package HTML::HTML5::Builder;
2              
3 2     2   36162 use 5.010;
  2         7  
  2         86  
4 2     2   13 use base qw[Exporter];
  2         3  
  2         250  
5 2     2   11996 use common::sense;
  2         29  
  2         12  
6 2     2   147 use constant { FALSE => 0, TRUE => 1 };
  2         4  
  2         369  
7 2     2   11 use constant XHTML_NS => 'http://www.w3.org/1999/xhtml';
  2         2  
  2         71  
8 2     2   7948 use overload;
  2         3923  
  2         14  
9 2     2   4185 use utf8;
  2         54  
  2         9  
10 2     2   385 use warnings::register;
  2         4  
  2         287  
11              
12             BEGIN {
13 2     2   58 $HTML::HTML5::Builder::AUTHORITY = 'cpan:TOBYINK';
14             }
15             BEGIN {
16 2     2   457 $HTML::HTML5::Builder::VERSION = '0.004';
17             }
18              
19 2     2   12 use Carp 0 qw();
  2         132  
  2         45  
20 2     2   2059 use HTML::HTML5::Builder::Document;
  0            
  0            
21             use HTML::HTML5::Entities 0.001 qw();
22             use Scalar::Util 0 qw(blessed);
23             use XML::LibXML 1.60 qw();
24              
25             my (@elements, @uc_elements, @conforming);
26             our (@EXPORT_OK, @EXPORT, %EXPORT_TAGS);
27             BEGIN
28             {
29             @elements = qw{
30             a abbr acronym address applet area article aside audio b base
31             basefont bb bdo bgsound big blink blockquote body br button canvas
32             caption center cite code col colgroup command datagrid datalist
33             dd del details dfn dialog dir div dl dt em embed fieldset figure
34             figcaption font footer form frame frameset h1 h2 h3 h4 h5 h6
35             head header hgroup hr html i iframe img input ins isindex kbd
36             keygen label legend li link listing map mark marquee menu meta
37             meter nav nobr noembed noframes noscript object ol optgroup
38             option output p param plaintext pre progress q rp rt ruby s
39             samp script select section small source spacer span strike
40             strong style sub sup summary table tbody td textarea tfoot th
41             thead time title tr track tt u ul var video wbr xmp
42             };
43             @uc_elements = qw{Q Sub Time Map Meta Link S};
44             @conforming = qw{
45             a abbr address area article aside audio b base bb bdo blockquote
46             body br button canvas caption cite code col colgroup command
47             datagrid datalist dd del details dfn dialog div dl dt em embed
48             fieldset figure footer form h1 h2 h3 h4 h5 h6 head header hr html
49             i iframe img input ins kbd label legend li mark menu
50             meter nav noscript object ol optgroup option output p param
51             pre progress rp rt ruby samp script section select small source
52             span strong style sup table tbody td textarea tfoot th thead
53             title tr ul var video
54             };
55             my @cool_stuff = qw{COMMENT CHUNK XML_CHUNK RAW_CHUNK ELEMENT TEXT};
56             my @boilerplate = qw{JQUERY CREATIVE_COMMONS OPENGRAPH};
57              
58             @EXPORT_OK = (@elements, @cool_stuff, @uc_elements, @boilerplate);
59             @EXPORT = ();
60             %EXPORT_TAGS = (
61             all => \@EXPORT_OK,
62             standard => [@conforming, @cool_stuff, qw{Q Sub Time Map Meta Link}],
63             default => \@EXPORT,
64             metadata => [qw(head title base Link Meta style)],
65             sections => [qw(body div section nav article aside h1 h2 h3 h4 h5 h6 header footer address)],
66             grouping => [qw(p hr br pre dialog blockquote ol ul li dl dt dd)],
67             text => [qw(a cite em strong small mark dfn abbr progress
68             meter code var samp kbd sup span i b bdo ruby rt rp Q Sub Time)],
69             embedded => [qw(figure img iframe embed object param video audio source
70             canvas area Map)],
71             tabular => [qw(table thead tbody tfoot th td colgroup col caption)],
72             form => [qw(form fieldset label input button select datalist
73             optgroup option textarea output)],
74             );
75             }
76              
77             sub new
78             {
79             my ($class, %options) = @_;
80             bless \%options, $class;
81             }
82              
83             sub ELEMENT
84             {
85             shift if blessed($_[0]) && $_[0]->isa(__PACKAGE__);
86             my ($el, @params) = @_;
87              
88             if (warnings::enabled())
89             {
90             Carp::carp("Non-standard HTML element <$el>")
91             unless grep { lc $el eq $_ } @elements;
92             }
93              
94             my $EL = XML::LibXML::Element->new($el);
95             $EL->setNamespace(XHTML_NS, undef, TRUE);
96            
97             if ($el eq 'time' and blessed($params[0]) and $params[0]->isa('DateTime'))
98             {
99             my $dt = shift @params;
100             my $string = $dt->strftime('%FT%T');
101             if ($dt->time_zone->is_utc)
102             {
103             $string .= 'Z';
104             }
105             elsif (!$dt->time_zone->is_floating)
106             {
107             my $zone = $dt->strftime('%z');
108             $zone =~ s/^(.\d{2})(\d{2})$/$1:$2/;
109             $string .= $zone;
110             }
111             $EL->setAttribute('datetime', $string);
112             if (!@params)
113             {
114             push @params, "$dt";
115             }
116             }
117            
118             PARAM: while (@params)
119             {
120             my $thing = shift @params;
121            
122             if (blessed($thing) and $thing->isa('XML::LibXML::Element'))
123             {
124             $EL->appendChild($thing);
125             }
126             elsif (blessed($thing) and $thing->isa('XML::LibXML::Text'))
127             {
128             $EL->appendChild($thing);
129             }
130             elsif (blessed($thing) and $thing->isa('XML::LibXML::Comment'))
131             {
132             $EL->appendChild($thing);
133             }
134             elsif (blessed($thing) and $thing->isa('XML::LibXML::PI'))
135             {
136             $EL->appendChild($thing);
137             }
138             elsif (blessed($thing) and $thing->isa('XML::LibXML::NodeList'))
139             {
140             $EL->appendChild($_) foreach $thing->get_nodelist;
141             }
142             elsif (blessed($thing) and $thing->isa('XML::LibXML::Attr'))
143             {
144             $EL->setAttribute($thing->nodeName, $thing->getValue);
145             }
146             elsif (ref $thing eq 'IO')
147             {
148             local $/ = undef;
149             my $string = <$thing>;
150             $EL->appendText($string);
151             }
152             elsif (ref $thing eq 'SCALAR')
153             {
154             $$thing = $EL;
155             }
156             elsif (ref $thing eq 'ARRAY')
157             {
158             unshift @params, map
159             { ref $_ ? $_ : XML::LibXML::Text->new($_); }
160             @$thing;
161             redo PARAM;
162             }
163             elsif (ref $thing eq 'HASH')
164             {
165             while (my ($k, $v) = each %$thing)
166             {
167             $k =~ s/^-//g;
168             $EL->setAttribute($k, "$v");
169             }
170             }
171             elsif (!ref $thing and $thing =~ /^-(\S+)$/ and @params)
172             {
173             my $attr = $1;
174             my $value = shift @params;
175             $EL->setAttribute($attr, "$value");
176             }
177             elsif (defined $thing)
178             {
179             if (warnings::enabled())
180             {
181             if (defined ref($thing) and ref($thing) =~ /^(CODE|REF|GLOB|LVALUE|FORMAT|Regexp)$/)
182             { Carp::carp(sprintf("Passed a %s reference", ref($thing))); }
183             elsif (blessed($thing) and !overload::Method($thing, '""'))
184             { Carp::carp(sprintf("Passed a blessed reference (%s) that does not overload stringification", ref($thing))); }
185             }
186            
187             $EL->appendText("$thing");
188             }
189             }
190            
191             if ($el eq 'html')
192             {
193             my $doc = HTML::HTML5::Builder::Document->new('1.0', 'utf-8');
194             $doc->adoptNode($EL);
195             $doc->setDocumentElement($EL);
196             return $doc;
197             }
198            
199             return $EL;
200             }
201              
202             sub _mksub
203             {
204             no strict 'refs';
205            
206             my ($function, $element) = @_;
207             $element = lc $function unless defined $element and length $element;
208            
209             my $sub = sub
210             {
211             shift if blessed($_[0]) && $_[0]->isa(__PACKAGE__);
212             return ELEMENT($element, @_);
213             };
214            
215             return $sub;
216             }
217              
218             sub AUTOLOAD
219             {
220             my ($func) = our $AUTOLOAD =~ /::(\w+)$/;
221             Carp::croak("Undefined function") unless $func =~ /^(([A-Za-z][a-z]*)|([Hh][1-6]))$/;
222             Carp::croak("Undefined function") unless grep { lc $func eq $_ } @elements;
223             my $sub = *{$func} = _mksub($func);
224             return $sub->(@_);
225             }
226              
227             sub DESTROY {} # not AUTOLOAD
228              
229             # Thanks to AUTOLOAD, UNIVERSAL::can is a little broken here, so instead
230             # we define out own can.
231             sub can
232             {
233             my ($class, $func) = @_;
234            
235             my $answer = UNIVERSAL::can(__PACKAGE__, $func)
236             || __PACKAGE__->SUPER::can($func);
237             return $answer if $answer;
238            
239             if ($func =~ /^(([A-Za-z][a-z]*)|([Hh][1-6]))$/
240             and grep { lc $func eq $_ } @elements)
241             {
242             return _mksub($func);
243             }
244            
245             return;
246             }
247              
248             sub TEXT
249             {
250             shift if blessed($_[0]) && $_[0]->isa(__PACKAGE__);
251             return XML::LibXML::TextNode->new($_[0]);
252             }
253              
254             sub COMMENT
255             {
256             shift if blessed($_[0]) && $_[0]->isa(__PACKAGE__);
257             return XML::LibXML::Comment->new($_[0]);
258             }
259              
260             {
261             my $parser = undef;
262             sub CHUNK
263             {
264             shift if blessed($_[0]) && $_[0]->isa(__PACKAGE__);
265            
266             unless ($parser)
267             {
268             eval 'use HTML::HTML5::Parser; 1;'
269             or Carp::croak("This feature requires HTML::HTML5::Parser.\n");
270             $parser = HTML::HTML5::Parser->new;
271             }
272            
273             my $dom = $parser->parse_string($_[0]);
274             my @kids = $dom->getElementsByTagName('body')->shift->childNodes;
275             return @kids;
276             }
277             }
278              
279             {
280             my $parser = undef;
281             sub XML_CHUNK
282             {
283             shift if blessed($_[0]) && $_[0]->isa(__PACKAGE__);
284            
285             unless ($parser)
286             {
287             $parser = XML::LibXML->new;
288             }
289            
290             my $dom = $parser->parse_balanced_chunk($_[0]);
291             my @kids = $dom->childNodes;
292             return @kids;
293             }
294             }
295              
296             {
297             my $dummyDoc = XML::LibXML::Document->new;
298             sub RAW_CHUNK
299             {
300             shift if blessed($_[0]) && $_[0]->isa(__PACKAGE__);
301             return $dummyDoc->createPI('decode', HTML::HTML5::Entities::encode_numeric($_[0]));
302             }
303             }
304              
305             sub _find_version
306             {
307             my ($req, @versions) = @_;
308              
309             return $versions[-1] unless defined $req;
310              
311             my $requested = do {
312             my ($maj, $min, $rev) = split /\./, $req;
313             ($maj||0)*1_000_000 + ($min||0)*1_000 + ($rev||0);
314             };
315             foreach my $v (@versions)
316             {
317             my $thisver = do {
318             my ($maj, $min, $rev) = split /\./, $v;
319             ($maj||0)*1_000_000 + ($min||0)*1_000 + ($rev||0);
320             };
321             if ($thisver >= $requested)
322             {
323             return $v;
324             }
325             }
326             return $req;
327             }
328              
329             sub JQUERY
330             {
331             shift if blessed($_[0]) && $_[0]->isa(__PACKAGE__);
332             my (%opts) = (scalar @_==1) ? (-version => $_[0]) : @_;
333             my @rv;
334            
335             my $templates = {
336             official => {
337             jQuery => 'http://code.jquery.com/jquery-%s.%s',
338             jQuery_v => '1.2.1, 1.2.2, 1.2.3, 1.2.4, 1.2.5, 1.2.6, 1.3.0, 1.3.1, 1.3.2, 1.4.0, 1.4.1, 1.4.2, 1.4.3, 1.4.4, 1.5.0, 1.5.1, 1.5.2, 1.6.0, 1.6.1, 1.6.2, 1.6.3, 1.6.4',
339             jQueryUI => 'http://code.jquery.com/ui/%s/jquery-ui.%s',
340             jQueryUI_v => '1.7.0, 1.7.1, 1.7.2, 1.7.3, 1.8.0, 1.8.1, 1.8.2, 1.8.4, 1.8.5, 1.8.6, 1.8.7, 1.8.8, 1.8.9, 1.8.10, 1.8.11, 1.8.12, 1.8.13, 1.8.14, 1.8.15, 1.8.16',
341             Style => 'http://code.jquery.com/ui/%s/themes/%s/jquery-ui.css',
342             Themes => 'base black-tie blitzer cupertino dark-hive dot-luv eggplant excite-bike flick hot-sneaks humanity le-frog mint-choc overcast pepper-grinder redmond smoothness south-street start sunny swanky-purse trontastic ui-darkness ui-lightness vader',
343             },
344             google => {
345             jQuery => 'https://ajax.googleapis.com/ajax/libs/jquery/%s/jquery.%s',
346             jQuery_v => '1.2.3, 1.2.6, 1.3.0, 1.3.1, 1.3.2, 1.4.0, 1.4.1, 1.4.2, 1.4.3, 1.4.4, 1.5.0, 1.5.1, 1.5.2, 1.6.0, 1.6.1, 1.6.2, 1.6.3, 1.6.4',
347             jQueryUI => 'https://ajax.googleapis.com/ajax/libs/jqueryui/%s/jquery-ui.%s',
348             jQueryUI_v => '1.5.2, 1.5.3, 1.6.0, 1.7.0, 1.7.1, 1.7.2, 1.7.3, 1.8.0, 1.8.1, 1.8.2, 1.8.4, 1.8.5, 1.8.6, 1.8.7, 1.8.8, 1.8.9, 1.8.10, 1.8.11, 1.8.12, 1.8.13, 1.8.14, 1.8.15, 1.8.16',
349             Style => 'http://ajax.googleapis.com/ajax/libs/jqueryui/%s/themes/%s/jquery-ui.css',
350             Themes => 'base black-tie blitzer cupertino dark-hive dot-luv eggplant excite-bike flick hot-sneaks humanity le-frog mint-choc overcast pepper-grinder redmond smoothness south-street start sunny swanky-purse trontastic ui-darkness ui-lightness vader',
351             },
352             microsoft => {
353             jQuery => 'http://ajax.aspnetcdn.com/ajax/jquery/jquery-%s.%s',
354             jQuery_v => '1.3.2, 1.4, 1.4.1, 1.4.2, 1.4.3, 1.4.4, 1.5, 1.5.1, 1.5.2, 1.6, 1.6.1, 1.6.2, 1.6.3, 1.6.4',
355             jQueryUI => 'http://ajax.aspnetcdn.com/ajax/jquery.ui/%s/jquery-ui.%s',
356             jQueryUI_v => '1.8.5, 1.8.6, 1.8.7, 1.8.8, 1.8.9, 1.8.10, 1.8.11, 1.8.12, 1.8.13, 1.8.14, 1.8.15, 1.8.16',
357             Style => 'http://ajax.aspnetcdn.com/ajax/jquery.ui/%s/themes/%s/jquery-ui.css',
358             Themes => 'base black-tie blitzer cupertino dark-hive dot-luv eggplant excite-bike flick hot-sneaks humanity le-frog mint-choc overcast pepper-grinder redmond smoothness south-street start sunny swanky-purse trontastic ui-darkness ui-lightness vader',
359             },
360             };
361             my $template = $templates->{ lc $opts{-source}||'google' } || $templates->{'google'};
362              
363             my @jquery_versions = split /\s*\,\s*/, $template->{jQuery_v};
364             my $url = sprintf($template->{jQuery},
365             _find_version($opts{-version}, @jquery_versions),
366             (!defined $opts{-min} or $opts{-min})?'min.js':'js',
367             );
368             push @rv, script(
369             -type => 'text/javascript',
370             -src => $url,
371             );
372            
373             if ((defined $opts{-ui} or defined $opts{-ui_version} or defined $opts{-theme})
374             and not (defined $opts{-ui} and !$opts{-ui}))
375             {
376             my @jqueryui_versions = split /\s*\,\s*/, $template->{jQueryUI_v};
377             my $url = sprintf($template->{jQueryUI},
378             _find_version($opts{-ui_version}, @jqueryui_versions),
379             (!defined $opts{-min} or $opts{-min})?'min.js':'js',
380             );
381             push @rv, script(
382             -type => 'text/javascript',
383             -src => $url,
384             );
385            
386             if (defined $opts{-theme})
387             {
388             my $theme = $opts{-theme};
389             $theme = 'base' unless $template->{Themes} =~ /\b$theme\b/;
390            
391             my $url = sprintf($template->{Style},
392             _find_version($opts{-ui_version}, @jqueryui_versions),
393             $theme,
394             );
395             push @rv, Link(
396             -rel => 'stylesheet',
397             -media => 'screen',
398             -type => 'text/css',
399             -src => $url,
400             );
401             }
402             }
403            
404             return wantarray ? @rv : XML::LibXML::NodeList->new_from_ref(\@rv, 1);
405             }
406              
407             sub CREATIVE_COMMONS
408             {
409             shift if blessed($_[0]) && $_[0]->isa(__PACKAGE__);
410             my (%opts) = (scalar @_==1) ? (-licence => $_[0]) : @_;
411             $opts{-licence} ||= delete $opts{-license}; # be kind to Americans
412             $opts{-licence} ||= 'by-sa';
413             my @rv;
414            
415             if (warnings::enabled())
416             {
417             Carp::carp("Unknown licence")
418             unless lc $opts{-licence} =~ /^by(-(sa|nd|nc|nc-sa|nc-nd))?$/;
419             }
420            
421             my $size = {
422             'small' => '80x15',
423             'large' => '88x31',
424             '80x15' => '80x15',
425             '88x31' => '88x31',
426             }->{ lc $opts{-size} || 'large' } || '88x31';
427            
428             push @rv, a(
429             -rel => 'license',
430             -href => sprintf("http://creativecommons.org/licenses/%s/3.0/", lc $opts{-licence}),
431             img(
432             -alt => ($opts{-alt} || sprintf("Creative Commons %s Licence", uc $opts{-licence})),
433             -style => 'border:0',
434             -src => sprintf("http://i.creativecommons.org/l/%s/3.0/%s.png", lc $opts{-licence}, $size),
435             ),
436             );
437            
438             if (defined $opts{-attributionName}
439             or defined $opts{-attributionURL}
440             or defined $opts{-type}
441             or defined $opts{-title})
442             {
443             my ($this_work, $attribution);
444             push @rv, br(), span(
445             span(\$this_work, $opts{-title}||'This work'),
446             defined $opts{-attributionName} ? [' by ', span(\$attribution, -property=>'cc:attributionName', [$opts{-attributionName}])] : [],
447             ' is licensed under a ',
448             a(
449             -rel => 'license',
450             -href => sprintf("http://creativecommons.org/licenses/%s/3.0/", lc $opts{-licence}),
451             [$opts{-alt} || sprintf("Creative Commons %s Licence", uc $opts{-licence})],
452             ),
453             );
454            
455             if (defined $opts{-title})
456             {
457             $this_work->setAttribute(property => 'dc:title');
458             }
459             if (defined $opts{-type} and length $opts{-type})
460             {
461             $this_work->setAttribute(rel => 'dc:type');
462             $this_work->setAttribute(resource => sprintf('http://purl.org/dc/dcmitype/%s', ucfirst lc $opts{-type}));
463             }
464             if (defined $attribution and defined $opts{-attributionURL})
465             {
466             $this_work->setAttribute(rel => 'cc:attributionURL');
467             $this_work->setAttribute(resource => $opts{-attributionURL});
468             }
469             }
470              
471             return span(@rv,
472             -class=>'creative_commons',
473             (defined $opts{-url} ? { -about => $opts{-url} } : {}),
474             );
475             }
476              
477             sub OPENGRAPH
478             {
479             shift if blessed($_[0]) && $_[0]->isa(__PACKAGE__);
480             my (%opts) = (scalar @_==1) ? %{$_[0]} : @_;
481            
482             my $map = {
483             title => 'og:title dc:title',
484             url => 'og:url dc:identifier',
485             description => 'og:description dc:description',
486             };
487            
488             my @rv;
489             while (my ($key, $value) = each %opts)
490             {
491             $key = lc $key;
492             $key =~ s/^-//;
493            
494             push @rv, Meta(-property => ($map->{$key}||sprintf('og:%s', $key)), -content => $value);
495             }
496            
497             return wantarray ? @rv : XML::LibXML::NodeList->new_from_ref(\@rv, 1);
498             }
499              
500             1;
501              
502             __END__