File Coverage

blib/lib/String/UnicodeUTF8.pm
Criterion Covered Total %
statement 100 114 87.7
branch 45 60 75.0
condition 11 18 61.1
subroutine 24 24 100.0
pod 17 17 100.0
total 197 233 84.5


line stmt bran cond sub pod time code
1             package String::UnicodeUTF8;
2              
3 2     2   111113 use strict;
  2         11  
  2         48  
4 2     2   8 use warnings;
  2         3  
  2         38  
5              
6 2     2   746 use String::Unquotemeta ();
  2         399  
  2         41  
7 2     2   746 use Module::Want 0.6 ();
  2         1814  
  2         104  
8              
9             $String::UnicodeUTF8::VERSION = '0.23';
10              
11             sub import {
12 23 100   23   11676 return 1 if @_ == 1; # no-op import()
13              
14 21         35 my $caller = caller();
15              
16 2     2   13 no strict 'refs'; ## no critic
  2         3  
  2         3010  
17 21         51 for ( @_[ 1 .. $#_ ] ) {
18 21 100 100     73 next if $_ eq 'import' || $_ eq '_pre_581_is_utf8_hack';
19 19 100       24 *{ $caller . '::' . $_ } = \&{$_} if defined &{$_};
  18         65  
  18         33  
  19         58  
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 450 my ( $string, %allow_specials ) = @_;
43 24         43 my $uni_str = get_unicode($string);
44              
45 24         56 for my $name ( keys %specials ) {
46 84 100       144 next if $allow_specials{$name};
47 76 100       293 return 1 if $uni_str =~ m/$specials{$name}/;
48             }
49              
50 16 100       78 return 1 if $uni_str =~ m/$invisible/;
51 14 100       49 return 1 if $uni_str =~ m/$disallowed_whitespace/;
52 12 100       41 return 1 if $uni_str =~ m/$control/;
53              
54 10         41 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 2715 return CORE::length( get_unicode( $_[0] ) );
64             }
65              
66             sub bytes_size {
67 7     7 1 75 return CORE::length( get_utf8( $_[0] ) );
68             }
69              
70             sub get_unicode {
71 135     135 1 239 my ($string) = @_;
72              
73 135 100       292 if ( !is_unicode($string) ) {
74 71 50       131 if ( defined &utf8::decode ) {
75 71         153 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       122 if ( !is_unicode($string) ) {
83              
84             # force strings without unicode characters to be unicode strings
85 33 50       67 if ( defined &utf8::upgrade ) {
86 33         52 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       224 $pre_573_is_utf8_hack->{$string} = '' if ref $pre_573_is_utf8_hack;
96 135         347 return $string;
97             }
98              
99             sub get_utf8 {
100 293     293 1 488 my ($string) = @_;
101 293 100       538 if ( is_unicode($string) ) {
102 63 50       112 if ( defined &utf8::encode ) {
103 63         99 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       445 delete $pre_573_is_utf8_hack->{$string} if ref $pre_573_is_utf8_hack;
111 293         856 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 594 my ( $s, $quotemeta ) = @_; # undocumented second flag for internal use
119              
120 58         98 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     164 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         232 $s =~ s{([^A-Za-z_0-9])}
134 232         365 {
135 232         250 my $chr = "$1";
136             my $n = ord($chr);
137              
138             # if ( exists $esc{$chr} ) { # more universal way ???
139             # $esc{$chr};
140             # }
141 232 100 100     570 # els
    100          
142 83 100 66     366 if ( $n < 32 || $n > 126 ) {
143             sprintf( ( !$is_uni && $n < 255 ? '\x%02x' : '\x{%04x}' ), $n );
144             }
145 84         208 elsif ($quotemeta) {
146             quotemeta($chr);
147             }
148 65         162 else {
149             $chr
150             }
151             }ge;
152 58         109  
153             return get_utf8($s);
154             }
155              
156 9     9 1 60 sub escape_utf8 {
157 9         16 my ($string) = @_;
158 9         16 $string = get_utf8($string);
159             return escape_utf8_or_unicode($string);
160             }
161              
162 19     19 1 10304 sub escape_unicode {
163 19         42 my ($string) = @_;
164 19         37 $string = get_unicode($string);
165             return escape_utf8_or_unicode($string);
166             }
167              
168 139     139 1 234 sub unescape_utf8_or_unicode {
169 139 100       446 my ( $string, $unquotemeta ) = @_; # undocumented second flag for internal use
170             my $is_uni = $string =~ m/\\x\{[0-9a-fA-f]+\}/ ? 1 : 0;
171 139         404  
  58         2857  
172 139 100       409 $string =~ s/((?:\\x(?:[0-9a-fA-f]{2}|\{[0-9a-fA-f]+\}))+)/eval qq{"$1"}/eg; ## no critic
173 139 100       1613 $string = String::Unquotemeta::unquotemeta($string) if $unquotemeta;
174 112         201 return get_unicode($string) if $is_uni;
175             return get_utf8($string);
176             }
177              
178 2     2 1 4 sub unescape_utf8 {
179 2         4 my ($string) = @_;
180 2         4 $string = unescape_utf8_or_unicode($string);
181             return get_utf8($string);
182             }
183              
184 2     2 1 4 sub unescape_unicode {
185 2         4 my ($string) = @_;
186 2         5 $string = unescape_utf8_or_unicode($string);
187             return get_unicode($string);
188             }
189              
190 7     7 1 75 sub quotemeta_bytes { # I ♥ perl\'s coolness
191 7         13 my $utf8_quoted = quotemeta_utf8( $_[0] );
192             return unescape_utf8_or_unicode($utf8_quoted);
193             }
194              
195 14     14 1 76 sub quotemeta_utf8 { # I \xe2\x99\xa5 perl\'s coolness
196 14         27 my ($string) = @_;
197 14         28 $string = get_utf8($string);
198             return escape_utf8_or_unicode( $string, 1 );
199             }
200              
201 7     7 1 78 sub quotemeta_unicode { # I \x{2665} perl\'s coolness
202 7         16 my ($string) = @_;
203 7         14 $string = get_unicode($string);
204             return escape_utf8_or_unicode( $string, 1 );
205             }
206              
207 42     42 1 5774 sub unquotemeta_bytes {
208             goto &unquotemeta_utf8;
209             }
210              
211 84     84 1 139 sub unquotemeta_utf8 {
212 84         147 my ($escaped_string) = @_;
213 84         123 $escaped_string = unescape_utf8_or_unicode( $escaped_string, 1 );
214             return get_utf8($escaped_string);
215             }
216              
217 42     42 1 79 sub unquotemeta_unicode {
218 42         69 my ($escaped_string) = @_;
219 42         70 $escaped_string = unescape_utf8_or_unicode( $escaped_string, 1 );
220             return get_unicode($escaped_string);
221             }
222              
223 7     7   4011 sub _pre_581_is_utf8_hack {
224             my ($string) = @_;
225              
226 7         547 # strings with unicode characters that are unicode strings
227 7 100       29 require bytes;
228             return 1 if bytes::length($string) != CORE::length($string);
229              
230 5 50       688 # strings without unicode characters that are unicode strings
231 5 100       8430 if ( Module::Want::have_mod('Encode') ) {
232             return 1 if Encode::is_utf8($string);
233             }
234             else {
235              
236 0 0 0     0 # 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 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             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 0 0       0 # not fool proof (same text–different-string/variable or [down|up]grade() outside of get_[utf8|unicode])
245             return 1 if exists $pre_573_is_utf8_hack->{$string};
246             }
247             }
248 4         46  
249             return;
250             }
251              
252             1;
253              
254             __END__