File Coverage

blib/lib/Text/HTML/CollapseWhitespace.pm
Criterion Covered Total %
statement 65 68 95.5
branch 20 24 83.3
condition 34 43 79.0
subroutine 8 8 100.0
pod 2 2 100.0
total 129 145 88.9


line stmt bran cond sub pod time code
1             package Text::HTML::CollapseWhitespace 0.11;
2 5     5   99 use 5.020;
  5         28  
3 5     5   26 use experimental 'signatures';
  5         12  
  5         34  
4 5     5   904 use stable 'postderef';
  5         9  
  5         34  
5 5     5   454 use Exporter 'import';
  5         8  
  5         6397  
6              
7             require Text::HTML::Turndown::Node;
8              
9             our @EXPORT_OK = ('collapseWhitespace');
10              
11             =head1 NAME
12              
13             Text::HTML::CollapseWhitespace - remove extraneous whitespace from a fragment
14              
15             =head1 SYNOPSIS
16              
17             my $tree = XML::LibXML->new->parse_html_string(
18             $input,
19             { recover => 2, encoding => 'UTF-8' }
20             );
21             $tree = collapseWhitespace($tree);
22              
23             =cut
24              
25             =head1 FUNCTIONS
26              
27             =head2 C<< collapseWhitespace (%options) >>
28              
29             collapseWhitespace( element => $tree,
30             isVoid => \&_isVoid,
31             )
32              
33             our @voidElements = (
34             'AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT',
35             'KEYGEN', 'LINK', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR'
36             );
37             our %voidElements = map { $_ => 1, lc $_ => 1 } @voidElements;
38             sub _isVoid( $element ) {
39             $voidElements{ $element->nodeName }
40             }
41              
42             This function modifies the tree in-place and removes extraneous whitespace
43             from the elements. The C, C and C predicates allow you
44             to customize what elements are recognized as pre , void or block HTML elements
45             if needed.
46              
47             =cut
48              
49 165     165 1 360 sub collapseWhitespace (%options) {
  165         747  
  165         269  
50 165         362 my $element = $options{ element }; # should be XML::LibXML
51 165   50     548 my $isBlock = $options{ isBlock } // \&Text::HTML::Turndown::Node::_isBlock;
52 165   50     621 my $isVoid = $options{ isVoid } // \&Text::HTML::Turndown::Node::_isVoid;
53 1407     1407   8712 my $isPre = $options{ isPre } || sub ($node) {
  1407         2054  
  1407         1714  
54 1407         7069 return uc($node->nodeName) eq 'PRE'
55 165   66     1164 };
56              
57             return
58 165 50 33     1531 if (!$element->firstChild || $isPre->($element));
59              
60 165         943 my $prevText;
61             my $keepLeadingWs;
62              
63 165         0 my $prev;
64 165         472 my $node = _next($prev, $element, $isPre);
65              
66 165         1724 while (! $node->isEqual($element)) {
67 2201 100 66     10077 if ($node->nodeType == 3 || $node->nodeType == 4) { # Node.TEXT_NODE or Node.CDATA_SECTION_NODE
    100          
68 644         5123 my $text = $node->data =~ s/[ \r\n\t]+/ /gr; # we only want to fold ASCII whitespace here
69              
70 644 100 100     4385 if ((!$prevText || $prevText->data =~ / $/) &&
      100        
      100        
71             !$keepLeadingWs && substr($text,0,1) eq ' ') {
72 369         871 $text = substr($text, 1);
73             }
74              
75             # `text` might be empty at this point.
76 644 100       1615 if (!$text) {
77 362         688 $node = remove($node);
78 362         4902 next;
79             }
80              
81 282         1163 $node->setData( $text );
82              
83 282         595 $prevText = $node
84             } elsif ($node->nodeType == 1) { # Node.ELEMENT_NODE
85 1391 100 100     3260 if ($isBlock->($node) || uc $node->nodeName eq 'BR') {
    100 100        
    100          
86 1255 100       2577 if ($prevText) {
87 250         2228 $prevText->setData( $prevText->data =~ s/ $//r );
88             }
89 1255         1954 undef $prevText;
90 1255         1695 undef $keepLeadingWs;
91             } elsif ($isVoid->($node) || $isPre->($node)) {
92             # Avoid trimming space around non-block, non-BR void elements and inline PRE.
93 18         40 undef $prevText;
94 18         33 $keepLeadingWs = 1;
95             } elsif ($prevText) {
96             # Drop protection if set previously.
97 75         445 undef $keepLeadingWs;
98             }
99             } else {
100 166         441 $node = remove($node);
101 166         711 next;
102             }
103              
104 1673         3399 my $nextNode = _next($prev, $node, $isPre);
105 1673         23637 $prev = $node;
106 1673         14960 $node = $nextNode;
107             }
108              
109 165 50       392 if ($prevText) {
110 0         0 $prevText->{data} =~ s/ $//;
111 0 0       0 if (!$prevText->{data}) {
112 0         0 remove($prevText)
113             }
114             }
115 165         497 return $element;
116             }
117              
118             =head2 remove($note)
119              
120             my $next = remove( $node );
121              
122             remove(node) removes the given node from the DOM and returns the
123             next node in the sequence.
124              
125             =cut
126              
127 528     528 1 755 sub remove ($node) {
  528         767  
  528         710  
128 528   66     2431 my $next = $node->nextSibling || $node->parentNode;
129              
130 528         4787 $node->parentNode->removeChild($node);
131              
132 528         1798 return $next
133             }
134              
135             =head2 _next(prev, current, isPre)
136              
137             my $next = _next( $node );
138              
139             returns the next node in the sequence, given the
140             current and previous nodes.
141              
142             @param {Node} prev
143             @param {Node} current
144             @param {Function} isPre
145             @return {Node}
146              
147             =cut
148              
149 1838     1838   2565 sub _next($prev, $current, $isPre) {
  1838         2502  
  1838         2581  
  1838         2342  
  1838         2300  
150 1838 100 100     4741 if (($prev && $prev->parentNode->isEqual($current)) || $isPre->($current)) {
      100        
151 691   66     7482 return $current->nextSibling || $current->parentNode
152             }
153              
154 1147   66     3715 return $current->firstChild || $current->nextSibling || $current->parentNode
155             }
156              
157             1;
158              
159             =head1 LICENSE
160              
161             The collapseWhitespace function is adapted from collapse-whitespace
162             by Luc Thevenard.
163              
164             The MIT License (MIT)
165              
166             Copyright (c) 2014 Luc Thevenard
167              
168             Permission is hereby granted, free of charge, to any person obtaining a copy
169             of this software and associated documentation files (the "Software"), to deal
170             in the Software without restriction, including without limitation the rights
171             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
172             copies of the Software, and to permit persons to whom the Software is
173             furnished to do so, subject to the following conditions:
174              
175             The above copyright notice and this permission notice shall be included in
176             all copies or substantial portions of the Software.
177              
178             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
179             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
180             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
181             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
182             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
183             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
184             THE SOFTWARE.
185              
186             =head1 REPOSITORY
187              
188             The public repository of this module is
189             L.
190              
191             =head1 SUPPORT
192              
193             The public support forum of this module is L.
194              
195             =head1 BUG TRACKER
196              
197             Please report bugs in this module via the Github bug queue at
198             L
199              
200             =head1 AUTHOR
201              
202             Max Maischein C
203              
204             =head1 COPYRIGHT (c)
205              
206             Copyright 2025- by Max Maischein C.
207              
208             =head1 LICENSE
209              
210             This module is released under the Artistic License 2.0
211             as far as permitted by the license of the original code.
212              
213             =cut