File Coverage

blib/lib/HTML/Embellish.pm
Criterion Covered Total %
statement 127 134 94.7
branch 51 62 82.2
condition 20 30 66.6
subroutine 11 11 100.0
pod 3 4 75.0
total 212 241 87.9


line stmt bran cond sub pod time code
1             #---------------------------------------------------------------------
2             package HTML::Embellish;
3             #
4             # Copyright 2010 Christopher J. Madsen
5             #
6             # Author: Christopher J. Madsen
7             # Created: October 8, 2006
8             #
9             # This program is free software; you can redistribute it and/or modify
10             # it under the same terms as Perl itself.
11             #
12             # This program is distributed in the hope that it will be useful,
13             # but WITHOUT ANY WARRANTY; without even the implied warranty of
14             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either the
15             # GNU General Public License or the Artistic License for more details.
16             #
17             # ABSTRACT: Typographically enhance HTML trees
18             #---------------------------------------------------------------------
19              
20 4     4   118016 use 5.008; # Need good Unicode support; Perl 5.10 recommended but 5.8 may work
  4         10  
  4         106  
21 4     4   17 use warnings;
  4         3  
  4         90  
22 4     4   12 use strict;
  4         5  
  4         125  
23 4     4   15 use Carp qw(croak);
  4         5  
  4         236  
24              
25 4     4   22 use Exporter 5.57 'import'; # exported import method
  4         74  
  4         895  
26              
27             ###open(LOG, '>:utf8', 'em.log');
28              
29             #=====================================================================
30             # Package Global Variables:
31              
32             our $VERSION = '1.001';
33             # This file is part of HTML-Embellish 1.001 (April 11, 2015)
34              
35             our @EXPORT = qw(embellish);
36              
37             my $mdash = chr(0x2014);
38             my $lsquo = chr(0x2018);
39             my $rsquo = chr(0x2019);
40             my $ldquo = chr(0x201C);
41             my $rdquo = chr(0x201D);
42             my $hellip = chr(0x2026);
43              
44             my $notQuote = qq/[^\"$ldquo$rdquo]/;
45             my $balancedQuoteString = qr/(?: (?>[^ \t\n\r\pP]+)
46             | (?= [ \t\n\r\pP])$notQuote
47             | $ldquo (?>$notQuote*) $rdquo )*/x;
48              
49             #=====================================================================
50             # Constants:
51             #---------------------------------------------------------------------
52              
53             BEGIN
54             {
55 4     4   4 my $i = 0;
56 4         8 for (qw(textRefs fixQuotes fixDashes fixEllipses fixEllipseSpace fixHellip
57             totalFields)) {
58             ## no critic (ProhibitStringyEval)
59 28         626 eval "sub $_ () { $i }";
60 28         1210 ++$i;
61             }
62             } # end BEGIN
63              
64             #=====================================================================
65             # Exported functions:
66             #---------------------------------------------------------------------
67             sub embellish
68             {
69 73     73 1 45429 my $html = shift @_;
70              
71 73 100 66     521 croak "First parameter of embellish must be an HTML::Element"
72             unless ref $html and $html->can('content_refs_list');
73              
74 72         292 my $e = HTML::Embellish->new(@_);
75 71         113 $e->process($html);
76             } # end embellish
77              
78             #=====================================================================
79             # Class Methods:
80             #---------------------------------------------------------------------
81             sub new
82             {
83 73     73 1 491 my $class = shift;
84 73 100       239 croak "Odd number of parameters passed to HTML::Embellish->new" if @_ % 2;
85 72         96 my %parms = @_;
86              
87 72         117 my $self = [ (undef) x totalFields ];
88 72         109 bless $self, $class;
89              
90 72 100       111 my $def = (exists $parms{default} ? $parms{default} : 1);
91              
92 72         83 $self->[textRefs] = undef;
93 72 100       102 $self->[fixDashes] = (exists $parms{dashes} ? $parms{dashes} : $def);
94 72 100       88 $self->[fixEllipses] = (exists $parms{ellipses} ? $parms{ellipses} : $def);
95 72 100       82 $self->[fixQuotes] = (exists $parms{quotes} ? $parms{quotes} : $def);
96              
97 72 50       101 $self->[fixHellip] = (exists $parms{hellip}
98             ? $parms{hellip} : $self->[fixEllipses]);
99 72 50       81 $self->[fixEllipseSpace] = (exists $parms{space_ellipses}
100             ? $parms{space_ellipses} : $self->[fixEllipses]);
101              
102 72         91 return $self;
103             } # end new
104              
105             #---------------------------------------------------------------------
106             # Convert quotes & apostrophes into curly quotes:
107             #
108             # Input:
109             # self: The HTML::Embellish object
110             # refs: Arrayref of stringrefs to the text of this paragraph
111              
112             sub processTextRefs
113             {
114 69     69 0 56 my ($self, $refs) = @_;
115              
116 69         74 local $_ = join('', map { $$_ } @$refs);
  70         277  
117 69         111 utf8::upgrade($_);
118              
119 69         53 my $fixQuotes = $self->[fixQuotes];
120 69 100       95 if ($fixQuotes) {
121 65         214 s/\("/($ldquo/g;
122 65         217 s/"\)/$rdquo)/g;
123              
124 65         233 s/^([\xA0\s]*)"/$1$ldquo/;
125 4     4   1913 s/(?<=[\s\pZ])"(?=[^\s\pZ])/$ldquo/g;
  4         28  
  4         38  
  65         249  
126 65         233 s/(?<=\pP)"(?=\w)/$ldquo/g;
127 65         216 s/(?<=[ \t\n\r])"(?=\xA0)/$ldquo/g;
128              
129 65         222 s/"[\xA0\s]*$/$rdquo/;
130 65         234 s/(?
131 65         223 s/(?<=\w)"(?=\pP)/$rdquo/g;
132 65         223 s/(?<=\xA0)"(?=[ \t\n\r]|[\s\xA0]+$)/$rdquo/g;
133 65         289 s/(?<=[,;.!?])"(?=[-$mdash])/$rdquo/go;
134              
135 65         508 s/'(?=
136             (?: angmans?
137             | aves?
138             | cause
139             | cept
140             | d
141             | e[mr]?e?
142             | fraidy?
143             | gainst
144             | igh\w*
145             | im
146             | m
147             | n
148             | nam
149             | nothers?
150             | nuff
151             | onou?rs?
152             | re?
153             | rithmetic
154             | s
155             | scuse
156             | spects?
157             | t
158             | til
159             | tisn?
160             | tw(?:asn?|ere?n?|ould\w*)
161             | ud
162             | uns?
163             ) \b
164             | \d\d\W?s
165             | \d\d(?!\w)
166             )
167             /$rsquo/igx;
168              
169 65         4120 s/'([ \xA0]?$rdquo)/$rsquo$1/go;
170              
171 65         220 s/`/$lsquo/g;
172 65         192 s/^'/$lsquo/;
173 65         236 s/(?<=[\s\pZ])'(?=[^\s\pZ])/$lsquo/g;
174 65         227 s/(?<=\pP)(?
175 65         223 s/(?<=[ \t\n\r])'(?=\xA0)/$lsquo/g;
176              
177 65         232 s/'/$rsquo/g;
178              
179 65         281 s/(?
180 65         519 s/(${rsquo}[\xA0\s]+)"(?!\PZ)/$1$rdquo/go;
181              
182 65 100       310 if (/"/) {
183 1   66     24 1 while s/^($balancedQuoteString (?![\"$ldquo$rdquo])[ \t\n\r\pP]) "
184             /$1$ldquo/xo
185             or s/^($balancedQuoteString $ldquo $notQuote*) "/$1$rdquo/xo;
186             } # end if straight quotes remaining in string
187              
188             #s/(?<=\p{IsPunct})"(?=\p{IsAlpha})/$ldquo/go;
189 65         1644 s/(?<=[[:punct:]])"(?=[[:alpha:]])/$ldquo/go;
190              
191 65         284 s/${ldquo}\s([$lsquo$rsquo])/$ldquo\xA0$1/go;
192 65         299 s/${rsquo}\s$rdquo/$rsquo\xA0$rdquo/go;
193             } # end if fixQuotes
194              
195 69 100       95 if ($self->[fixEllipses]) {
196 67         285 s/( [\"$ldquo$lsquo] \.(?:\xA0\.)+ ) \s /$1\xA0/xog;
197 67         1093 s/\s (?= \. (?:\xA0[.,!?])+ [$rdquo$rsquo\xA0\"]* $)/\xA0/xo;
198             }
199              
200             # Return the text to where it came from:
201             # This only works because the replacement text is always
202             # the same length as the original.
203 69         79 foreach my $r (@$refs) {
204 70         550 $$r = substr($_, 0, length($$r), '');
205 70 100       99 if ($fixQuotes) {
206             # Since the replacement text isn't the same length,
207             # these can't be done on the string as a whole:
208 66         1920 $$r =~ s/(?<=[$ldquo$rdquo])(?=[$lsquo$rsquo])/\xA0/go;
209 66         1860 $$r =~ s/(?<=[$lsquo$rsquo])(?=[$ldquo$rdquo])/\xA0/go;
210 66         351 $$r =~ s/(?<=[$ldquo$lsquo])\xA0(?=\.\xA0\.)//go;
211             } # end if fixQuotes
212             } # end foreach @$refs
213             } # end processTextRefs
214              
215             #---------------------------------------------------------------------
216             # Recursively process an HTML::Element tree:
217              
218             sub process
219             {
220 74     74 1 67 my ($self, $elt) = @_;
221              
222 74 100 66     396 croak "HTML::Embellish->process must be passed an HTML::Element"
223             unless ref $elt and $elt->can('content_refs_list');
224              
225 73 50       135 return if $elt->is_empty;
226              
227 73         327 my $parentRefs;
228 73         147 my $isP = ($elt->tag =~ /^(?: p | h\d | d[dt] | div | blockquote | title )$/x);
229              
230 73 100 100     603 if ($isP and ($self->[fixQuotes] or $self->[fixEllipses])) {
      66        
231 69         254 $parentRefs = $self->[textRefs];
232 69         83 $self->[textRefs] = []
233             } # end if need to collect text refs
234              
235 73         183 $elt->normalize_content;
236 73         1038 my @content = $elt->content_refs_list;
237              
238 73 50 66     499 if ($self->[fixQuotes] and $self->[textRefs] and @content) {
      66        
239             # A " that opens a tag can be assumed to be a left quote
240 66 100       49 ${$content[ 0]} =~ s/^"/$ldquo/ unless ref ${$content[ 0]};
  65         245  
  66         110  
241             # A " that ends a tag can be assumed to be a right quote
242 66 100       45 ${$content[-1]} =~ s/"$/$rdquo/ unless ref ${$content[-1]};
  65         226  
  66         92  
243             }
244              
245 73         75 foreach my $r (@content) {
246 74 100       89 if (ref $$r) { # element node
247 2         4 my $tag = $$r->tag;
248 2 50       10 next if $tag =~ /^(?: ~comment | script | style )$/x;
249              
250 2 50 33     8 if ($self->[textRefs] and $tag eq 'br') {
251 0         0 my $break = "\n";
252 0         0 push @{$self->[textRefs]}, \$break;
  0         0  
253             }
254 2         7 $self->process($$r);
255             } else { # text node
256             # Convert -- to em-dash:
257 72         126 utf8::upgrade($$r);
258 72 100       96 if ($self->[fixDashes]) {
259 64         247 $$r =~ s/(?
260 64         243 $$r =~ s/(?
261             } # end if fixDashes
262              
263 72 100       337 $$r =~ s/$hellip/.../go if $self->[fixHellip];
264              
265             # Fix ellipses:
266 72 100       98 if ($self->[fixEllipses]) {
267 67         256 $$r =~ s/(?
268 67         249 $$r =~ s/(?
269 67         2378 $$r =~ s/(?<= \.) [^\PZ\x{200B}] (?=[.,?!])/\xA0/gx;
270 67         405 $$r =~ s/(?:(?<=\w)|\A) (\.\xA0\.\xA0\.|\.\.\.)(?=[ \xA0\n\"\'?!$rsquo$rdquo])(?![ \xA0\n]+\w)/\xA0$1/go;
271             } # end if fixEllipses
272              
273 72 100       105 if ($self->[fixEllipseSpace]) {
274 67         261 $$r =~ s/(?<=\w) (\.(?:\xA0\.)+) (?=\w)/ $1 /gx;
275 67         238 $$r =~ s/(?<=\w[!?,;]) (\.(?:\xA0\.)+) (?=\w)/ $1 /gx;
276 67         291 $$r =~ s/( [\"$ldquo$lsquo] \.(?:\xA0\.)+ ) (?=\w) /$1\xA0/xog;
277 67         239 $$r =~ s/(?<=\w) (\.\xA0\.\xA0\.) (?![\xA0\w])/\xA0$1/gx;
278              
279 67 50 33     119 if ($self->[textRefs] and @{$self->[textRefs]}) {
  67         155  
280 0         0 $$r =~ s/^(\.(?:\xA0\.)+) (?=\w)/ $1 /gx
281 0 0       0 if ${$self->[textRefs][-1]} =~ /\w[!?,;]?$/;
282              
283 0 0       0 ${$self->[textRefs][-1]} =~ s/(?<=\w)\xA0(\.\xA0\.\xA0\.)$/ $1 /
  0         0  
284             if $$r =~ /^\w/;
285             }
286             } # end if fixEllipseSpace
287              
288 72 100       103 push @{$self->[textRefs]}, $r if $self->[textRefs];
  69         132  
289             } # end else text node
290             } # end foreach $r
291              
292 73 100 100     209 if ($isP and $self->[textRefs]) {
293             ### print LOG (map { utf8::is_utf8($$_) . "{$$_}" } @{ $self->[textRefs] }), "\n";
294 69         105 $self->processTextRefs($self->[textRefs]);
295 69 100       93 push @$parentRefs, @{$self->[textRefs]} if $parentRefs;
  1         1  
296 69         186 $self->[textRefs] = $parentRefs;
297             } # end if this was a paragraph-like element
298             } # end process
299              
300             #=====================================================================
301             # Package Return Value:
302              
303             1;
304              
305             __END__