File Coverage

blib/lib/Text/HTML/Turndown/Node.pm
Criterion Covered Total %
statement 89 92 96.7
branch 22 26 84.6
condition 24 30 80.0
subroutine 17 17 100.0
pod 0 6 0.0
total 152 171 88.8


line stmt bran cond sub pod time code
1             package Text::HTML::Turndown::Node 0.12;
2 6     6   117 use 5.020;
  6         23  
3 6     6   34 use Moo;
  6         25  
  6         62  
4 6     6   2714 use experimental 'signatures';
  6         15  
  6         50  
5 6     6   1266 use stable 'postderef';
  6         14  
  6         58  
6              
7             our @blockElements = (
8             'ADDRESS', 'ARTICLE', 'ASIDE', 'AUDIO', 'BLOCKQUOTE', 'BODY', 'CANVAS',
9             'CENTER', 'DD', 'DIR', 'DIV', 'DL', 'DT', 'FIELDSET', 'FIGCAPTION', 'FIGURE',
10             'FOOTER', 'FORM', 'FRAMESET', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEADER',
11             'HGROUP', 'HR', 'HTML', 'ISINDEX', 'LI', 'MAIN', 'MENU', 'NAV', 'NOFRAMES',
12             'NOSCRIPT', 'OL', 'OUTPUT', 'P', 'PRE', 'SECTION', 'TABLE', 'TBODY', 'TD',
13             'TFOOT', 'TH', 'THEAD', 'TR', 'UL'
14             );
15             our %blockElements = map { $_ => 1, lc $_ => 1 } @blockElements;
16              
17 3246     3246   4307 sub _isBlock ($self) {
  3246         4655  
  3246         3982  
18 3246         27503 $blockElements{ $self->nodeName }
19             };
20              
21             our @voidElements = (
22             'AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT',
23             'KEYGEN', 'LINK', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR'
24             );
25             our %voidElements = map { $_ => 1, lc $_ => 1 } @voidElements;
26              
27              
28             has '_node' => (
29             is => 'ro',
30             required => 1,
31             handles => [qw[
32             parentNode
33             firstChild
34             previousSibling
35             nextSibling
36             childNodes
37             lastChild
38             nodeName
39             nodeValue
40             nodeType
41             textContent
42             getAttribute
43             isEqual
44              
45             toString
46              
47             find
48             ]],
49             );
50              
51 1221     1221   1667 sub _isVoid( $self ) {
  1221         1810  
  1221         1518  
52 1221         7701 $voidElements{ $self->nodeName }
53             }
54              
55 1078     1078   1485 sub _hasVoid( $self ) {
  1078         1471  
  1078         1263  
56 1078         3681 return _has($self, \%voidElements)
57             }
58              
59             has ['isVoid', 'hasVoid', 'isBlock', 'isMeaningfulWhenBlank', 'hasMeaningfulWhenBlank',
60             'isCode'] => (
61             is => 'ro',
62             required => 1,
63             );
64              
65 4     4 0 64 sub className( $self ) {
  4         9  
  4         7  
66 4         85 $self->getAttribute('class');
67             }
68              
69 4715     4715   6211 sub _isCode( $self ) {
  4715         6320  
  4715         5927  
70 4715 100       17390 return 1 if uc $self->nodeName eq 'CODE';
71 4644         13743 my $p = $self->parentNode;
72 4644 100 66     17693 if( $p and $p->can('nodeName')) {
73 3637         32087 return _isCode($self->parentNode)
74             };
75             }
76              
77             our @meaningfulWhenBlankElements = (
78             'A', 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TH', 'TD', 'IFRAME', 'SCRIPT',
79             'AUDIO', 'VIDEO'
80             );
81             our %meaningfulWhenBlankElements = map { $_ => 1, lc $_ => 1 } @meaningfulWhenBlankElements;
82              
83 1078     1078   1525 sub _isMeaningfulWhenBlank( $self ) {
  1078         1524  
  1078         1363  
84 1078         5439 $meaningfulWhenBlankElements{ $self->nodeName }
85             }
86              
87 1078     1078   1445 sub _hasMeaningfulWhenBlank( $self ) {
  1078         1378  
  1078         1422  
88 1078         2170 _has( $self, \%meaningfulWhenBlankElements )
89             }
90              
91 2156     2156   3067 sub _has( $self, $nodeNames ) {
  2156         2784  
  2156         2931  
  2156         2817  
92 2156 100       10710 return if ! $self->can('getElementsByTagName');
93 1514         21917 for my $tag (sort keys $nodeNames->%*) {
94 39451 100       1564272 return 1 if $self->getElementsByTagName($tag)
95             }
96             }
97              
98 54     54 0 1096 sub firstNonBlankChild( $self ) {
  54         78  
  54         69  
99 54         223 return ([$self->_node->nonBlankChildNodes]->[0]);
100             }
101              
102 757     757 0 1132 sub isBlank( $self ) {
  757         1044  
  757         983  
103 757 100 100     14951 !$self->isVoid
      100        
      100        
104             && !$self->isMeaningfulWhenBlank
105             && $self->textContent =~ /^\s*$/
106             && !$self->hasVoid
107             && !$self->hasMeaningfulWhenBlank()
108             }
109              
110             around BUILDARGS => sub( $orig, $class, %args ) {
111             my $node = $args{ _node };
112             if( ! exists $args{ isVoid } ) {
113             $args{ isVoid } = _isVoid( $node );
114             };
115             if( ! exists $args{ hasVoid } ) {
116             $args{ hasVoid } = _hasVoid( $node );
117             };
118             if( ! exists $args{ isMeaningfulWhenBlank } ) {
119             $args{ isMeaningfulWhenBlank } = _isMeaningfulWhenBlank( $node );
120             };
121             if( ! exists $args{ hasMeaningfulWhenBlank } ) {
122             $args{ hasMeaningfulWhenBlank } = _hasMeaningfulWhenBlank( $node );
123             };
124             if( ! exists $args{ isCode } ) {
125             $args{ isCode } = _isCode( $node );
126             };
127             if( ! exists $args{ isBlock } ) {
128             $args{ isBlock } = _isBlock( $node );
129             };
130              
131             return $class->$orig(\%args);
132             };
133              
134              
135 757     757 0 5917 sub flankingWhitespace( $node, $options ) {
  757         1203  
  757         997  
  757         993  
136 757 100 100     1646 if( _isBlock($node) || ($options->{ preformattedCode } && isCode($node) )) {
      100        
137 651         22572 return { leading => '', trailing => '' };
138             }
139              
140 106         5264 my $edges = edgeWhitespace( $node->textContent );
141              
142             # abandon leading ASCII WS if left-flanked by ASCII WS
143 106 50 66     403 if ($edges->{leadingAscii} && isFlankedByWhitespace('left', $node, $options)) {
144 0         0 $edges->{leading} = $edges->{leadingNonAscii};
145             }
146              
147             # abandon trailing ASCII WS if right-flanked by ASCII WS
148 106 50 66     350 if ($edges->{trailingAscii} && isFlankedByWhitespace('right', $node, $options)) {
149             $edges->{trailing} = $edges->{trailingNonAscii}
150 0         0 }
151              
152             return { leading => $edges->{leading}, trailing => $edges->{trailing}, }
153 106         668 }
154              
155              
156 106     106 0 2869 sub edgeWhitespace ($string) {
  106         217  
  106         161  
157 106         766 $string =~ /^(([ \t\r\n]*)(\s*))(?:(?=\S)[\s\S]*\S)?((\s*?)([ \t\r\n]*))$/;
158             return {
159 106         1250 leading => $1, # whole string for whitespace-only strings
160             leadingAscii => $2,
161             leadingNonAscii => $3,
162             trailing => $4, # empty for whitespace-only strings
163             trailingNonAscii => $5,
164             trailingAscii => $6,
165             }
166             }
167              
168 16     16 0 27 sub isFlankedByWhitespace( $side, $node, $options) {
  16         23  
  16         30  
  16         22  
  16         22  
169 16         40 my $sibling;
170             my $regExp;
171 16         0 my $isFlanked;
172              
173 16 100       36 if ($side eq 'left') {
174 7         123 $sibling = $node->previousSibling;
175 7         188 $regExp = qr/ $/;
176             } else {
177 9         133 $sibling = $node->nextSibling;
178 9         237 $regExp = qr/^ /;
179             }
180              
181 16 100       56 if ($sibling) {
182 10 100 66     94 if ($sibling->nodeType == 3) {
    50 33        
    50          
183 6         43 $isFlanked = $sibling->nodeValue =~ /$regExp/;
184             } elsif ($options->{preformattedCode} && $sibling->nodeName eq 'CODE') {
185 0         0 $isFlanked = undef;
186             } elsif ($sibling->nodeType == 1 && !_isBlock($sibling)) {
187 4         40 $isFlanked = $sibling->textContent =~ /$regExp/;
188             }
189             }
190 16         79 return $isFlanked
191             }
192              
193             1;
194             =head1 REPOSITORY
195              
196             The public repository of this module is
197             L.
198              
199             =head1 SUPPORT
200              
201             The public support forum of this module is L.
202              
203             =head1 BUG TRACKER
204              
205             Please report bugs in this module via the Github bug queue at
206             L
207              
208             =head1 AUTHOR
209              
210             Max Maischein C
211              
212             =head1 COPYRIGHT (c)
213              
214             Copyright 2025- by Max Maischein C.
215              
216             =head1 LICENSE
217              
218             This module is released under the Artistic License 2.0.
219              
220             =cut