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.12;
2 6     6   148 use 5.020;
  6         26  
3 6     6   38 use experimental 'signatures';
  6         21  
  6         48  
4 6     6   1112 use stable 'postderef';
  6         61  
  6         47  
5 6     6   568 use Exporter 'import';
  6         11  
  6         8437  
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 167     167 1 300 sub collapseWhitespace (%options) {
  167         747  
  167         315  
50 167         344 my $element = $options{ element }; # should be XML::LibXML
51 167   50     491 my $isBlock = $options{ isBlock } // \&Text::HTML::Turndown::Node::_isBlock;
52 167   50     468 my $isVoid = $options{ isVoid } // \&Text::HTML::Turndown::Node::_isVoid;
53 1429     1429   9022 my $isPre = $options{ isPre } || sub ($node) {
  1429         2113  
  1429         1802  
54 1429         7337 return uc($node->nodeName) eq 'PRE'
55 167   66     1292 };
56              
57             return
58 167 50 33     1684 if (!$element->firstChild || $isPre->($element));
59              
60 167         864 my $prevText;
61             my $keepLeadingWs;
62              
63 167         0 my $prev;
64 167         462 my $node = _next($prev, $element, $isPre);
65              
66 167         1652 while (! $node->isEqual($element)) {
67 2222 100 66     10165 if ($node->nodeType == 3 || $node->nodeType == 4) { # Node.TEXT_NODE or Node.CDATA_SECTION_NODE
    100          
68 647         5265 my $text = $node->data =~ s/[ \r\n\t]+/ /gr; # we only want to fold ASCII whitespace here
69              
70 647 100 100     4573 if ((!$prevText || $prevText->data =~ / $/) &&
      100        
      100        
71             !$keepLeadingWs && substr($text,0,1) eq ' ') {
72 369         1015 $text = substr($text, 1);
73             }
74              
75             # `text` might be empty at this point.
76 647 100       3701 if (!$text) {
77 362         823 $node = remove($node);
78 362         5383 next;
79             }
80              
81 285         1205 $node->setData( $text );
82              
83 285         557 $prevText = $node
84             } elsif ($node->nodeType == 1) { # Node.ELEMENT_NODE
85 1407 100 100     3343 if ($isBlock->($node) || uc $node->nodeName eq 'BR') {
    100 100        
    100          
86 1264 100       2863 if ($prevText) {
87 252         2221 $prevText->setData( $prevText->data =~ s/ $//r );
88             }
89 1264         1923 undef $prevText;
90 1264         1733 undef $keepLeadingWs;
91             } elsif ($isVoid->($node) || $isPre->($node)) {
92             # Avoid trimming space around non-block, non-BR void elements and inline PRE.
93 19         35 undef $prevText;
94 19         35 $keepLeadingWs = 1;
95             } elsif ($prevText) {
96             # Drop protection if set previously.
97 78         361 undef $keepLeadingWs;
98             }
99             } else {
100 168         457 $node = remove($node);
101 168         677 next;
102             }
103              
104 1692         3264 my $nextNode = _next($prev, $node, $isPre);
105 1692         23819 $prev = $node;
106 1692         15233 $node = $nextNode;
107             }
108              
109 167 50       360 if ($prevText) {
110 0         0 $prevText->{data} =~ s/ $//;
111 0 0       0 if (!$prevText->{data}) {
112 0         0 remove($prevText)
113             }
114             }
115 167         494 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 530     530 1 868 sub remove ($node) {
  530         790  
  530         707  
128 530   66     2696 my $next = $node->nextSibling || $node->parentNode;
129              
130 530         5035 $node->parentNode->removeChild($node);
131              
132 530         1886 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 1859     1859   2534 sub _next($prev, $current, $isPre) {
  1859         2461  
  1859         2579  
  1859         2512  
  1859         2323  
150 1859 100 100     4698 if (($prev && $prev->parentNode->isEqual($current)) || $isPre->($current)) {
      100        
151 698   66     7413 return $current->nextSibling || $current->parentNode
152             }
153              
154 1161   66     3810 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