File Coverage

blib/lib/String/UnicodeUTF8.pm
Criterion Covered Total %
statement 99 114 86.8
branch 44 60 73.3
condition 11 18 61.1
subroutine 24 24 100.0
pod 17 17 100.0
total 195 233 83.6


line stmt bran cond sub pod time code
1             package String::UnicodeUTF8;
2              
3 2     2   27724 use strict;
  2         3  
  2         47  
4 2     2   6 use warnings;
  2         2  
  2         38  
5              
6 2     2   755 use String::Unquotemeta ();
  2         444  
  2         42  
7 2     2   750 use Module::Want 0.6 ();
  2         1573  
  2         85  
8              
9             $String::UnicodeUTF8::VERSION = '0.22';
10              
11             sub import {
12 23 100   23   13560 return 1 if @_ == 1; # no-op import()
13              
14 21         38 my $caller = caller();
15              
16 2     2   8 no strict 'refs'; ## no critic
  2         2  
  2         2741  
17 21         53 for ( @_[ 1 .. $#_ ] ) {
18 21 100 100     90 next if $_ eq 'import' || $_ eq '_pre_581_is_utf8_hack';
19 19 100       16 *{ $caller . '::' . $_ } = \&{$_} if defined &{$_};
  18         78  
  18         23  
  19         61  
20             }
21             }
22              
23             # characters the caller may or may not consider “safe” depending on context
24             my %specials = (
25             'NO-BREAK SPACE' => qr/\x{00A0}/,
26             'LINE FEED (LF)' => qr/\x{000A}/,
27             'CARRIAGE RETURN (CR)' => qr/\x{000D}/,
28             'CHARACTER TABULATION' => qr/\x{0009}/,
29             );
30              
31             # `unichars '\p{WhiteSpace}'` sans SPACE/0020 and %specials
32             my $disallowed_whitespace = qr/(?:\x{000B}|\x{000C}|\x{0085}|\x{1680}|\x{180E}|\x{2000}|\x{2001}|\x{2002}|\x{2003}|\x{2004}|\x{2005}|\x{2006}|\x{2007}|\x{2008}|\x{2009}|\x{200A}|\x{2028}|\x{2029}|\x{202F}|\x{205F}|\x{3000})/;
33              
34             # unichars '\p{Control}' ` sans %specials
35             my $control =
36             qr/(?:\x{0000}|\x{0001}|\x{0002}|\x{0003}|\x{0004}|\x{0005}|\x{0006}|\x{0007}|\x{0008}|\x{000B}|\x{000C}|\x{000E}|\x{000F}|\x{0010}|\x{0011}|\x{0012}|\x{0013}|\x{0014}|\x{0015}|\x{0016}|\x{0017}|\x{0018}|\x{0019}|\x{001A}|\x{001B}|\x{001C}|\x{001D}|\x{001E}|\x{001F}|\x{007F}|\x{0080}|\x{0081}|\x{0082}|\x{0083}|\x{0084}|\x{0085}|\x{0086}|\x{0087}|\x{0088}|\x{0089}|\x{008A}|\x{008B}|\x{008C}|\x{008D}|\x{008E}|\x{008F}|\x{0090}|\x{0091}|\x{0092}|\x{0093}|\x{0094}|\x{0095}|\x{0096}|\x{0097}|\x{0098}|\x{0099}|\x{009A}|\x{009B}|\x{009C}|\x{009D}|\x{009E}|\x{009F})/;
37              
38             # `uninames invisible`
39             my $invisible = qr/(?:\x{200B}|\x{2062}|\x{2063}|\x{2064})/;
40              
41             sub contains_nonhuman_characters {
42 24     24 1 292 my ( $string, %allow_specials ) = @_;
43 24         28 my $uni_str = get_unicode($string);
44              
45 24         45 for my $name ( keys %specials ) {
46 84 100       102 next if $allow_specials{$name};
47 76 100       249 return 1 if $uni_str =~ m/$specials{$name}/;
48             }
49              
50 16 100       66 return 1 if $uni_str =~ m/$invisible/;
51 14 100       45 return 1 if $uni_str =~ m/$disallowed_whitespace/;
52 12 100       34 return 1 if $uni_str =~ m/$control/;
53              
54 10         32 return;
55             }
56              
57             # is_utf8() is confusing, it really means “is this a Unicode string”, not “is this a utf-8 bytes string”)
58             *is_unicode = $] >= 5.008_001 ? \&utf8::is_utf8 : \&_pre_581_is_utf8_hack; # or just 'use 5.8.1;' and drop this ?
59              
60             my $pre_573_is_utf8_hack = $] >= 5.007_003 ? undef : {};
61              
62             sub char_count {
63 7     7 1 1531 return CORE::length( get_unicode( $_[0] ) );
64             }
65              
66             sub bytes_size {
67 7     7 1 43 return CORE::length( get_utf8( $_[0] ) );
68             }
69              
70             sub get_unicode {
71 135     135 1 151 my ($string) = @_;
72              
73 135 100       245 if ( !is_unicode($string) ) {
74 71 50       94 if ( defined &utf8::decode ) {
75 71         118 utf8::decode($string);
76             }
77             else { # decode() a hacky way:
78 0         0 $string = pack( "U*", unpack( "C0U*", $string ) ); # 5.6+ at least
79             }
80              
81             # if decode() did not fully do it (e.g. it only contained ascii characters and utf8::decode() was called)
82 71 100       111 if ( !is_unicode($string) ) {
83              
84             # force strings without unicode characters to be unicode strings
85 33 50       34 if ( defined &utf8::upgrade ) {
86 33         43 utf8::upgrade($string);
87             }
88             else { # upgrade() the hacky way: (TODO: how?)
89 0         0 require Carp;
90 0         0 Carp::carp("pack() did not result in unicode string and there is no way to emulate utf8::upgrade");
91             }
92             }
93             }
94              
95 135 50       168 $pre_573_is_utf8_hack->{$string} = '' if ref $pre_573_is_utf8_hack;
96 135         306 return $string;
97             }
98              
99             sub get_utf8 {
100 293     293 1 271 my ($string) = @_;
101 293 100       496 if ( is_unicode($string) ) {
102 63 50       85 if ( defined &utf8::encode ) {
103 63         93 utf8::encode($string);
104             }
105             else { # encode() the hacky way:
106 0         0 $string = pack( "C0U*", unpack( "U*", $string ) ); # 5.6+ at least
107             }
108             }
109              
110 293 50       362 delete $pre_573_is_utf8_hack->{$string} if ref $pre_573_is_utf8_hack;
111 293         704 return $string;
112             }
113              
114             # ? want to serialize these too ?
115             # my %esc = ( "\n" => '\n', "\t" => '\t', "\r" => '\r', "\\" => '\\\\', "\a" => '\a', "\b" => '\b', "\f" => '\f' );
116              
117             sub escape_utf8_or_unicode {
118 58     58 1 348 my ( $s, $quotemeta ) = @_; # undocumented second flag for internal use
119              
120 58         70 my $is_uni = is_unicode($s); # otherwise you'll get \xae\x{301} instead of \x{ae}\x{301}
121              
122             # ick: patches uber welcome
123 58 50 66     177 if ( $is_uni && $] < 5.008_001 && Module::Want::have_mod('Data::Dumper') ) {
      33        
124 0         0 local $Data::Dumper::Terse = 1;
125 0         0 $s = Data::Dumper::Dumper($s);
126 0         0 $s =~ s/\A(["|'])//;
127 0         0 my $quote = $1;
128 0         0 $s =~ s/$quote\s*\z//;
129 0 0       0 $s =~ s/'/\\'/g unless $quote eq "'";
130 0         0 return get_utf8($s);
131             }
132              
133 58         186 $s =~ s{([^!#&()*+,\-.\/0123456789:;<=>?ABCDEFGHIJKLMNOPQRSTUVWXYZ\[\]\^_`abcdefghijklmnopqrstuvwxyz{|}~ ])}
134             {
135 104         113 my $chr = "$1";
136 104         77 my $n = ord($chr);
137              
138             # if ( exists $esc{$chr} ) { # more universal way ???
139             # $esc{$chr};
140             # }
141             # els
142 104 100 100     283 if ( $n < 32 || $n > 126 ) {
    50          
143 83 100 66     347 sprintf( ( !$is_uni && $n < 255 ? '\x%02x' : '\x{%04x}' ), $n );
144             }
145             elsif ($quotemeta) {
146 21         48 quotemeta($chr);
147             }
148             else {
149 0         0 $chr
150             }
151             }ge;
152              
153 58         76 return get_utf8($s);
154             }
155              
156             sub escape_utf8 {
157 9     9 1 41 my ($string) = @_;
158 9         22 $string = get_utf8($string);
159 9         14 return escape_utf8_or_unicode($string);
160             }
161              
162             sub escape_unicode {
163 19     19 1 4856 my ($string) = @_;
164 19         24 $string = get_unicode($string);
165 19         28 return escape_utf8_or_unicode($string);
166             }
167              
168             sub unescape_utf8_or_unicode {
169 139     139 1 114 my ( $string, $unquotemeta ) = @_; # undocumented second flag for internal use
170 139 100       307 my $is_uni = $string =~ m/\\x\{[0-9a-fA-f]+\}/ ? 1 : 0;
171              
172 139         301 $string =~ s/((?:\\x(?:[0-9a-fA-f]{2}|\{[0-9a-fA-f]+\}))+)/eval qq{"$1"}/eg; ## no critic
  58         2420  
173 139 100       358 $string = String::Unquotemeta::unquotemeta($string) if $unquotemeta;
174 139 100       1008 return get_unicode($string) if $is_uni;
175 112         131 return get_utf8($string);
176             }
177              
178             sub unescape_utf8 {
179 2     2 1 3 my ($string) = @_;
180 2         3 $string = unescape_utf8_or_unicode($string);
181 2         3 return get_utf8($string);
182             }
183              
184             sub unescape_unicode {
185 2     2 1 3 my ($string) = @_;
186 2         3 $string = unescape_utf8_or_unicode($string);
187 2         3 return get_unicode($string);
188             }
189              
190             sub quotemeta_bytes { # I ♥ perl\'s coolness
191 7     7 1 51 my $utf8_quoted = quotemeta_utf8( $_[0] );
192 7         9 return unescape_utf8_or_unicode($utf8_quoted);
193             }
194              
195             sub quotemeta_utf8 { # I \xe2\x99\xa5 perl\'s coolness
196 14     14 1 65 my ($string) = @_;
197 14         19 $string = get_utf8($string);
198 14         19 return escape_utf8_or_unicode( $string, 1 );
199             }
200              
201             sub quotemeta_unicode { # I \x{2665} perl\'s coolness
202 7     7 1 45 my ($string) = @_;
203 7         10 $string = get_unicode($string);
204 7         9 return escape_utf8_or_unicode( $string, 1 );
205             }
206              
207             sub unquotemeta_bytes {
208 42     42 1 3203 goto &unquotemeta_utf8;
209             }
210              
211             sub unquotemeta_utf8 {
212 84     84 1 102 my ($escaped_string) = @_;
213 84         107 $escaped_string = unescape_utf8_or_unicode( $escaped_string, 1 );
214 84         94 return get_utf8($escaped_string);
215             }
216              
217             sub unquotemeta_unicode {
218 42     42 1 51 my ($escaped_string) = @_;
219 42         55 $escaped_string = unescape_utf8_or_unicode( $escaped_string, 1 );
220 42         52 return get_unicode($escaped_string);
221             }
222              
223             sub _pre_581_is_utf8_hack {
224 7     7   2398 my ($string) = @_;
225              
226             # strings with unicode characters that are unicode strings
227 7         31 require bytes;
228 7 100       18 return 1 if bytes::length($string) != CORE::length($string);
229              
230             # strings without unicode characters that are unicode strings
231 5 50       827 if ( Module::Want::have_mod('Encode') ) {
232 5 100       7568 return 1 if Encode::is_utf8($string);
233             }
234             else {
235              
236             # So we have a string without unicode characters and no utf8::is_utf8() or Encode::is_utf8(), time to get hacky!
237 0 0 0     0 if ( Module::Want::have_mod('B::Flags') && defined &B::svref_2object ) { # B::Flags brings in B *but* B::svref_2object can be compiled away in some specific circumstances
238 0 0       0 return 1 if B::svref_2object( \$string )->flagspv() =~ m/UTF.?8/i; # works on 5.6!
239             }
240             else {
241              
242             # oi, still nothing is available at this point so time to get reeeeeaaaallly hacky! (patches very very welcome, this is a terrible last ditch effort)
243              
244             # not fool proof (same text–different-string/variable or [down|up]grade() outside of get_[utf8|unicode])
245 0 0       0 return 1 if exists $pre_573_is_utf8_hack->{$string};
246             }
247             }
248              
249 4         29 return;
250             }
251              
252             1;
253              
254             __END__