File Coverage

blib/lib/Text/LineFold.pm
Criterion Covered Total %
statement 146 180 81.1
branch 70 122 57.3
condition 17 41 41.4
subroutine 13 13 100.0
pod 4 4 100.0
total 250 360 69.4


line stmt bran cond sub pod time code
1             #-*- perl -*-
2              
3             package Text::LineFold;
4             require 5.008;
5              
6             =encoding utf-8
7              
8             =head1 NAME
9              
10             Text::LineFold - Line Folding for Plain Text
11              
12             =head1 SYNOPSIS
13              
14             use Text::LineFold;
15             $lf = Text::LineFold->new();
16            
17             # Fold lines
18             $folded = $lf->fold($string, 'PLAIN');
19             $indented = $lf->fold(' ' x 8, ' ' x 4, $string);
20              
21             # Unfold lines
22             $unfolded = $lf->unfold($string, 'FIXED');
23              
24             =head1 DESCRIPTION
25              
26             Text::LineFold folds or unfolds lines of plain text.
27             As it mainly focuses on plain text e-mail messages,
28             RFC 3676 flowed format is also supported.
29              
30             =cut
31              
32             ### Pragmas:
33 3     3   2821 use strict;
  3         6  
  3         108  
34 3     3   17 use vars qw($VERSION @EXPORT_OK @ISA $Config);
  3         6  
  3         303  
35              
36             ### Exporting:
37 3     3   22 use Exporter;
  3         5  
  3         155  
38              
39             ### Inheritance:
40             our @ISA = qw(Exporter Unicode::LineBreak);
41              
42             ### Other modules:
43 3     3   17 use Carp qw(croak carp);
  3         6  
  3         310  
44 3     3   1690 use Encode qw(is_utf8);
  3         32025  
  3         297  
45 3     3   1600 use MIME::Charset;
  3         16838  
  3         236  
46 3     3   1447 use Unicode::LineBreak qw(:all);
  3         9  
  3         10176  
47              
48             ### Globals
49              
50             ### The package Version
51             our $VERSION = '2018.012';
52              
53             ### Public Configuration Attributes
54             our $Config = {
55             ### %{$Unicode::LineBreak::Config},
56             Charset => 'UTF-8',
57             Language => 'XX',
58             OutputCharset => undef,
59             TabSize => 8,
60             };
61              
62             ### Privates
63              
64             my %FORMAT_FUNCS = (
65             'FIXED' => sub {
66             my $self = shift;
67             my $action = shift;
68             my $str = shift;
69             if ($action =~ /^so[tp]/) {
70             $self->{_} = {};
71             $self->{_}->{'ColMax'} = $self->config('ColMax');
72             $self->config('ColMax' => 0) if $str =~ /^>/;
73             } elsif ($action eq "") {
74             $self->{_}->{line} = $str;
75             } elsif ($action eq "eol") {
76             return $self->config('Newline');
77             } elsif ($action =~ /^eo/) {
78             if (length $self->{_}->{line} and $self->config('ColMax')) {
79             $str = $self->config('Newline').$self->config('Newline');
80             } else {
81             $str = $self->config('Newline');
82             }
83             $self->config('ColMax' => $self->{_}->{'ColMax'});
84             delete $self->{_};
85             return $str;
86             }
87             undef;
88             },
89             'FLOWED' => sub { # RFC 3676
90             my $self = shift;
91             my $action = shift;
92             my $str = shift;
93             if ($action eq 'sol') {
94             if ($self->{_}->{prefix}) {
95             return $self->{_}->{prefix}.' '.$str;
96             }
97             } elsif ($action =~ /^so/) {
98             $self->{_} = {};
99             if ($str =~ /^(>+)/) {
100             $self->{_}->{prefix} = $1;
101             } else {
102             $self->{_}->{prefix} = '';
103             }
104             } elsif ($action eq "") {
105             if ($str =~ /^(?: |From )/
106             or $str =~ /^>/ and !length $self->{_}->{prefix}) {
107             return $self->{_}->{line} = ' ' . $str;
108             }
109             $self->{_}->{line} = $str;
110             } elsif ($action eq 'eol') {
111             $str = ' ' if length $str;
112             return $str.' '.$self->config('Newline');
113             } elsif ($action =~ /^eo/) {
114             if (length $self->{_}->{line} and !length $self->{_}->{prefix}) {
115             $str = ' '.$self->config('Newline').$self->config('Newline');
116             } else {
117             $str = $self->config('Newline');
118             }
119             delete $self->{_};
120             return $str;
121             }
122             undef;
123             },
124             'PLAIN' => sub {
125             return $_[0]->config('Newline') if $_[1] =~ /^eo/;
126             undef;
127             },
128             );
129              
130             =head2 Public Interface
131              
132             =over 4
133              
134             =item new ([KEY => VALUE, ...])
135              
136             I.
137             About KEY => VALUE pairs see config method.
138              
139             =back
140              
141             =cut
142              
143             sub new {
144 8     8 1 6519 my $class = shift;
145 8         57 my $self = bless __PACKAGE__->SUPER::new(), $class;
146 8         41 $self->config(@_);
147 8         24 $self;
148             }
149              
150             =over 4
151              
152             =item $self->config (KEY)
153              
154             =item $self->config ([KEY => VAL, ...])
155              
156             I.
157             Get or update configuration. Following KEY => VALUE pairs may be specified.
158              
159             =over 4
160              
161             =item Charset => CHARSET
162              
163             Character set that is used to encode string.
164             It may be string or L object.
165             Default is C<"UTF-8">.
166              
167             =item Language => LANGUAGE
168              
169             Along with Charset option, this may be used to define language/region
170             context.
171             Default is C<"XX">.
172             See also L option.
173              
174             =item Newline => STRING
175              
176             String to be used for newline sequence.
177             Default is C<"\n">.
178              
179             =item OutputCharset => CHARSET
180              
181             Character set that is used to encode result of fold()/unfold().
182             It may be string or L object.
183             If a special value C<"_UNICODE_"> is specified, result will be Unicode string.
184             Default is the value of Charset option.
185              
186             =item TabSize => NUMBER
187              
188             Column width of tab stops.
189             When 0 is specified, tab stops are ignored.
190             Default is 8.
191              
192             =item BreakIndent
193              
194             =item CharMax
195              
196             =item ColMax
197              
198             =item ColMin
199              
200             =item ComplexBreaking
201              
202             =item EAWidth
203              
204             =item HangulAsAL
205              
206             =item LBClass
207              
208             =item LegacyCM
209              
210             =item Prep
211              
212             =item Urgent
213              
214             See L.
215              
216             =back
217              
218             =back
219              
220             =cut
221              
222             sub config {
223 86     86 1 127 my $self = shift;
224 86         186 my @opts = qw{Charset Language OutputCharset TabSize};
225 86         146 my %opts = map { (uc $_ => $_) } @opts;
  344         774  
226 86         168 my $newline = undef;
227              
228             # Get config.
229 86 100       180 if (scalar @_ == 1) {
230 78 50       169 if ($opts{uc $_[0]}) {
231 0         0 return $self->{$opts{uc $_[0]}};
232             }
233 78         181 return $self->SUPER::config($_[0]);
234             }
235              
236             # Set config.
237 8         34 my @o = ();
238 8         25 while (scalar @_) {
239 4         7 my $k = shift;
240 4         7 my $v = shift;
241 4 50       13 if ($opts{uc $k}) {
    50          
242 0         0 $self->{$opts{uc $k}} = $v;
243             } elsif (uc $k eq uc 'Newline') {
244 0         0 $newline = $v;
245             } else {
246 4         12 push @o, $k => $v;
247             }
248             }
249 8 100       29 $self->SUPER::config(@o) if scalar @o;
250              
251             # Character set and language assumed.
252 8 50       197 if (ref $self->{Charset} eq 'MIME::Charset') {
253 0         0 $self->{_charset} = $self->{Charset};
254             } else {
255 8   33     76 $self->{Charset} ||= $Config->{Charset};
256 8         54 $self->{_charset} = MIME::Charset->new($self->{Charset});
257             }
258 8         1619 $self->{Charset} = $self->{_charset}->as_string;
259 8   33     104 my $ocharset = uc($self->{OutputCharset} || $self->{Charset});
260 8 50 33     55 $ocharset = MIME::Charset->new($ocharset)
261             unless ref $ocharset eq 'MIME::Charset' or $ocharset eq '_UNICODE_';
262 8 50       943 unless ($ocharset eq '_UNICODE_') {
263 8         51 $self->{_charset}->encoder($ocharset);
264 8         142 $self->{OutputCharset} = $ocharset->as_string;
265             }
266 8   33     103 $self->{Language} = uc($self->{Language} || $Config->{Language});
267              
268             ## Context
269             $self->SUPER::config(Context =>
270             context(Charset => $self->{Charset},
271 8         57 Language => $self->{Language}));
272              
273             ## Set sizing method.
274             $self->SUPER::config(Sizing => sub {
275 2328     2328   5430 my ($self, $cols, $pre, $spc, $str) = @_;
276              
277 2328         5792 my $tabsize = $self->{TabSize};
278 2328         8018 my $spcstr = $spc.$str;
279 2328         6097 $spcstr->pos(0);
280 2328   100     55798 while (!$spcstr->eos and $spcstr->item->lbc == LB_SP) {
281 1016         4481 my $c = $spcstr->next;
282 1016 100       4370 if ($c eq "\t") {
283 10 50       285 $cols += $tabsize - $cols % $tabsize if $tabsize;
284             } else {
285 1006         26023 $cols += $c->columns;
286             }
287             }
288 2328         33141 return $cols + $spcstr->substr($spcstr->pos)->columns;
289 8         81 });
290              
291             ## Classify horizontal tab as line breaking class SP.
292 8         241 $self->SUPER::config(LBClass => [ord("\t") => LB_SP]);
293             ## Tab size
294 8 50       43 if (defined $self->{TabSize}) {
295 0 0       0 croak "Invalid TabSize option" unless $self->{TabSize} =~ /^\d+$/;
296 0         0 $self->{TabSize} += 0;
297             } else {
298 8         24 $self->{TabSize} = $Config->{TabSize};
299             }
300              
301             ## Newline
302 8 50       48 if (defined $newline) {
303 0 0       0 $newline = $self->{_charset}->decode($newline)
304             unless is_utf8($newline);
305 0         0 $self->SUPER::config(Newline => $newline);
306             }
307             }
308              
309             =over 4
310              
311             =item $self->fold (STRING, [METHOD])
312              
313             =item $self->fold (INITIAL_TAB, SUBSEQUENT_TAB, STRING, ...)
314              
315             I.
316             fold() folds lines of string STRING and returns it.
317             Surplus SPACEs and horizontal tabs at end of line are removed,
318             newline sequences are replaced by that specified by Newline option
319             and newline is appended at end of text if it does not exist.
320             Horizontal tabs are treated as tab stops according to TabSize option.
321              
322             By the first style, following options may be specified for METHOD argument.
323              
324             =over 4
325              
326             =item C<"FIXED">
327              
328             Lines preceded by C<"E"> won't be folded.
329             Paragraphs are separated by empty line.
330              
331             =item C<"FLOWED">
332              
333             C<"Format=Flowed; DelSp=Yes"> formatting defined by RFC 3676.
334              
335             =item C<"PLAIN">
336              
337             Default method. All lines are folded.
338              
339             =back
340              
341             Second style is similar to L.
342             All lines are folded.
343             INITIAL_TAB is inserted at beginning of paragraphs and SUBSEQUENT_TAB
344             at beginning of other broken lines.
345              
346             =back
347              
348             =cut
349              
350             # Special breaking characters: VT, FF, NEL, LS, PS
351             my $special_break = qr/([\x{000B}\x{000C}\x{0085}\x{2028}\x{2029}])/os;
352              
353             sub fold {
354 13     13 1 4655 my $self = shift;
355 13         23 my $str;
356              
357 13 100       42 if (2 < scalar @_) {
358 2   50     19 my $initial_tab = shift || '';
359 2 50       22 $initial_tab = $self->{_charset}->decode($initial_tab)
360             unless is_utf8($initial_tab);
361 2   50     48 my $subsequent_tab = shift || '';
362 2 50       10 $subsequent_tab = $self->{_charset}->decode($subsequent_tab)
363             unless is_utf8($subsequent_tab);
364 2         34 my @str = @_;
365              
366             ## Decode and concat strings.
367 2         5 $str = shift @str;
368 2 50       11 $str = $self->{_charset}->decode($str) unless is_utf8($str);
369 2         30 foreach my $s (@str) {
370 0 0 0     0 next unless defined $s and length $s;
371              
372 0 0       0 $s = $self->{_charset}->decode($s) unless is_utf8($s);
373 0 0       0 unless (length $str) {
    0          
374 0         0 $str = $s;
375 0 0       0 } elsif ($str =~ /(\s|$special_break)$/ or
376             $s =~ /^(\s|$special_break)/) {
377 0         0 $str .= $s;
378             } else {
379 0 0       0 $str .= ' ' if $self->breakingRule($str, $s) == INDIRECT;
380 0         0 $str .= $s;
381             }
382             }
383              
384             ## Set format method.
385             $self->SUPER::config(Format => sub {
386 96     96   189 my $self = shift;
387 96         130 my $event = shift;
388 96         139 my $str = shift;
389 96 100       242 if ($event =~ /^eo/) { return $self->config('Newline'); }
  32         92  
390 64 100       145 if ($event =~ /^so[tp]/) { return $initial_tab.$str; }
  14         464  
391 50 100       109 if ($event eq 'sol') { return $subsequent_tab.$str; }
  18         242  
392 32         190 undef;
393 2         20 });
394             } else {
395 11         24 $str = shift;
396 11   50     42 my $method = uc(shift || '');
397 11 50 33     50 return '' unless defined $str and length $str;
398              
399             ## Decode string.
400 11 50       77 $str = $self->{_charset}->decode($str) unless is_utf8($str);
401              
402             ## Set format method.
403             $self->SUPER::config(Format => $FORMAT_FUNCS{$method} ||
404 11   33     252 $FORMAT_FUNCS{'PLAIN'});
405             }
406              
407             ## Do folding.
408 13         28 my $result = '';
409 13         294 foreach my $s (split $special_break, $str) {
410 13 50       210 if ($s =~ $special_break) {
411 0         0 $result .= $s;
412             } else {
413 13         1198 $result .= $self->break($str);
414             }
415             }
416              
417             ## Encode result.
418 13 50       78 if ($self->{OutputCharset} eq '_UNICODE_') {
419 0         0 return $result;
420             } else {
421 13         71 return $self->{_charset}->encode($result);
422             }
423             }
424              
425             =over 4
426              
427             =item $self->unfold (STRING, METHOD)
428              
429             Conjunct folded paragraphs of string STRING and returns it.
430              
431             Following options may be specified for METHOD argument.
432              
433             =over 4
434              
435             =item C<"FIXED">
436              
437             Default method.
438             Lines preceded by C<"E"> won't be conjuncted.
439             Treat empty line as paragraph separator.
440              
441             =item C<"FLOWED">
442              
443             Unfold C<"Format=Flowed; DelSp=Yes"> formatting defined by RFC 3676.
444              
445             =item C<"FLOWEDSP">
446              
447             Unfold C<"Format=Flowed; DelSp=No"> formatting defined by RFC 3676.
448              
449             =begin comment
450              
451             =item C<"OBSFLOWED">
452              
453             Unfold C<"Format=Flowed> formatting defined by (obsoleted) RFC 2646
454             as well as possible.
455              
456             =end comment
457              
458             =back
459              
460             =back
461              
462             =cut
463              
464             sub unfold {
465 8     8 1 4610 my $self = shift;
466 8         20 my $str = shift;
467 8 50 33     53 return '' unless defined $str and length $str;
468              
469             ## Get format method.
470 8   50     28 my $method = uc(shift || 'FIXED');
471 8 50       53 $method = 'FIXED' unless $method =~ /^(?:FIXED|FLOWED|FLOWEDSP|OBSFLOWED)$/;
472 8         19 my $delsp = $method eq 'FLOWED';
473              
474             ## Decode string and canonizalize newline.
475 8 50       65 $str = $self->{_charset}->decode($str) unless is_utf8($str);
476 8         193 $str =~ s/\r\n|\r/\n/g;
477              
478             ## Do unfolding.
479 8         16 my $result = '';
480 8         207 foreach my $s (split $special_break, $str) {
481 8 50 66     181 if ($s eq '') {
    50 33        
    100          
    50          
482 0         0 next;
483             } elsif ($s =~ $special_break) {
484 0         0 $result .= $s;
485 0         0 next;
486             } elsif ($method eq 'FIXED') {
487 3         25 pos($s) = 0;
488 3         18 while ($s !~ /\G\z/cg) {
489 48 100       478 if ($s =~ /\G\n/cg) {
    100          
    100          
    50          
    50          
    0          
    0          
490 6         15 $result .= $self->config('Newline');
491             } elsif ($s =~ /\G(.+)\n\n/cg) {
492 15         37 $result .= $1.$self->config('Newline');
493             } elsif ($s =~ /\G(>.*)\n/cg) {
494 9         22 $result .= $1.$self->config('Newline');
495             } elsif ($s =~ /\G(.+)\n(?=>)/cg) {
496 0         0 $result .= $1.$self->config('Newline');
497             } elsif ($s =~ /\G(.+?)( *)\n(?=(.+))/cg) {
498 18         72 my ($l, $s, $n) = ($1, $2, $3);
499 18         32 $result .= $l;
500 18 50       66 if ($n =~ /^ /) {
    50          
    50          
501 0         0 $result .= $self->config('Newline');
502             } elsif (length $s) {
503 0         0 $result .= $s;
504             } elsif (length $l) {
505 18 100       362 $result .= ' '
506             if $self->breakingRule($l, $n) == INDIRECT;
507             }
508             } elsif ($s =~ /\G(.+)\n/cg) {
509 0         0 $result .= $1.$self->config('Newline');
510             } elsif ($s =~ /\G(.+)/cg) {
511 0         0 $result .= $1.$self->config('Newline');
512 0         0 last;
513             }
514             }
515             } elsif ($method eq 'FLOWED' or $method eq 'FLOWEDSP' or
516             $method eq 'OBSFLOWED') {
517 5         14 my $prefix = undef;
518 5         34 pos($s) = 0;
519 5         25 while ($s !~ /\G\z/cg) {
520 96 100       672 if ($s =~ /\G(>+) ?(.*?)( ?)\n/cg) {
    50          
    0          
521 26         85 my ($p, $l, $s) = ($1, $2, $3);
522 26 100       52 unless (defined $prefix) {
    100          
523 17         46 $result .= $p.' '.$l;
524 0         0 } elsif ($p ne $prefix) {
525 1         4 $result .= $self->config('Newline');
526 1         5 $result .= $p.' '.$l;
527             } else {
528 8         17 $result .= $l;
529             }
530 26 100       58 unless (length $s) {
531 17         35 $result .= $self->config('Newline');
532 17         59 $prefix = undef;
533             } else {
534 9         15 $prefix = $p;
535 9 100       34 $result .= $s unless $delsp;
536             }
537             } elsif ($s =~ /\G ?(.*?)( ?)\n/cg) {
538 70         175 my ($l, $s) = ($1, $2);
539 70 50       127 unless (defined $prefix) {
    100          
540 30         52 $result .= $l;
541 0         0 } elsif ('' ne $prefix) {
542 0         0 $result .= $self->config('Newline');
543 0         0 $result .= $l;
544             } else {
545 40         64 $result .= $l;
546             }
547 70 100       121 unless (length $s) {
548 30         60 $result .= $self->config('Newline');
549 30         107 $prefix = undef;
550             } else {
551 40 100       73 $result .= $s unless $delsp;
552 40         104 $prefix = '';
553             }
554             } elsif ($s =~ /\G ?(.*)/cg) {
555 0         0 $result .= $1.$self->config('Newline');
556 0         0 last;
557             }
558             }
559             }
560             }
561             ## Encode result.
562 8 50       42 if ($self->{OutputCharset} eq '_UNICODE_') {
563 0         0 return $result;
564             } else {
565 8         34 return $self->{_charset}->encode($result);
566             }
567             }
568              
569             =head1 BUGS
570              
571             Please report bugs or buggy behaviors to developer.
572              
573             CPAN Request Tracker:
574             L.
575              
576             =head1 VERSION
577              
578             Consult $VERSION variable.
579              
580             =head1 SEE ALSO
581              
582             L, L.
583              
584             =head1 AUTHOR
585              
586             Copyright (C) 2009-2012 Hatuka*nezumi - IKEDA Soji .
587              
588             This program is free software; you can redistribute it and/or modify it
589             under the same terms as Perl itself.
590              
591             =cut
592              
593             1;