File Coverage

blib/lib/Text/Conceal.pm
Criterion Covered Total %
statement 20 113 17.7
branch 0 48 0.0
condition 0 13 0.0
subroutine 7 20 35.0
pod 3 6 50.0
total 30 200 15.0


line stmt bran cond sub pod time code
1             package Text::Conceal;
2              
3             our $VERSION = '0.99';
4              
5 1     1   788 use v5.14;
  1         3  
6 1     1   5 use warnings;
  1         2  
  1         24  
7 1     1   659 use utf8;
  1         14  
  1         5  
8 1     1   39 use Carp;
  1         2  
  1         54  
9 1     1   625 use Data::Dumper;
  1         6932  
  1         66  
10             {
11 1     1   8 no warnings 'redefine', 'once';
  1         2  
  1         856  
12 0     0     *Data::Dumper::qquote = sub { qq["${\(shift)}"] };
  0            
13             $Data::Dumper::Useperl = 1;
14             $Data::Dumper::Sortkey = 1;
15             }
16              
17             my %char_range = (
18             STRAIGHT => [ [0x01=>0x07], [0x10=>0x1f], [0x21=>0x7e], [0x81=>0xfe] ],
19             MODERATE => [ [0x21=>0x7e], [0x01=>0x07], [0x10=>0x1f], [0x81=>0xfe] ],
20             VISIBLE => [ [0x21=>0x7e] ],
21             );
22              
23             my %default = (
24             test => undef,
25             length => sub { length $_[0] },
26             match => qr/.+/s,
27             except => '',
28             max => 0,
29             visible => 0,
30             ordered => 1,
31             );
32              
33             sub new {
34 0     0 1   my $class = shift;
35 0           my $obj = bless { %default }, $class;
36 0 0         $obj->configure(@_) if @_;
37 0           $obj;
38             }
39              
40             sub configure {
41 0     0 0   my $obj = shift;
42 0           while (my($k, $v) = splice @_, 0, 2) {
43 0 0         if (not exists $default{$k}) {
44 0           croak "$k: invalid parameter";
45             }
46 0 0         if ($k eq 'test') {
47 0           $obj->{$k} = do {
48 0 0   0     if (not $v) { sub { 1 } }
  0 0          
  0 0          
49 0     0     elsif (ref $v eq 'Regexp') { sub { $_[0] =~ $v } }
  0            
50 0           elsif (ref $v eq 'CODE') { $v }
51 0     0     else { sub { 1 } }
  0            
52             };
53             } else {
54 0 0 0       $k eq 'length' and ( ref $v eq 'CODE' or die );
55 0           $obj->{$k} = $v;
56             }
57             }
58 0           $obj;
59             }
60              
61             sub encode {
62 0     0 1   my $obj = shift;
63 0           $obj->{replace} = [];
64 0 0         my $conceal = $obj->concealer(grep defined, $obj->{except}, @_)
65             or return @_;
66 0 0         my $match = $obj->{match} or die;
67 0           my $test = $obj->{test};
68 0           for my $arg (grep defined, @_) {
69 0 0 0       not $test or $test->($arg) or next;
70 0           $arg =~ s{$match}{
71 0 0         if (my($replace, $regex, $len) = $conceal->(${^MATCH})) {
72 0           push @{$obj->{replace}}, [ $regex, ${^MATCH}, $len ];
  0            
73 0           $replace;
74             } else {
75 0           ${^MATCH};
76             }
77             }pge;
78             }
79 0           @_;
80             }
81              
82             sub decode {
83 0     0 1   my $obj = shift;
84 0 0         my @replace = @{$obj->{replace}} or return @_;
  0            
85             ARGS:
86 0           for (@_) {
87 0           for my $i (0 .. $#replace) {
88 0           my($regex, $orig, $len) = @{$replace[$i]};
  0            
89 0 0         if (s/$regex/_replace(${^MATCH}, $orig, $len)/pe) {
  0            
90 0 0         if ($obj->{ordered}) {
91 0           splice @replace, 0, $i + 1;
92             } else {
93 0           splice @replace, $i, 1;
94             }
95 0           redo ARGS;
96             }
97             }
98             }
99 0           @_;
100             }
101              
102             sub _replace {
103 0     0     my($matched, $orig, $len) = @_;
104 0           my $width = length $matched;
105 0 0         if ($width == $len) {
106 0           $orig;
107             } else {
108 0           _trim($orig, $width);
109             }
110             }
111              
112             sub _trim {
113 0     0     my($str, $width) = @_;
114 1     1   528 use Text::ANSI::Fold;
  1         31968  
  1         473  
115 0           state $f = Text::ANSI::Fold->new(padding => 1);
116 0           my($folded, $rest, $w) = $f->fold($str, width => $width);
117 0 0         if ($w <= $width) {
    0          
118 0           $folded;
119             } elsif ($width == 1) {
120 0           ' '; # wide char not fit to single column
121             } else {
122 0           die "Panic"; # should never reach here...
123             }
124             }
125              
126             sub concealer {
127 0     0 0   my $obj = shift;
128 0           my $max = $obj->{max};
129 0           local $_ = join '', @_;
130 0           my @a;
131 0           my @range = do {
132 0           map { $_->[0] .. $_->[1] }
133 0   0       @{ $obj->{range} //= $obj->char_range };
  0            
134             };
135 0           for my $i (@range) {
136 0           my $c = pack "C", $i;
137 0 0         push @a, $c unless /\Q$c/;
138 0 0 0       last if $max && @a > $max;
139             }
140 0 0         return if @a < 2;
141 0           my $lead = do { local $" = ''; qr/[^\Q@a\E]*+/ };
  0            
  0            
142 0           my $b = shift @a;
143             return sub {
144 0     0     my $len = $obj->{length}->(+shift =~ s/\X\cH+//gr);
145 0 0         return if $len < 1;
146 0           my $a = $a[ (state $n)++ % @a ];
147 0           my $bl = $len - 1;
148 0           ( $a . ($b x $bl), qr/\G${lead}\K\Q$a$b\E{0,$bl}(?!\Q$b\E)/, $len );
149 0           };
150             }
151              
152             sub char_range {
153 0     0 0   my $obj = shift;
154 0   0       my $v = $obj->{visible} // 0;
155 0 0         if ($v == 0) { $char_range{STRAIGHT} }
  0 0          
    0          
156 0           elsif ($v == 1) { $char_range{MODERATE} }
157 0           elsif ($v == 2) { $char_range{VISIBLE} }
158 0           else { die }
159             }
160              
161             1;
162              
163             __END__