File Coverage

blib/lib/HTML/Make.pm
Criterion Covered Total %
statement 189 203 93.1
branch 85 96 88.5
condition 24 30 80.0
subroutine 19 20 95.0
pod 11 15 73.3
total 328 364 90.1


line stmt bran cond sub pod time code
1             package HTML::Make;
2 10     10   644510 use warnings;
  10         105  
  10         380  
3 10     10   57 use strict;
  10         18  
  10         427  
4             require Exporter;
5             our $VERSION = '0.16';
6 10     10   56 use Carp;
  10         19  
  10         887  
7 10     10   5420 use HTML::Valid::Tagset ':all';
  10         128038  
  10         3920  
8 10     10   92 use JSON::Parse '0.62', 'read_json';
  10         21  
  10         23527  
9              
10             my $dir = __FILE__;
11             $dir =~ s/Make\.pm/Make/;
12             my $infofile = "$dir/info.json";
13             my $info = read_json ($infofile);
14              
15             # This is a list of valid tags.
16              
17             my %tags = %HTML::Valid::Tagset::isKnown;
18             my %noCloseTags = %HTML::Valid::Tagset::emptyElement;
19             my %isBlock = %HTML::Valid::Tagset::isBlock;
20              
21             our $texttype = 'text';
22             our $blanktype = 'blank';
23              
24             sub op
25       0 0   {
26             }
27              
28             sub new
29             {
30 93     93 1 27491 my ($class, $type, %options) = @_;
31 93         174 my $obj = {};
32 93         178 bless $obj;
33 93 100       195 if (! $type) {
34 1         2 $type = $blanktype;
35             }
36 93         280 $obj->{type} = lc ($type);
37             # User is not allowed to use 'text' type.
38 93 100       214 if ($type eq $texttype) {
39 24         71 my ($package, undef, undef) = caller ();
40 24 100       62 if ($package ne __PACKAGE__) {
41 1         16 die "Illegal use of text type";
42             }
43 23 100       50 if (! defined $options{text}) {
44 1         167 croak "Text type object with empty text";
45             }
46 22 100       48 if (ref $options{text}) {
47 1         227 croak "text field must be a scalar";
48             }
49 21         32 $obj->{text} = $options{text};
50 21         52 return $obj;
51             }
52 69 100       160 if ($options{nocheck}) {
53             # We won't check when the user adds attributes later.
54 3         10 $obj->{nocheck} = 1;
55 3         5 delete $options{nocheck};
56             }
57 69 100       159 if (! $obj->{nocheck}) {
58 66 100       144 if ($type ne $blanktype) {
59 65 100       302 if (! $tags{lc $type}) {
    100          
60 2         192 carp "Unknown tag type '$type'";
61             }
62             elsif (! $isHTML5{lc $type}) {
63 1         94 carp "<$type> is not HTML5";
64             }
65             }
66             }
67 69 100       302 if (exists $options{text}) {
68 16 100       42 if (! defined $options{text}) {
69 1         182 carp "Undefined value for text";
70 1         9 $options{text} = '';
71             }
72 16         43 $obj->add_text ($options{text});
73 15         28 delete $options{text};
74             }
75 68 100       144 if ($options{attr}) {
76 9         16 $obj->add_attr (%{$options{attr}});
  9         44  
77 9         21 delete $options{attr};
78             }
79             # Convenience shortcuts
80 68 100       141 if ($options{id}) {
81 1         4 $obj->add_attr (id => $options{id});
82 1         2 delete $options{id};
83             }
84 68 100       134 if ($options{class}) {
85 1         6 $obj->add_attr (class => $options{class});
86 1         3 delete $options{class};
87             }
88 68 100       130 if ($options{href}) {
89 2 100 66     10 if ($type ne 'a' && $type ne 'link') {
90 1         238 carp "href is only allowed with an 'a' or 'link' element";
91             }
92             else {
93 1         4 $obj->add_attr (href => $options{href});
94             }
95 2         13 delete $options{href};
96             }
97 68 50       131 if ($options{src}) {
98 0 0 0     0 if ($type ne 'img' && $type ne 'image') {
99 0         0 carp "src is only allowed with an 'img' element";
100             }
101             else {
102 0         0 $obj->add_attr (src => $options{src});
103             }
104 0         0 delete $options{src};
105             }
106 68         172 for my $k (keys %options) {
107 1         102 carp "Unknown option '$k'";
108 1         8 delete $options{$k};
109             }
110 68         179 return $obj;
111             }
112              
113             sub check_attributes
114             {
115 13     13 0 35 my ($obj, %attr) = @_;
116 13 50       55 if ($obj->{nocheck}) {
117 0         0 return;
118             }
119 13 100       36 if ($attr{id}) {
120             # This is a bit of a bug since \s matches more things than the
121             # 5 characters disallowed in HTML IDs.
122 3 50       15 if ($attr{id} =~ /\s/) {
123 0         0 carp "ID attributes cannot contain spaces";
124             }
125             }
126 13         38 for my $k (keys %attr) {
127 15         430 my $type = lc $obj->{type};
128 15 100       65 if (! tag_attr_ok (lc $type, $k)) {
129 2         836 carp "attribute $k is not allowed for <$type> in HTML5";
130             }
131             }
132             }
133              
134             sub add_attr
135             {
136 14     14 1 48 my ($obj, %attr) = @_;
137 14 100       43 if (! $obj->{nocheck}) {
138 13         49 check_attributes ($obj, %attr);
139             }
140 14         2505 for my $k (sort keys %attr) {
141 16 100       52 if ($obj->{attr}->{$k}) {
142 1         85 carp "Overwriting attribute '$k' for '$obj->{type}' tag";
143             }
144 16         105 $obj->{attr}->{$k} = $attr{$k};
145             }
146             }
147              
148             sub add_class
149             {
150 3     3 1 16 my ($obj, $class) = @_;
151 3         7 my $oldclass = $obj->{attr}{class};
152 3         3 my $newclass;
153 3 100       8 if ($oldclass) {
154 2         5 $newclass = $oldclass . ' ' . $class;
155             }
156             else {
157 1         2 $newclass = $class;
158             }
159 3         7 $obj->{attr}{class} = $newclass;
160             }
161              
162             sub add_text
163             {
164 22     22 1 65 my ($obj, $text) = @_;
165 22         59 my $x = __PACKAGE__->new ($texttype, text => $text);
166 20         30 CORE::push @{$obj->{children}}, $x;
  20         49  
167 20         33 return $x;
168             }
169              
170             sub add_comment
171             {
172 1     1 1 5 my ($obj, $comment) = @_;
173 1         6 my $x = __PACKAGE__->new ($texttype, text => "");
174 1         3 CORE::push @{$obj->{children}}, $x;
  1         2  
175 1         3 return $x;
176             }
177              
178             sub attr
179             {
180 2     2 1 488 my ($obj) = @_;
181 2         4 my $attr = $obj->{attr};
182 2 100       9 if (! $attr) {
183 1         3 return {};
184             }
185             # Copy the hash so that the caller does not accidentally alter
186             # this object's internals
187 1         6 my %attr = %$attr;
188 1         3 return \%attr;
189             }
190              
191             sub check_mismatched_tags
192             {
193 21     21 0 39 my ($obj, $el) = @_;
194 21         37 my $ptype = $obj->{type};
195 21         75 my $is_table_el = ($el =~ /^(th|td)$/i);
196 21 100 100     71 if ($ptype eq 'tr' && ! $is_table_el) {
197 1         104 carp "Pushing non-table element <$el> to a table row";
198 1         41 return;
199             }
200 20 100 100     53 if ($is_table_el && $ptype ne 'tr') {
201 2         159 carp "Pushing <$el> to a non-tr element <$ptype>";
202 2         79 return;
203             }
204 18         51 my $is_list_parent = ($ptype =~ /^(ol|ul)$/);
205 18 100 100     101 if (lc ($el) eq 'li' && ! $is_list_parent) {
206 1         84 carp "Pushing
  • to a non-list parent <$ptype>";
  • 207 1         39 return;
    208             }
    209             }
    210              
    211             sub children
    212             {
    213 3     3 1 577 my ($obj) = @_;
    214 3 100       10 if (! $obj->{children}) {
    215 1         3 return [];
    216             }
    217 2         2 my @children = @{$obj->{children}};
      2         7  
    218 2         5 return \@children;
    219             }
    220              
    221             sub multiply
    222             {
    223 3     3 1 25 my ($parent, $element, $contents) = @_;
    224 3         5 my @elements;
    225 3 100       13 if (! defined $element) {
    226 1         72 croak "No element given";
    227             }
    228 2 50 33     13 if (! defined $contents || ref $contents ne 'ARRAY') {
    229 0         0 croak 'contents not array or not defined';
    230             }
    231 2         8 for my $content (@$contents) {
    232 6         15 my $x = $parent->push ($element, text => $content);
    233 6         12 CORE::push @elements, $x;
    234             }
    235 2 50       10 if (@elements != @$contents) {
    236 0         0 die "Mismatch of number of elements";
    237             }
    238 2         6 return @elements;
    239             }
    240              
    241             sub opening_tag
    242             {
    243 37     37 1 64 my ($obj) = @_;
    244 37         54 my $text = '';
    245 37         68 my $type = $obj->{type};
    246 37 100       74 if ($type eq 'html') {
    247 1         2 $text .= "\n";
    248             }
    249 37         91 $text .= "<$type";
    250 37 100       96 if ($obj->{attr}) {
    251 9         14 my @attr;
    252 9         24 my %attr = %{$obj->{attr}};
      9         43  
    253 9         35 for my $k (sort keys %attr) {
    254 11         53 my $v = $attr{$k};
    255 11 50       46 if (! defined $v) {
    256 0         0 carp "Value of attribute '$k' in element of type '$type' is undefined";
    257 0         0 $v = '';
    258             }
    259 11         32 $v =~ s/"/\\"/g;
    260 11 100 100     69 if ($type eq 'script' && ($k eq 'async' || $k eq 'defer')) {
          100        
    261 2         7 CORE::push @attr, "$k";
    262             }
    263             else {
    264 9         42 CORE::push @attr, "$k=\"$v\"";
    265             }
    266             }
    267 9         29 my $attr = join (' ', @attr);
    268 9         30 $text .= " $attr";
    269             }
    270 37         59 $text .= ">";
    271 37 100       101 if ($info->{newline}{$type}) {
    272 1         2 $text .= "\n";
    273             }
    274 37         81 return $text;
    275             }
    276              
    277             sub HTML::Make::push
    278             {
    279 24 50   24 1 123 if (scalar (@_) % 2 != 0) {
    280 0         0 carp "Usage: ->push ('el', class => 'a', );";
    281 0         0 return;
    282             }
    283 24         69 my ($obj, $el, %options) = @_;
    284 24         34 my $x;
    285 24 100       53 if (ref $el eq __PACKAGE__) {
    286 3         5 $x = $el;
    287 3 100       10 if ($x->{parent}) {
    288 1         76 carp "Pushed element of type $x->{type} already has a parent of type $x->{parent}{type}";
    289 1         42 return;
    290             }
    291             }
    292             else {
    293 21         59 check_mismatched_tags ($obj, $el);
    294 21         60 $x = __PACKAGE__->new ($el, %options);
    295             }
    296 23         34 CORE::push @{$obj->{children}}, $x;
      23         61  
    297 23         55 $x->{parent} = $obj;
    298 23         47 return $x;
    299             }
    300              
    301             sub text
    302             {
    303 56     56 1 2216 my ($obj) = @_;
    304 56         91 my $type = $obj->{type};
    305 56 50       124 if (! $type) {
    306 0         0 croak "No type";
    307             }
    308 56 100       121 if ($type eq $texttype) {
    309 18         44 return $obj->{text};
    310             }
    311 38         62 my $text = '';
    312 38 100       83 if ($type ne $blanktype) {
    313 37         88 $text = $obj->opening_tag ();
    314 37 100 100     143 if ($isBlock{$type} || $type eq 'tr') {
    315 19         34 $text .= "\n";
    316             }
    317             }
    318             # Recursively add text
    319 38         52 for my $child (@{$obj->{children}}) {
      38         104  
    320 33         93 $text .= $child->text ();
    321             }
    322 38 100 100     186 if ($type ne $blanktype && ! $noCloseTags{$type}) {
    323 36         76 $text .= "\n";
    324             }
    325 38         115 return $text;
    326             }
    327              
    328             sub type
    329             {
    330 2     2 0 1152 my ($obj) = @_;
    331 2         5 my $type = $obj->{type};
    332 2 50       6 if ($type eq $blanktype) {
    333 0         0 return undef;
    334             }
    335 2         9 return $type;
    336             }
    337              
    338             1;
    339