File Coverage

blib/lib/HTML/Blitz/Builder.pm
Criterion Covered Total %
statement 146 148 98.6
branch 62 82 75.6
condition 17 21 80.9
subroutine 22 22 100.0
pod 5 7 71.4
total 252 280 90.0


line stmt bran cond sub pod time code
1             package HTML::Blitz::Builder 0.1101;
2 2     2   578034 use v5.20;
  2         7  
3 2     2   14 use warnings qw(all FATAL uninitialized);
  2         5  
  2         200  
4 2     2   13 no warnings qw(experimental::signatures);
  2         3  
  2         69  
5 2     2   11 use feature qw(signatures);
  2         4  
  2         414  
6 2     2   11 use Exporter qw(import);
  2         3  
  2         84  
7 2     2   16 use Carp qw(croak);
  2         3  
  2         98  
8 2     2   9 use Scalar::Util qw(refaddr);
  2         4  
  2         930  
9              
10             our @EXPORT_OK = qw(
11             mk_doctype
12             mk_comment
13             mk_elem
14             to_html
15             fuse_fragment
16             );
17              
18             # data Fragment
19             # = Null
20             # | Txt Text
21             # | Html Text
22             # instance IsString Fragment where
23             # fromString = Txt . T.pack
24             # instance Monoid Fragment where
25             # mempty = Null
26             # mappend Null y = y
27             # mappend x Null = x
28             # mappend (Txt s) (Txt t) = Txt (s <> t)
29             # mappend (Html s) (Html t) = Html (s <> t)
30             # mappend (Txt s) (Html t) = Html (escape s <> t)
31             # mappend (Html s) (Txt t) = Html (s <> escape t)
32              
33 15     15 0 2121 sub FREEZE($self, $type) { $$self }
  15         29  
  15         47  
  15         29  
  15         79  
34 15     15 0 13324 sub THAW($class, $type, $str) { bless \$str, $class }
  15         31  
  15         23  
  15         25  
  15         26  
  15         57  
35              
36             my $Null;
37             package HTML::Blitz::Builder::_Null {
38             $Null = bless do { \my $tmp };
39              
40 1     1   8083 sub FREEZE($self, $type) { () }
  1         3  
  1         2  
  1         2  
  1         8  
41 1     1   798 sub THAW($class, $type) { $Null }
  1         2  
  1         3  
  1         2  
  1         3  
42             }
43              
44             package HTML::Blitz::Builder::_Text {
45             use overload
46 125     125   907 '""' => sub ($self, @) { $$self },
  125         1571  
  125         184  
  125         213  
47 2     2   14 fallback => 1;
  2         6  
  2         20  
48              
49 63     63   96 sub _mk($str) { bless \$str }
  63         105  
  63         83  
  63         203  
50              
51 2     2   274 sub FREEZE($self, $type) { $$self }
  2         5  
  2         4  
  2         4  
  2         39  
52 2     2   1415 sub THAW($class, $type, $str) { bless \$str, $class }
  2         6  
  2         3  
  2         4  
  2         4  
  2         8  
53             }
54              
55             sub fuse_fragment {
56 172 100   172 1 175329 return $Null if !@_;
57 158         266 my $acc = shift;
58 158         358 my $can_mod = 0;
59 158         316 for (@_) {
60 30 100 100     117 next if (refaddr($_) // 0) == $Null;
61 29 100 100     88 if ((refaddr($acc) // 0) == $Null) {
62 1         3 $acc = $_;
63 1         2 $can_mod = 0;
64 1         3 next;
65             }
66 28 100       59 if (ref($acc) eq __PACKAGE__) {
67 14 100       29 if (!$can_mod) {
68 5         12 my $tmp = $$acc;
69 5         10 $acc = bless \$tmp;
70 5         12 $can_mod = 1;
71             }
72 14 0       54 $$acc .= ref($_) eq __PACKAGE__ ? $$_ : s{(?![\n\t])([[:cntrl:]&<])}{ $1 eq '<' ? '<' : $1 eq '&' ? '&' : '&#' . ord($1) . ';' }egr;
  0 0       0  
    100          
73             } else {
74 14 100       51 if (ref($_) eq __PACKAGE__) {
75 4         16 my $tmp =
76 2 0       10 $acc =~ s{(?![\n\t])([[:cntrl:]&<])}{ $1 eq '<' ? '<' : $1 eq '&' ? '&' : '&#' . ord($1) . ';' }egr
    50          
77             . $$_;
78 4         10 $acc = bless \$tmp;
79 4         6 $can_mod = 1;
80             } else {
81 10         31 $acc .= $_;
82             }
83             }
84             }
85 158 100 100     1161 (refaddr($acc) // 0) == $Null
86             || ref($acc) eq __PACKAGE__
87             ? $acc
88             : HTML::Blitz::Builder::_Text::_mk "$acc"
89             }
90              
91 92     92 1 1159 sub to_html(@parts) {
  92         162  
  92         136  
92 92         180 my $item = fuse_fragment @parts;
93 92 100 50     502 ref($item) eq __PACKAGE__ ? $$item
    100          
94             : (refaddr($item) // 0) == $Null ? ''
95 4 0       34 : $item =~ s{(?![\n\t])([[:cntrl:]&<])}{ $1 eq '<' ? '<' : $1 eq '&' ? '&' : '&#' . ord($1) . ';' }egr
    50          
96             }
97              
98 4     4 1 230723 sub mk_doctype() {
  4         9  
99 4         10 my $code = '';
100 4         19 bless \$code
101             }
102              
103 13     13 1 1501 sub mk_comment($content) {
  13         24  
  13         13  
104 13 100       317 $content =~ /\A(-?>)/
105             and croak "HTML comment cannot start with '$1': '$content'";
106 11 100       255 $content =~ /(";
109 8         29 bless \$code
110             }
111              
112 6     6   12 sub _mk_attr($name, $value) {
  6         8  
  6         16  
  6         10  
113 6 50       25 $name =~ m{\A[^\s/>="'<[:cntrl:]]+\z}
114             or croak "Invalid attribute name '$name'";
115 6         13 my $code = " $name";
116 6 50       17 if ($value ne '') {
117 6         12 $code .= '=';
118 6 100       20 if ($value !~ m{[\s"'=<>`]}) {
119 3 0       9 $code .= $value =~ s{([[:cntrl:]&])}{ $1 eq '&' ? '&' : '&#' . ord($1) . ';' }egr;
  0         0  
120             } else {
121 3 50       12 $code .= '"' . $value =~ s{(?![\n\t])([[:cntrl:]&"])}{ $1 eq '"' ? '"' : $1 eq '&' ? '&' : '&#' . ord($1) . ';' }egr . '"';
  1 50       8  
122             }
123             }
124             $code
125 6         23 }
126              
127             my %is_void = map +($_ => 1), qw(
128             area
129             base basefont bgsound br
130             col
131             embed
132             frame
133             hr
134             img input
135             keygen
136             link
137             meta
138             param
139             source
140             track
141             wbr
142             );
143              
144             my %is_childless = map +($_ => 1), qw(
145             title
146             textarea
147             script
148             style
149             );
150              
151 57     57 1 12778 sub mk_elem($name, @args) {
  57         109  
  57         109  
  57         173  
152 57 100 100     293 my $attrs = @args && ref($args[0]) eq 'HASH'
153             ? shift @args
154             : {};
155 57 50       397 $name =~ m{\A[A-Za-z][^\s/>[:cntrl:]]*\z}
156             or croak "Invalid tag name '$name'";
157 57         135 (my $lc_name = $name) =~ tr/A-Z/a-z/;
158 57         128 my $item = fuse_fragment @args;
159 57         211 my $attr_str = join '', map _mk_attr($_, $attrs->{$_}), sort keys %$attrs;
160 57         150 my $html = "<$name$attr_str>";
161 57 100       158 if ($is_void{$lc_name}) {
162 9 100 50     228 croak "<$name> tag cannot have contents" if (refaddr($item) // 0) != $Null;
163             } else {
164 48 100 100     481 croak "<$name> tag cannot have child elements" if $is_childless{$lc_name} && ref($item) eq __PACKAGE__;
165 46         68 my $contents;
166 46 100       124 if ($lc_name eq 'style') {
    100          
167 6 50 50     21 $contents = (refaddr($item) // 0) == $Null ? '' : $item;
168 6 100       16 $contents =~ m{(])}aai
169             and croak "<$name> tag cannot contain '$1'";
170             } elsif ($lc_name eq 'script') {
171 24 50 50     88 $contents = (refaddr($item) // 0) == $Null ? '' : $item;
172             SCRIPT_DATA: {
173 24 100       40 $contents =~ m{ ( ) | ( < (/?) script [ \t\r\n\f/>] ) }xaaigc
  11         24  
178             or last SCRIPT_DATA;
179 8 100       27 $1 and redo SCRIPT_DATA;
180 6 50       24 $3 and croak "<$name> tag cannot contain '$2'";
181              
182 6 100       11 $contents =~ m{ (-->) | ] }xaaigc
183             or croak "<$name> tag cannot contain '' or ''";
184 5 100       17 $1 and redo SCRIPT_DATA;
185 2         5 redo SCRIPT_DATA_ESCAPED;
186             }
187             }
188             } else {
189 16         34 $contents = to_html $item;
190             }
191 37         101 $html .= "$contents";
192             }
193 44         220 bless \$html
194             }
195              
196             1
197             __END__