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