File Coverage

blib/lib/HTML/Make.pm
Criterion Covered Total %
statement 158 178 88.7
branch 63 82 76.8
condition 26 33 78.7
subroutine 19 19 100.0
pod 11 14 78.5
total 277 326 84.9


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