File Coverage

blib/lib/Text/Autoformat.pm
Criterion Covered Total %
statement 191 322 59.3
branch 84 192 43.7
condition 37 95 38.9
subroutine 19 23 82.6
pod 0 7 0.0
total 331 639 51.8


line stmt bran cond sub pod time code
1             package Text::Autoformat;
2             $Text::Autoformat::VERSION = '1.73';
3 4     4   64332 use 5.006;
  4         15  
4 4     4   19 use strict;
  4         7  
  4         123  
5 4     4   20 use warnings;
  4         11  
  4         115  
6 4     4   19 use Carp;
  4         5  
  4         404  
7              
8             require Exporter;
9              
10 4     4   4044 use Text::Reform qw( form tag break_at break_with break_wrap break_TeX );
  4         30070  
  4         28  
11 4     4   3195 use Text::Autoformat::Hang;
  4         11  
  4         135  
12 4     4   2252 use Text::Autoformat::NullHang;
  4         10  
  4         2690  
13              
14             our @ISA = qw(Exporter);
15             our @EXPORT = qw( autoformat );
16             our @EXPORT_OK =
17             qw( form tag break_at break_with break_wrap break_TeX ignore_headers );
18              
19              
20             my %std_highlight_ignore = map {$_=>1} qw {
21             a an at as and are
22             but by
23             ere
24             for from
25             in into is
26             of on onto or over
27             per
28             the to that than
29             until unto upon
30             via
31             with while whilst within without
32             };
33              
34             my $STD_HIGHLIGHT_IGNORES = sub {
35             my ($word) = @_;
36             return $std_highlight_ignore{lc $word} ? recase($word,'lower') : recase($word,'title');
37             };
38              
39             my @entities = qw {
40             Á á Â â Æ æ
41             À à Α α Ã ã
42             Ä ä Β β Ç ç
43             Χ χ Δ δ É é
44             Ê ê È è Ε ε
45             Η η Ð ð Ë ë
46             Γ γ Í í Î î
47             Ì ì Ι ι Ï ï
48             Κ κ Λ λ Μ μ
49             Ñ ñ Ν ν Ó ó
50             Ô ô Œ œ Ò ò
51             Ω ω Ο ο Õ õ
52             Ö ö Φ φ Π π
53             ″ ′ Ψ ψ Ρ ρ
54             Š š Σ σ Τ τ
55             Θ θ Þ þ Ú ú
56             Û û Ù ù Υ υ
57             Ü ü Ξ ξ Ý ý
58             Ÿ ÿ Ζ ζ
59             };
60              
61             my %lower_entities = @entities;
62             my %upper_entities = reverse @entities;
63              
64             my %casing = (
65             lower => [ \%lower_entities, \%lower_entities,
66             sub { $_ = lc }, sub { $_ = lc } ],
67             upper => [ \%upper_entities, \%upper_entities,
68             sub { $_ = uc }, sub { $_ = uc } ],
69             title => [ \%upper_entities, \%lower_entities,
70             sub { $_ = ucfirst lc }, sub { $_ = lc } ],
71             );
72              
73             my $default_margin = 72;
74             my $default_widow = 10;
75              
76             $Text::Autoformat::widow_slack = 0.1;
77              
78              
79             sub defn($)
80             {
81 130 50   130 0 541 return $_[0] if defined $_[0];
82 0         0 return "";
83             }
84              
85             my $ignore_headers = qr/
86             \A
87             (?: From \b .* $)?
88             (?: [^:\n]+ : .* \n
89             (?: [ \t] .* \n)*
90             )+
91             \s*
92             \Z
93             /mx;
94             my $ignore_indent = qr/^[^\S\n].*(\n[^\S\n].*)*$/;
95              
96 6 100   6 0 43 sub ignore_headers { $_[0] && /$ignore_headers/ }
97              
98             # BITS OF A TEXT LINE
99              
100             my $quotechar = qq{[!#%=|:;]};
101             my $quotechunk = qq{(?:$quotechar(?![a-z])|(?:[a-z]\\w*)?>+)};
102             my $QUOTER = qq{(?:(?i)(?:$quotechunk(?:[ \\t]*$quotechunk)*))};
103              
104             my $separator = q/(?:[-_]{2,}|[=#*]{3,}|[+~]{4,})/;
105              
106 4     4   6626 use overload;
  4         4815  
  4         29  
107             sub autoformat # ($text, %args)
108             {
109 4     4 0 917 my ($text,%args,$toSTDOUT);
110              
111 4         13 foreach ( @_ )
112             {
113 7 100 33     51 if (ref eq 'HASH')
    50 33        
114 3         17 { %args = (%args, %$_) }
115             elsif (!defined($text) && !ref || overload::Method($_,'""'))
116 4         13 { $text = "$_" }
117             else {
118 0         0 croak q{Usage: autoformat([text],[{options}])}
119             }
120             }
121              
122 4 50       20 unless (defined $text) {
123 0         0 $text = join("",);
124 0         0 $toSTDOUT = !defined wantarray();
125             }
126              
127 4 100       23 return $text unless $text =~ /\S/;
128              
129 3 100       12 $args{right} = $default_margin unless exists $args{right};
130 3 50       13 $args{justify} = "" unless exists $args{justify};
131 3 50 50     23 $args{widow} = 0 if ($args{justify}||"") =~ /full/;
132 3 50       12 $args{widow} = $default_widow unless exists $args{widow};
133 3 50       12 $args{case} = '' unless exists $args{case};
134 3 100       11 $args{lists} = 1 unless exists $args{lists};
135 3 50       11 $args{squeeze} = 1 unless exists $args{squeeze};
136 3 50       12 $args{gap} = 0 unless exists $args{gap};
137 3 50       22 $args{break} = break_at('-') unless exists $args{break};
138 3         47 $args{impfill} = ! exists $args{fill};
139 3         7 $args{expfill} = $args{fill};
140 3 50       13 $args{tabspace} = 8 unless exists $args{tabspace};
141 3 50       10 $args{renumber} = 1 unless exists $args{renumber};
142 3 50       12 $args{autocentre} = 1 unless exists $args{autocentre};
143 3 50       10 $args{_centred} = 1 if $args{justify} =~ /cent(er(ed)?|red?)/;
144 3   100     20 $args{all} ||= $args{mail};
145              
146             # SPECIAL IGNORANCE...
147 3 100       7 if ($args{ignore}) {
148 2         4 $args{all} = 1;
149 2         6 $args{ignore} = _build_ignore( $args{ignore} );
150             }
151             else {
152 1     1   5 $args{ignore} = sub{0};
  1         2  
153             }
154              
155 3 100       10 if ( $args{mail} ) {
156 1         2 my $ignore = $args{ignore};
157 1 100   7   4 $args{ignore} = sub { $ignore->(@_) || ignore_headers(@_) };
  7         16  
158             }
159            
160             # DETABIFY
161 3         27 my @rawlines = split /\n/, $text;
162 4     4   5660 use Text::Tabs; $tabstop = $args{tabspace};
  4         3249  
  4         16654  
  3         8  
163 3         15 @rawlines = expand(@rawlines);
164              
165             # HANDLE QUOTING CHANGE
166 3 50       189 my $quoter = exists $args{quoter} ? $args{quoter} : $QUOTER;
167 3         9 $quoter =~ s//$QUOTER/g;
168              
169             # PARSE EACH LINE
170              
171 3         4 my $pre = 0;
172 3         5 my @lines;
173 3         8 foreach (@rawlines)
174             {
175 26         78 push @lines, { raw => $_ };
176 26 50       453 s/\A([ \t]*)($quoter?)([ \t]*)//
177             or die "Internal Error ($@) on '$_'";
178 26         65 $lines[-1]{presig} = $lines[-1]{prespace} = defn $1;
179 26         57 $lines[-1]{presig} .= $lines[-1]{quoter} = defn $2;
180 26         54 $lines[-1]{presig} .= $lines[-1]{quotespace} = defn $3;
181              
182 26         106 $lines[-1]{hang} = Text::Autoformat::Hang->new($_, $args{lists});
183              
184 26 50       234 s/([ \t]*)(.*?)(\s*)$//
185             or die "Internal Error ($@) on '$_'";
186 26         90 $lines[-1]{hangspace} = defn $1;
187 26         47 $lines[-1]{text} = defn $2;
188 26   66     85 $lines[-1]{empty} = $lines[-1]{hang}->empty() && $2 !~ /\S/;
189 26         190 $lines[-1]{separator} = $lines[-1]{text} =~ /^$separator$/;
190             }
191              
192             # SUBDIVIDE DOCUMENT INTO COHERENT SUBSECTIONS
193              
194 3         6 my @chunks;
195 3         8 push @chunks, [shift @lines];
196 3         8 foreach my $line (@lines)
197             {
198 23 100 33     179 if ($line->{separator} ||
      33        
      33        
      66        
199             $line->{quoter} ne $chunks[-1][-1]->{quoter} ||
200             $line->{empty} ||
201             @chunks && $chunks[-1][-1]->{empty})
202             {
203 10         24 push @chunks, [$line];
204             }
205             else
206             {
207 13         16 push @{$chunks[-1]}, $line;
  13         30  
208             }
209             }
210              
211              
212              
213             # DETECT CENTRED PARAS
214              
215 3         7 CHUNK: foreach my $chunk ( @chunks )
216             {
217 13 50 66     88 next CHUNK if !$args{autocentre} || @$chunk < 2 || $chunk->[0]{hang};
      66        
218 0         0 my @length;
219 0         0 my $ave = 0;
220 0         0 foreach my $line (@$chunk)
221             {
222             my $prespace = $line->{quoter} ? $line->{quotespace}
223 0 0       0 : $line->{prespace};
224             my $pagewidth =
225 0         0 2*length($prespace) + length($line->{text});
226 0         0 push @length, [length $prespace,$pagewidth];
227 0         0 $ave += $pagewidth;
228             }
229 0         0 $ave /= @length;
230 0         0 my $diffpre = 0;
231 0         0 foreach my $l (0..$#length)
232             {
233 0 0       0 next CHUNK unless abs($length[$l][1]-$ave) <= 2;
234 0 0 0     0 $diffpre ||= $length[$l-1][0] != $length[$l][0]
235             if $l > 0;
236             }
237 0 0       0 next CHUNK unless $diffpre;
238 0         0 foreach my $line (@$chunk)
239             {
240 0         0 $line->{centred} = 1;
241             ($line->{quoter} ? $line->{quotespace}
242 0 0       0 : $line->{prespace}) = "";
243             }
244             }
245              
246             # REDIVIDE INTO PARAGRAPHS
247              
248 3         4 my @paras;
249 3         7 foreach my $chunk ( @chunks )
250             {
251 13         16 my $first = 1;
252 13         13 my $firstfrom;
253 13         16 foreach my $line ( @{$chunk} )
  13         24  
254             {
255 26 100 66     145 if ($first ||
      66        
      33        
256             $line->{quoter} ne $paras[-1]->{quoter} ||
257             $paras[-1]->{separator} ||
258             !$line->{hang}->empty
259             )
260             {
261 13         19 push @paras, $line;
262 13         15 $first = 0;
263 13         28 $firstfrom = length($line->{raw}) - length($line->{text});
264             }
265             else
266             {
267 13         26 my $extraspace = length($line->{raw}) - length($line->{text}) - $firstfrom;
268 13         165 $paras[-1]->{text} .= "\n" . q{ }x$extraspace . $line->{text};
269 13         50 $paras[-1]->{raw} .= "\n" . $line->{raw};
270             }
271             }
272             }
273              
274             # SELECT PARAS TO HANDLE
275              
276 3         6 my $remainder = "";
277 3 50       10 if ($args{all}) { # STOP AT MAIL TERMINATOR IF $args{mail}
278 3         5 my $lastignored = 1;
279 3         18 for my $index (0..$#paras) {
280 13         64 local $_ = $paras[$index]{raw} . "\n";
281 13         28 $paras[$index]{ignore} = $args{ignore}($lastignored);
282 13   100     43 $lastignored &&= $paras[$index]{ignore};
283 13 50 66     70 next unless $args{mail} && /^--\s*$/;
284 0         0 $remainder = join "\n", map { $_->{raw} } splice @paras, $index;
  0         0  
285 0 0       0 $remainder .= "\n" unless $remainder =~ /\n\z/;
286 0         0 last;
287             }
288             }
289             else { # JUST THE FIRST PARA
290 0         0 $remainder = join "\n", map { $_->{raw} } @paras[1..$#paras];
  0         0  
291 0 0       0 $remainder .= "\n" unless $remainder =~ /\n\z/;
292 0         0 @paras = ( $paras[0] );
293             }
294              
295             # RE-CASE TEXT
296 3 50       12 if ($args{case}) {
297 0         0 foreach my $para ( @paras ) {
298 0 0       0 next if $para->{ignore};
299 0 0       0 if (ref $args{case} eq 'CODE') {
    0          
    0          
    0          
    0          
    0          
300 0         0 $para->{text} = entitle($para->{text}, $args{case});
301             }
302             elsif ($args{case} =~ /upper/i) {
303 0         0 $para->{text} = recase($para->{text}, 'upper');
304             }
305             elsif ($args{case} =~ /lower/i) {
306 0         0 $para->{text} = recase($para->{text}, 'lower');
307             }
308             elsif ($args{case} =~ /title/i) {
309 0         0 $para->{text} = entitle($para->{text}, 0);
310             }
311             elsif ($args{case} =~ /highlight/i) {
312 0         0 $para->{text} = entitle($para->{text}, $STD_HIGHLIGHT_IGNORES);
313             }
314             elsif ($args{case} =~ /sentence(\s*)/i) {
315 0         0 my $trailer = $1;
316 0 0 0     0 $args{squeeze}=0 if $trailer && $trailer ne " ";
317 0         0 ensentence();
318 0         0 $para->{text} =~ s/(\S+(\s+|$))/ensentence($1, $trailer)/ge;
  0         0  
319             }
320 0         0 $para->{text} =~ s/\b([A-Z])[.]/\U$1./gi; # ABBREVS
321             }
322             }
323              
324             # ALIGN QUOTERS
325             # DETERMINE HANGING MARKER TYPE (BULLET, ALPHA, ROMAN, ETC.)
326              
327 3         4 my %sigs;
328 3         6 my $lastquoted = 0;
329 3         5 my $lastprespace = 0;
330 3         8 for my $i ( 0..$#paras )
331             {
332 13         21 my $para = $paras[$i];
333 13 100       32 next if $para->{ignore};
334              
335 9 50       22 if ($para->{quoter})
336             {
337 0 0       0 if ($lastquoted) { $para->{prespace} = $lastprespace }
  0         0  
338 0         0 else { $lastquoted = 1; $lastprespace = $para->{prespace} }
  0         0  
339             }
340             else
341             {
342 9         17 $lastquoted = 0;
343             }
344             }
345              
346             # RENUMBER PARAGRAPHS
347              
348 3         8 for my $para ( @paras ) {
349 13 100       35 next if $para->{ignore};
350 9         34 my $sig = $para->{presig} . $para->{hang}->signature();
351 9         15 push @{$sigs{$sig}{hangref}}, $para;
  9         27  
352             $sigs{$sig}{hangfields} = $para->{hang}->fields()-1
353 9 100       40 unless defined $sigs{$sig}{hangfields};
354             }
355              
356 3         17 while (my ($sig,$val) = each %sigs) {
357 3 50       17 next unless $sig =~ /rom/;
358 0         0 field: for my $field ( 0..$val->{hangfields} )
359             {
360 0         0 my $romlen = 0;
361 0         0 foreach my $para ( @{$val->{hangref}} )
  0         0  
362             {
363 0         0 my $hang = $para->{hang};
364 0         0 my $fieldtype = $hang->field($field);
365             next field
366 0 0 0     0 unless $fieldtype && $fieldtype =~ /rom|let/;
367 0 0       0 if ($fieldtype eq 'let') {
368 0         0 foreach my $para ( @{$val->{hangref}} ) {
  0         0  
369 0         0 $hang->field($field=>'let')
370             }
371             }
372             else {
373 0         0 $romlen += length $hang->val($field);
374             }
375             }
376             # NO ROMAN LETTER > 1 CHAR -> ALPHABETICS
377 0 0       0 if ($romlen <= @{$val->{hangref}}) {
  0         0  
378 0         0 foreach my $para ( @{$val->{hangref}} ) {
  0         0  
379 0         0 $para->{hang}->field($field=>'let')
380             }
381             }
382             }
383             }
384              
385 3         5 my %prev;
386              
387 3         7 for my $para ( @paras ) {
388 13 100       32 next if $para->{ignore};
389 9         25 my $sig = $para->{presig} . $para->{hang}->signature();
390 9 50       24 if ($args{renumber}) {
391 9 50       22 unless ($para->{quoter}) {
392 9         41 $para->{hang}->incr($prev{""}, $prev{$sig});
393             $prev{""} = $prev{$sig} = $para->{hang}
394 9 50       31 unless $para->{hang}->empty;
395             }
396             }
397            
398             # COLLECT MAXIMAL HANG LENGTHS BY SIGNATURE
399              
400 9         27 my $siglen = $para->{hang}->length();
401             $sigs{$sig}{hanglen} = $siglen
402             if ! $sigs{$sig}{hanglen} ||
403 9 50 33     46 $sigs{$sig}{hanglen} < $siglen;
404             }
405              
406             # PROPAGATE MAXIMAL HANG LENGTH
407              
408 3         13 while (my ($sig,$val) = each %sigs)
409             {
410 3         8 foreach (@{$val->{hangref}}) {
  3         10  
411 9         25 $_->{hanglen} = $val->{hanglen};
412             }
413             }
414              
415             # BUILD FORMAT FOR EACH PARA THEN FILL IT
416              
417 3         5 $text = "";
418 3 50 33     16 my $gap = @paras && $paras[0]->{empty} ? 0 : $args{gap};
419 3         8 for my $para ( @paras )
420             {
421 13 100       29 if ($para->{empty}) {
422 5         11 $gap += 1 + ($para->{text} =~ tr/\n/\n/);
423             }
424 13 100       24 if ($para->{ignore}) {
425 4 50       142 $text .= (!$para->{empty} ? "\n"x($args{gap}-$gap) : "") ;
426 4         13 $text .= $para->{raw};
427 4 50       13 $text .= "\n" unless $para->{raw} =~ /\n\z/;
428             }
429             else {
430             my $leftmargin = $args{left} ? " "x($args{left}-1)
431 9 50       24 : $para->{prespace};
432 9   33     43 my $hlen = $para->{hanglen} || $para->{hang}->length;
433 9 50       26 my $hfield = ($hlen==1 ? '~' : '>'x$hlen);
434 9         11 my @hang;
435 9 50       19 push @hang, $para->{hang}->stringify if $hlen;
436             my $format = $leftmargin
437             . quotemeta($para->{quoter})
438             . $para->{quotespace}
439             . $hfield
440 9         29 . $para->{hangspace};
441 9         27 my $rightslack = int (($args{right}-length $leftmargin)*$Text::Autoformat::widow_slack);
442 9         13 my ($widow_okay, $rightindent, $firsttext, $newtext) = (0,0);
443 9   33     11 do {
444             my $tlen = $args{right}-$rightindent-length($leftmargin
445             . $para->{quoter}
446             . $para->{quotespace}
447             . $hfield
448 9         24 . $para->{hangspace});
449 9 50       34 next if blockquote($text,$para, $format, $tlen, \@hang, \%args);
450             my $tfield = ( $tlen==1 ? '~'
451             : $para->{centred}||$args{_centred} ? '|'x$tlen
452             : $args{justify} eq 'right' ? ']'x$tlen
453             : $args{justify} eq 'full' ? '['x($tlen-2) . ']]'
454 9 50 33     89 : $para->{centred}||$args{_centred} ? '|'x$tlen
    50 33        
    50          
    50          
    50          
455             : '['x$tlen
456             );
457 9         15 my $tryformat = "$format$tfield";
458             $newtext = (!$para->{empty} ? "\n"x($args{gap}-$gap) : "")
459             . form( { squeeze=>$args{squeeze}, trim=>1,
460             break=>$args{break},
461             fill => !(!($args{expfill}
462             || $args{impfill} &&
463             !$para->{centred}))
464             },
465             $tryformat, @hang,
466 9 100 33     245 $para->{text});
467 9   33     5234 $firsttext ||= $newtext;
468 9         61 (my $widow) = $newtext =~ /([^\n]*)$/;
469 9         20 $widow =~ s/^\s+//;
470 9   66     68 $widow_okay = $para->{empty} || length($widow) >= $args{widow};
471             } until $widow_okay || ++$rightindent > $rightslack;
472            
473 9 50       34 $text .= $widow_okay ? $newtext : $firsttext;
474             }
475 13 100       39 $gap = 0 unless $para->{empty};
476             }
477              
478              
479             # RETURN FORMATTED TEXT
480              
481 3 50       11 if ($toSTDOUT) { print STDOUT $text . $remainder; return }
  0         0  
  0         0  
482 3         107 return $text . $remainder;
483             }
484              
485             sub _build_ignore {
486 4     4   8 my $ignore_arg = shift;
487 4         8 my $ig_type = ref $ignore_arg;
488 4         5 my $ignore;
489 4 100       10 if ($ig_type eq 'Regexp') {
    50          
    0          
490 3         5 my $regex = $ignore_arg;
491 3     16   12 $ignore = sub { /$regex/ };
  16         89  
492             } elsif ($ig_type eq 'ARRAY') {
493 1         3 my @elements = map { _build_ignore($_) } @$ignore_arg;
  2         7  
494             $ignore = sub {
495 5 100   5   7 for my $sub (@elements) { return 1 if $sub->(@_) }
  9         17  
496 3         7 return 0;
497 1         7 };
498             }
499             elsif ($ignore_arg =~ /^indent/i) {
500 0 0   0   0 $ignore = sub { ignore_headers(@_) || /$ignore_indent/ };
  0         0  
501             }
502             else {
503 0         0 $ignore = $ignore_arg;
504             }
505 4 50       13 croak "Expected suboutine reference as value for -ignore option"
506             if ref $ignore ne 'CODE';
507 4         12 return $ignore;
508             }
509              
510             my $alpha = qr/[^\W\d_]/;
511             my $notalpha = qr/[\W\d_]/;
512 4     4   4696 my $word = qr/\pL(?:\pL'?)*/;
  4         36  
  4         58  
513             my $upper = qr/[^\Wa-z\d_]/;
514             my $lower = qr/[^\WA-Z\d_]/;
515             my $mixed = qr/$alpha*?(?:$lower$upper|$upper$lower)$alpha*/;
516              
517             sub recase {
518 0     0 0 0 my ($origtext, $case) = @_;
519 0         0 my ($entities, $other_entities, $first, $rest) = @{$casing{$case}};
  0         0  
520              
521 0         0 my $text = "";
522 0         0 my @pieces = split /(&[a-z]+;)/i, $origtext;
523 0 0       0 push @pieces, "" if @pieces % 2;
524 0 0       0 return $text unless @pieces;
525 0         0 local $_ = shift @pieces;
526 0 0       0 if (length $_) {
527 0         0 $entities = $other_entities;
528 0         0 &$first;
529 0         0 $text .= $_;
530             }
531 0 0       0 return $text unless @pieces;
532 0         0 $_ = shift @pieces;
533 0   0     0 $text .= $entities->{$_} || $_;
534 0         0 while (@pieces) {
535 0         0 $_ = shift @pieces; &$rest; $text .= $_;
  0         0  
  0         0  
536 0   0     0 $_ = shift @pieces; $text .= $other_entities->{$_} || $_;
  0         0  
537             }
538 0         0 return $text;
539             }
540              
541             my $alword = qr{(?:\pL|&[a-z]+;)(?:[\pL']|&[a-z]+;)*}i;
542              
543             sub entitle {
544 0     0 0 0 my ($text, $retitler_ref) = @_;
545              
546             # put into lowercase if on stop list, else titlecase
547 0         0 $text =~ s{($alword)}
548 0 0       0 { $retitler_ref ? $retitler_ref->($1) : recase($1,'title') }gex;
549              
550 0 0       0 if ($retitler_ref == $STD_HIGHLIGHT_IGNORES) {
551             # First and final words always capitalized...
552 0         0 $text =~ s/^($alword) /recase($1,'title')/ex;
  0         0  
553 0         0 $text =~ s/ ($alword)$/recase($1,'title')/ex;
  0         0  
554              
555             # treat parethesized portion as a complete title
556 0         0 $text =~ s/\( ($alword) /'('.recase($1,'title')/ex;
  0         0  
557 0         0 $text =~ s/($alword) \) /recase($1,'title').')'/ex;
  0         0  
558              
559             # capitalize first word following colon or semi-colon
560 0         0 $text =~ s/ ( [:;] \s+ ) ($alword) /$1 . recase($2,'title')/ex;
  0         0  
561             }
562              
563 0         0 return $text;
564             }
565              
566             my $gen_abbrev = join '|',
567             qw{ etc[.] pp[.] ph[.]?d[.] },
568             '(?:[A-Z][.])(?:[A-Z][.])+',
569             '(^[^a-zA-Z]*([a-z][.])+)';
570              
571             my $term = q{(?:[.]|[!?]+)};
572              
573             my $eos = 1;
574             my $brsent = 0;
575              
576             sub ensentence {
577 0 0   0 0 0 do { $eos = 1; return } unless @_;
  0         0  
  0         0  
578 0         0 my ($str, $trailer) = @_;
579 0 0       0 if ($str =~ /^([^a-z]*)I[^a-z]*?($term?)[^a-z]*$/i) {
580 0         0 $eos = $2;
581 0         0 $brsent = $1 =~ /^[[(]/;
582 0         0 return uc $str
583             }
584 0 0       0 unless ($str =~ /[a-z].*[A-Z]|[A-Z].*[a-z]/) {
585 0         0 $str = lc $str;
586             }
587 0 0       0 if ($eos) {
588 0         0 $str =~ s/([a-z])/uc $1/ie;
  0         0  
589 0         0 $brsent = $str =~ /^[[(]/;
590             }
591 0   0     0 $eos = $str !~ /^($gen_abbrev)[^a-z]*\s/i
592             && $str =~ /[a-z][^a-z]*$term([^a-z]*)\s/
593             && !($1=~/[])]/ && !$brsent);
594 0 0 0     0 $str =~ s/\s+$/$trailer/ if $eos && $trailer;
595 0         0 return $str;
596             }
597              
598             # blockquote($text,$para, $format, $tlen, \@hang, \%args);
599             sub blockquote {
600 9     9 0 15 my ($dummy, $para, $format, $tlen, $hang, $args) = @_;
601              
602             $para->{text} =~
603 9 50       45 / \A(\s*) # $1 - leading whitespace (quotation)
604             (["']|``) # $2 - opening quotemark
605             (.*) # $3 - quotation
606             (''|\2) # $4 closing quotemark
607             \s*?\n # trailing whitespace
608             (\1[ ]+) # $5 - leading whitespace (attribution)
609             (--|-) # $6 - attribution introducer
610             (.*?$) # $7 - attribution line 1
611             ((\5.*?$)*) # $8 - attributions lines 2-N
612             \s*\Z
613             /xsm
614             or return;
615              
616             #print "[$1][$2][$3][$4][$5][$6][$7]\n";
617 0           my $indent = length $1;
618 0           my $text = $2.$3.$4;
619 0           my $qindent = length $2;
620 0           my $aindent = length $5;
621 0           my $attribintro = $6;
622 0           my $attrib = $7.$8;
623 0           $text =~ s/\n/ /g;
624              
625             $_[0] .=
626              
627             form {squeeze=>$args->{squeeze}, trim=>1,
628             fill => $args->{expfill}
629             },
630 0           $format . q{ }x$indent . q{<}x$tlen,
631             @$hang, $text,
632             $format . q{ }x($qindent) . q{[}x($tlen-$qindent),
633             @$hang, $text,
634             {squeeze=>0},
635             $format . q{ } x $aindent . q{>> } . q{[}x($tlen-$aindent-3),
636             @$hang, $attribintro, $attrib;
637 0           return 1;
638             }
639              
640             1;
641              
642             __END__