File Coverage

blib/lib/HTML/Embellish.pm
Criterion Covered Total %
statement 126 133 94.7
branch 51 62 82.2
condition 17 30 56.6
subroutine 11 11 100.0
pod 3 4 75.0
total 208 240 86.6


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   246080 use 5.008; # Need good Unicode support; Perl 5.10 recommended but 5.8 may work
  4         15  
21 4     4   20 use warnings;
  4         8  
  4         124  
22 4     4   20 use strict;
  4         6  
  4         93  
23 4     4   19 use Carp qw(croak);
  4         8  
  4         290  
24              
25 4     4   20 use Exporter 5.57 'import'; # exported import method
  4         68  
  4         754  
26              
27             ###open(LOG, '>:utf8', 'em.log');
28              
29             #=====================================================================
30             # Package Global Variables:
31              
32             our $VERSION = '1.002';
33             # This file is part of HTML-Embellish 1.002 (January 9, 2016)
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   11 my $i = 0;
56 4         14 for (qw(textRefs fixQuotes fixDashes fixEllipses fixEllipseSpace fixHellip
57             totalFields)) {
58             ## no critic (ProhibitStringyEval)
59 28         971 eval "sub $_ () { $i }";
60 28         2066 ++$i;
61             }
62             } # end BEGIN
63              
64             #=====================================================================
65             # Exported functions:
66             #---------------------------------------------------------------------
67             sub embellish
68             {
69 75     75 1 66284 my $html = shift @_;
70              
71 75 100 66     745 croak "First parameter of embellish must be an HTML::Element"
72             unless ref $html and $html->can('content_refs_list');
73              
74 74         216 my $e = HTML::Embellish->new(@_);
75 73         187 $e->process($html);
76             } # end embellish
77              
78             #=====================================================================
79             # Class Methods:
80             #---------------------------------------------------------------------
81             sub new
82             {
83 75     75 1 670 my $class = shift;
84 75 100       312 croak "Odd number of parameters passed to HTML::Embellish->new" if @_ % 2;
85 74         146 my %parms = @_;
86              
87 74         202 my $self = [ (undef) x totalFields ];
88 74         135 bless $self, $class;
89              
90 74 100       167 my $def = (exists $parms{default} ? $parms{default} : 1);
91              
92 74         109 $self->[textRefs] = undef;
93 74 100       162 $self->[fixDashes] = (exists $parms{dashes} ? $parms{dashes} : $def);
94 74 100       158 $self->[fixEllipses] = (exists $parms{ellipses} ? $parms{ellipses} : $def);
95 74 100       152 $self->[fixQuotes] = (exists $parms{quotes} ? $parms{quotes} : $def);
96              
97             $self->[fixHellip] = (exists $parms{hellip}
98 74 50       141 ? $parms{hellip} : $self->[fixEllipses]);
99             $self->[fixEllipseSpace] = (exists $parms{space_ellipses}
100 74 50       157 ? $parms{space_ellipses} : $self->[fixEllipses]);
101              
102 74         153 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 71     71 0 110 my ($self, $refs) = @_;
115              
116 71         122 local $_ = join('', map { $$_ } @$refs);
  72         276  
117 71         141 utf8::upgrade($_);
118              
119 71         104 my $fixQuotes = $self->[fixQuotes];
120 71 100       158 if ($fixQuotes) {
121 67         378 s/\("/($ldquo/g;
122 67         362 s/"\)/$rdquo)/g;
123              
124 67         367 s/^([\xA0\s]*)"/$1$ldquo/;
125 4     4   3593 s/(?<=[\s\pZ])"(?=[^\s\pZ])/$ldquo/g;
  4         41  
  4         57  
  67         389  
126 67         384 s/(?<=\pP)"(?=\w)/$ldquo/g;
127 67         365 s/(?<=[ \t\n\r])"(?=\xA0)/$ldquo/g;
128              
129 67         368 s/"[\xA0\s]*$/$rdquo/;
130 67         362 s/(?
131 67         361 s/(?<=\w)"(?=\pP)/$rdquo/g;
132 67         368 s/(?<=\xA0)"(?=[ \t\n\r]|[\s\xA0]+$)/$rdquo/g;
133 67         494 s/(?<=[,;.!?])"(?=[-$mdash])/$rdquo/go;
134              
135 67         686 s/'(?=
136             (?: angmans?
137             | aves?
138             | ay
139             | cause
140             | cept
141             | d
142             | e[mr]?e?
143             | fraidy?
144             | gainst
145             | igh\w*
146             | im
147             | m
148             | n
149             | nam
150             | nothers?
151             | nuff
152             | onou?rs?
153             | re?
154             | rithmetic
155             | s
156             | scuse
157             | spects?
158             | t
159             | til
160             | tisn?
161             | tw(?:asn?|ere?n?|ould\w*)
162             | ud
163             | uns?
164             | y
165             ) \b
166             | \d\d\W?s
167             | \d\d(?!\w)
168             )
169             /$rsquo/igx;
170              
171 67         7397 s/'([ \xA0]?$rdquo)/$rsquo$1/go;
172              
173 67         363 s/`/$lsquo/g;
174 67         296 s/^'/$lsquo/;
175 67         379 s/(?<=[\s\pZ])'(?=[^\s\pZ])/$lsquo/g;
176 67         378 s/(?<=\pP)(?
177 67         358 s/(?<=[ \t\n\r])'(?=\xA0)/$lsquo/g;
178              
179 67         363 s/'/$rsquo/g;
180              
181 67         544 s/(?
182 67         818 s/(${rsquo}[\xA0\s]+)"(?!\PZ)/$1$rdquo/go;
183              
184 67 100       439 if (/"/) {
185 1   66     42 1 while s/^($balancedQuoteString (?![\"$ldquo$rdquo])[ \t\n\r\pP]) "
186             /$1$ldquo/xo
187             or s/^($balancedQuoteString $ldquo $notQuote*) "/$1$rdquo/xo;
188             } # end if straight quotes remaining in string
189              
190             #s/(?<=\p{IsPunct})"(?=\p{IsAlpha})/$ldquo/go;
191 67         3010 s/(?<=[[:punct:]])"(?=[[:alpha:]])/$ldquo/go;
192              
193 67         427 s/${ldquo}\s([$lsquo$rsquo])/$ldquo\xA0$1/go;
194 67         472 s/${rsquo}\s$rdquo/$rsquo\xA0$rdquo/go;
195             } # end if fixQuotes
196              
197 71 100       161 if ($self->[fixEllipses]) {
198 69         428 s/( [\"$ldquo$lsquo] \.(?:\xA0\.)+ ) \s /$1\xA0/xog;
199 69         1741 s/\s (?= \. (?:\xA0[.,!?])+ [$rdquo$rsquo\xA0\"]* $)/\xA0/xo;
200             }
201              
202             # Return the text to where it came from:
203             # This only works because the replacement text is always
204             # the same length as the original.
205 71         133 foreach my $r (@$refs) {
206 72         814 $$r = substr($_, 0, length($$r), '');
207 72 100       158 if ($fixQuotes) {
208             # Since the replacement text isn't the same length,
209             # these can't be done on the string as a whole:
210 68         7026 $$r =~ s/(?<=[$ldquo$rdquo])(?=[$lsquo$rsquo])/\xA0/go;
211 68         7022 $$r =~ s/(?<=[$lsquo$rsquo])(?=[$ldquo$rdquo])/\xA0/go;
212 68         531 $$r =~ s/(?<=[$ldquo$lsquo])\xA0(?=\.\xA0\.)//go;
213             } # end if fixQuotes
214             } # end foreach @$refs
215             } # end processTextRefs
216              
217             #---------------------------------------------------------------------
218             # Recursively process an HTML::Element tree:
219              
220             sub process
221             {
222 76     76 1 113 my ($self, $elt) = @_;
223              
224 76 100 66     531 croak "HTML::Embellish->process must be passed an HTML::Element"
225             unless ref $elt and $elt->can('content_refs_list');
226              
227 75 50       228 return if $elt->is_empty;
228              
229 75         515 my $parentRefs;
230 75         199 my $isP = ($elt->tag =~ /^(?: p | h\d | d[dt] | div | blockquote | title )$/x);
231              
232 75 100 66     841 if ($isP and ($self->[fixQuotes] or $self->[fixEllipses])) {
      66        
233 71         117 $parentRefs = $self->[textRefs];
234 71         126 $self->[textRefs] = []
235             } # end if need to collect text refs
236              
237 75         216 $elt->normalize_content;
238 75         1630 my @content = $elt->content_refs_list;
239              
240 75 50 66     743 if ($self->[fixQuotes] and $self->[textRefs] and @content) {
      33        
241             # A " that opens a tag can be assumed to be a left quote
242 68 100       75 ${$content[ 0]} =~ s/^"/$ldquo/ unless ref ${$content[ 0]};
  67         341  
  68         163  
243             # A " that ends a tag can be assumed to be a right quote
244 68 100       93 ${$content[-1]} =~ s/"$/$rdquo/ unless ref ${$content[-1]};
  67         353  
  68         155  
245             }
246              
247 75         123 foreach my $r (@content) {
248 76 100       163 if (ref $$r) { # element node
249 2         7 my $tag = $$r->tag;
250 2 50       15 next if $tag =~ /^(?: ~comment | script | style )$/x;
251              
252 2 50 33     13 if ($self->[textRefs] and $tag eq 'br') {
253 0         0 my $break = "\n";
254 0         0 push @{$self->[textRefs]}, \$break;
  0         0  
255             }
256 2         10 $self->process($$r);
257             } else { # text node
258             # Convert -- to em-dash:
259 74         166 utf8::upgrade($$r);
260 74 100       172 if ($self->[fixDashes]) {
261 66         466 $$r =~ s/(?
262 66         378 $$r =~ s/(?
263             } # end if fixDashes
264              
265 74 100       500 $$r =~ s/$hellip/.../go if $self->[fixHellip];
266              
267             # Fix ellipses:
268 74 100       181 if ($self->[fixEllipses]) {
269 69         384 $$r =~ s/(?
270 69         375 $$r =~ s/(?
271 69         3402 $$r =~ s/(?<= \.) [^\PZ\x{200B}] (?=[.,?!])/\xA0/gx;
272 69         597 $$r =~ s/(?:(?<=\w)|\A) (\.\xA0\.\xA0\.|\.\.\.)(?=[ \xA0\n\"\'?!$rsquo$rdquo])(?![ \xA0\n]+\w)/\xA0$1/go;
273             } # end if fixEllipses
274              
275 74 100       177 if ($self->[fixEllipseSpace]) {
276 69         405 $$r =~ s/(?<=\w) (\.(?:\xA0\.)+) (?=\w)/ $1 /gx;
277 69         374 $$r =~ s/(?<=\w[!?,;]) (\.(?:\xA0\.)+) (?=\w)/ $1 /gx;
278 69         437 $$r =~ s/( [\"$ldquo$lsquo] \.(?:\xA0\.)+ ) (?=\w) /$1\xA0/xog;
279 69         359 $$r =~ s/(?<=\w) (\.\xA0\.\xA0\.) (?![\xA0\w])/\xA0$1/gx;
280              
281 69 50 33     185 if ($self->[textRefs] and @{$self->[textRefs]}) {
  69         288  
282             $$r =~ s/^(\.(?:\xA0\.)+) (?=\w)/ $1 /gx
283 0 0       0 if ${$self->[textRefs][-1]} =~ /\w[!?,;]?$/;
  0         0  
284              
285 0 0       0 ${$self->[textRefs][-1]} =~ s/(?<=\w)\xA0(\.\xA0\.\xA0\.)$/ $1 /
  0         0  
286             if $$r =~ /^\w/;
287             }
288             } # end if fixEllipseSpace
289              
290 74 100       177 push @{$self->[textRefs]}, $r if $self->[textRefs];
  71         220  
291             } # end else text node
292             } # end foreach $r
293              
294 75 100 66     301 if ($isP and $self->[textRefs]) {
295             ### print LOG (map { utf8::is_utf8($$_) . "{$$_}" } @{ $self->[textRefs] }), "\n";
296 71         179 $self->processTextRefs($self->[textRefs]);
297 71 100       162 push @$parentRefs, @{$self->[textRefs]} if $parentRefs;
  1         2  
298 71         285 $self->[textRefs] = $parentRefs;
299             } # end if this was a paragraph-like element
300             } # end process
301              
302             #=====================================================================
303             # Package Return Value:
304              
305             1;
306              
307             __END__