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   284458 use 5.008; # Need good Unicode support; Perl 5.10 recommended but 5.8 may work
  4         17  
  4         176  
21 4     4   25 use warnings;
  4         9  
  4         132  
22 4     4   20 use strict;
  4         8  
  4         182  
23 4     4   22 use Carp qw(croak);
  4         13  
  4         331  
24              
25 4     4   22 use Exporter 5.57 'import'; # exported import method
  4         98  
  4         1271  
26              
27             ###open(LOG, '>:utf8', 'em.log');
28              
29             #=====================================================================
30             # Package Global Variables:
31              
32             our $VERSION = '1.000';
33             # This file is part of HTML-Embellish 1.000 (February 1, 2014)
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   9 my $i = 0;
56 4         11 for (qw(textRefs fixQuotes fixDashes fixEllipses fixEllipseSpace fixHellip
57             totalFields)) {
58             ## no critic (ProhibitStringyEval)
59 28         1116 eval "sub $_ () { $i }";
60 28         2462 ++$i;
61             }
62             } # end BEGIN
63              
64             #=====================================================================
65             # Exported functions:
66             #---------------------------------------------------------------------
67             sub embellish
68             {
69 64     64 1 84305 my $html = shift @_;
70              
71 64 100 66     681 croak "First parameter of embellish must be an HTML::Element"
72             unless ref $html and $html->can('content_refs_list');
73              
74 63         200 my $e = HTML::Embellish->new(@_);
75 62         172 $e->process($html);
76             } # end embellish
77              
78             #=====================================================================
79             # Class Methods:
80             #---------------------------------------------------------------------
81             sub new
82             {
83 64     64 1 603 my $class = shift;
84 64 100       275 croak "Odd number of parameters passed to HTML::Embellish->new" if @_ % 2;
85 63         136 my %parms = @_;
86              
87 63         214 my $self = [ (undef) x totalFields ];
88 63         144 bless $self, $class;
89              
90 63 100       153 my $def = (exists $parms{default} ? $parms{default} : 1);
91              
92 63         183 $self->[textRefs] = undef;
93 63 100       147 $self->[fixDashes] = (exists $parms{dashes} ? $parms{dashes} : $def);
94 63 100       129 $self->[fixEllipses] = (exists $parms{ellipses} ? $parms{ellipses} : $def);
95 63 100       141 $self->[fixQuotes] = (exists $parms{quotes} ? $parms{quotes} : $def);
96              
97 63 50       139 $self->[fixHellip] = (exists $parms{hellip}
98             ? $parms{hellip} : $self->[fixEllipses]);
99 63 50       124 $self->[fixEllipseSpace] = (exists $parms{space_ellipses}
100             ? $parms{space_ellipses} : $self->[fixEllipses]);
101              
102 63         134 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 60     60 0 97 my ($self, $refs) = @_;
115              
116 60         98 local $_ = join('', map { $$_ } @$refs);
  61         230  
117 60         114 utf8::upgrade($_);
118              
119 60         126 my $fixQuotes = $self->[fixQuotes];
120 60 100       134 if ($fixQuotes) {
121 56         329 s/\("/($ldquo/g;
122 56         306 s/"\)/$rdquo)/g;
123              
124 56         358 s/^([\xA0\s]*)"/$1$ldquo/;
125 4     4   5692 s/(?<=[\s\pZ])"(?=[^\s\pZ])/$ldquo/g;
  4         48  
  4         61  
  56         407  
126 56         357 s/(?<=\pP)"(?=\w)/$ldquo/g;
127 56         336 s/(?<=[ \t\n\r])"(?=\xA0)/$ldquo/g;
128              
129 56         360 s/"[\xA0\s]*$/$rdquo/;
130 56         372 s/(?
131 56         358 s/(?<=\w)"(?=\pP)/$rdquo/g;
132 56         339 s/(?<=\xA0)"(?=[ \t\n\r]|[\s\xA0]+$)/$rdquo/g;
133 56         430 s/(?<=[,;.!?])"(?=[-$mdash])/$rdquo/go;
134              
135 56         693 s/'(?=(?:cause|cept|d|e[mr]?e?|fraidy?|gainst|im|m|n|nam|nothers?|nuff|re?|rithmetic|s|scuse|spects?|t|til|tisn?|tw(?:asn?|ere?n?|ould\w*)|ud|uns?)\b|\d\d\W?s|\d\d(?!\w))/$rsquo/ig;
136              
137 56         7625 s/'([ \xA0]?$rdquo)/$rsquo$1/go;
138              
139 56         396 s/`/$lsquo/g;
140 56         286 s/^'/$lsquo/;
141 56         370 s/(?<=[\s\pZ])'(?=[^\s\pZ])/$lsquo/g;
142 56         339 s/(?<=\pP)(?
143 56         492 s/(?<=[ \t\n\r])'(?=\xA0)/$lsquo/g;
144              
145 56         342 s/'/$rsquo/g;
146              
147 56         390 s/(?
148 56         758 s/(${rsquo}[\xA0\s]+)"(?!\PZ)/$1$rdquo/go;
149              
150 56 100       588 if (/"/) {
151 1   66     42 1 while s/^($balancedQuoteString (?![\"$ldquo$rdquo])[ \t\n\r\pP]) "
152             /$1$ldquo/xo
153             or s/^($balancedQuoteString $ldquo $notQuote*) "/$1$rdquo/xo;
154             } # end if straight quotes remaining in string
155              
156             #s/(?<=\p{IsPunct})"(?=\p{IsAlpha})/$ldquo/go;
157 56         3506 s/(?<=[[:punct:]])"(?=[[:alpha:]])/$ldquo/go;
158              
159 56         448 s/${ldquo}\s([$lsquo$rsquo])/$ldquo\xA0$1/go;
160 56         421 s/${rsquo}\s$rdquo/$rsquo\xA0$rdquo/go;
161             } # end if fixQuotes
162              
163 60 100       141 if ($self->[fixEllipses]) {
164 58         413 s/( [\"$ldquo$lsquo] \.(?:\xA0\.)+ ) \s /$1\xA0/xog;
165 58         1791 s/\s (?= \. (?:\xA0[.,!?])+ [$rdquo$rsquo\xA0\"]* $)/\xA0/xo;
166             }
167              
168             # Return the text to where it came from:
169             # This only works because the replacement text is always
170             # the same length as the original.
171 60         109 foreach my $r (@$refs) {
172 61         808 $$r = substr($_, 0, length($$r), '');
173 61 100       135 if ($fixQuotes) {
174             # Since the replacement text isn't the same length,
175             # these can't be done on the string as a whole:
176 57         3414 $$r =~ s/(?<=[$ldquo$rdquo])(?=[$lsquo$rsquo])/\xA0/go;
177 57         3470 $$r =~ s/(?<=[$lsquo$rsquo])(?=[$ldquo$rdquo])/\xA0/go;
178 57         564 $$r =~ s/(?<=[$ldquo$lsquo])\xA0(?=\.\xA0\.)//go;
179             } # end if fixQuotes
180             } # end foreach @$refs
181             } # end processTextRefs
182              
183             #---------------------------------------------------------------------
184             # Recursively process an HTML::Element tree:
185              
186             sub process
187             {
188 65     65 1 95 my ($self, $elt) = @_;
189              
190 65 100 66     481 croak "HTML::Embellish->process must be passed an HTML::Element"
191             unless ref $elt and $elt->can('content_refs_list');
192              
193 64 50       232 return if $elt->is_empty;
194              
195 64         459 my $parentRefs;
196 64         179 my $isP = ($elt->tag =~ /^(?: p | h\d | d[dt] | div | blockquote | title )$/x);
197              
198 64 100 100     774 if ($isP and ($self->[fixQuotes] or $self->[fixEllipses])) {
      66        
199 60         81 $parentRefs = $self->[textRefs];
200 60         126 $self->[textRefs] = []
201             } # end if need to collect text refs
202              
203 64         252 $elt->normalize_content;
204 64         1685 my @content = $elt->content_refs_list;
205              
206 64 50 66     680 if ($self->[fixQuotes] and $self->[textRefs] and @content) {
      66        
207             # A " that opens a tag can be assumed to be a left quote
208 57 100       74 ${$content[ 0]} =~ s/^"/$ldquo/ unless ref ${$content[ 0]};
  56         308  
  57         137  
209             # A " that ends a tag can be assumed to be a right quote
210 57 100       71 ${$content[-1]} =~ s/"$/$rdquo/ unless ref ${$content[-1]};
  56         323  
  57         129  
211             }
212              
213 64         110 foreach my $r (@content) {
214 65 100       123 if (ref $$r) { # element node
215 2         8 my $tag = $$r->tag;
216 2 50       20 next if $tag =~ /^(?: ~comment | script | style )$/x;
217              
218 2 50 33     14 if ($self->[textRefs] and $tag eq 'br') {
219 0         0 my $break = "\n";
220 0         0 push @{$self->[textRefs]}, \$break;
  0         0  
221             }
222 2         14 $self->process($$r);
223             } else { # text node
224             # Convert -- to em-dash:
225 63         153 utf8::upgrade($$r);
226 63 100       143 if ($self->[fixDashes]) {
227 55         386 $$r =~ s/(?
228 55         318 $$r =~ s/(?
229             } # end if fixDashes
230              
231 63 100       606 $$r =~ s/$hellip/.../go if $self->[fixHellip];
232              
233             # Fix ellipses:
234 63 100       121 if ($self->[fixEllipses]) {
235 58         366 $$r =~ s/(?
236 58         342 $$r =~ s/(?
237 58         4164 $$r =~ s/(?<= \.) [^\PZ\x{200B}] (?=[.,?!])/\xA0/gx;
238 58         623 $$r =~ s/(?:(?<=\w)|\A) (\.\xA0\.\xA0\.|\.\.\.)(?=[ \xA0\n\"\'?!$rsquo$rdquo])(?![ \xA0\n]+\w)/\xA0$1/go;
239             } # end if fixEllipses
240              
241 63 100       137 if ($self->[fixEllipseSpace]) {
242 58         351 $$r =~ s/(?<=\w) (\.(?:\xA0\.)+) (?=\w)/ $1 /gx;
243 58         349 $$r =~ s/(?<=\w[!?,;]) (\.(?:\xA0\.)+) (?=\w)/ $1 /gx;
244 58         421 $$r =~ s/( [\"$ldquo$lsquo] \.(?:\xA0\.)+ ) (?=\w) /$1\xA0/xog;
245 58         333 $$r =~ s/(?<=\w) (\.\xA0\.\xA0\.) (?![\xA0\w])/\xA0$1/gx;
246              
247 58 50 33     156 if ($self->[textRefs] and @{$self->[textRefs]}) {
  58         206  
248 0         0 $$r =~ s/^(\.(?:\xA0\.)+) (?=\w)/ $1 /gx
249 0 0       0 if ${$self->[textRefs][-1]} =~ /\w[!?,;]?$/;
250              
251 0 0       0 ${$self->[textRefs][-1]} =~ s/(?<=\w)\xA0(\.\xA0\.\xA0\.)$/ $1 /
  0         0  
252             if $$r =~ /^\w/;
253             }
254             } # end if fixEllipseSpace
255              
256 63 100       154 push @{$self->[textRefs]}, $r if $self->[textRefs];
  60         188  
257             } # end else text node
258             } # end foreach $r
259              
260 64 100 100     304 if ($isP and $self->[textRefs]) {
261             ### print LOG (map { utf8::is_utf8($$_) . "{$$_}" } @{ $self->[textRefs] }), "\n";
262 60         152 $self->processTextRefs($self->[textRefs]);
263 60 100       132 push @$parentRefs, @{$self->[textRefs]} if $parentRefs;
  1         2  
264 60         276 $self->[textRefs] = $parentRefs;
265             } # end if this was a paragraph-like element
266             } # end process
267              
268             #=====================================================================
269             # Package Return Value:
270              
271             1;
272              
273             __END__