File Coverage

blib/lib/String/Unescape.pm
Criterion Covered Total %
statement 20 20 100.0
branch 4 6 66.6
condition 2 3 66.6
subroutine 6 6 100.0
pod 1 1 100.0
total 33 36 91.6


line stmt bran cond sub pod time code
1             package String::Unescape;
2              
3 5     5   8295 use 5.008;
  5         32  
  5         226  
4 5     5   32 use strict;
  5         9  
  5         179  
5 5     5   30 use warnings;
  5         10  
  5         454  
6              
7             # ABSTRACT: Unescape perl-escaped string
8             our $VERSION = 'v0.0.2'; # VERSION
9              
10             require Exporter;
11             our (@EXPORT_OK) = qw(unescape);
12              
13 5     5   29 use Carp;
  5         10  
  5         7651  
14              
15             my %map = (
16             t => "\t",
17             n => "\n",
18             r => "\r",
19             f => "\f",
20             b => "\b",
21             a => "\a",
22             e => "\e",
23             );
24              
25             my %mapc = map { chr($_) => chr($_ ^ 0x60) } 97..122;
26              
27             my %convs = (
28             l => sub { lcfirst shift },
29             u => sub { ucfirst shift },
30             );
31              
32             my %convp = (
33             L => sub { lc shift },
34             U => sub { uc shift },
35             Q => sub { quotemeta shift },
36             );
37              
38             if($^V ge v5.16.0) {
39             # All constant stringy eval so this should be safe.
40 5     5   27 eval q{use feature qw(fc); $convp{F} = sub { fc(shift) };}; ## no critic (ProhibitStringyEval)
  5         11  
  5         580  
41             } else {
42             $convp{F} = sub { 'F'.shift }; # \E omitted
43             }
44              
45             my $from_code = sub { chr(hex(shift)); };
46             my $from_name;
47              
48             if($^V ge v5.14.0) {
49             $from_name = sub {
50             my $name = shift;
51             return charnames::string_vianame($name) || die "Unknown charname $name";
52             };
53             } else {
54             $from_name = sub {
55             my $name = shift;
56             my $code = charnames::vianame($name);
57             die "Unknown charname $name" if ! defined $code;
58             return chr($code);
59             };
60             }
61              
62             my $re_single = qr/
63             \\([tnrfbae]) | # $1 : one char
64             \\c(.) | # $2 : control
65             \\x\{([0-9a-fA-F]*)[^}]*\} | # $3 : \x{}
66             \\x([0-9a-fA-F]{0,2}) | # $4 : \x
67             \\([0-7]{1,3}) | # $5 : \077
68             \\o\{([0-7]*)([^}]*)\} | # $6, $7 : \o{}
69             \\N\{U\+([^}]*)\} | # $8 : \N{U+}
70             \\N\{([^}]*)\} | # $9 : \N{name}
71              
72             \\(l|u)(.?) | # $10, $11 : \l, \u
73             \\E | #
74             \\?(.) # $12
75             /xs;
76              
77             my $convert_single = sub {
78             require charnames if defined $8 || defined $9;
79              
80             return $map{$1} if defined $1;
81             return exists $mapc{$2} ? $mapc{$2} : chr(ord($2) ^ 0x40) if defined $2;
82             return chr(hex($3)) if defined $3;
83             return chr(hex($4)) if defined $4;
84             return chr(oct($5)) if defined $5;
85             return chr(oct($6)) if defined $6 && $^V ge v5.14.0;
86             return 'o{'.$6.$7.'}' if defined $6;
87             # TODO: Need to check invalid cases
88             return $from_code->($8) if defined $8;
89             return $from_name->($9) if defined $9;
90             return $convs{$10}($11) if defined $10;
91             return $12 if defined $12;
92             return ''; # \E
93             };
94              
95             my $apply_single = sub {
96             my $target = shift;
97             while($target =~ s/\G$re_single/$convert_single->()/gxse) {
98             last unless defined pos($target);
99             }
100             return $target;
101             };
102              
103             # NOTE: I'm not sure the reason, but my $_re_recur; causes a error.
104             our $_re_recur;
105             $_re_recur = qr/
106             \\([LUQF])
107             (?:(?>(?:[^\\]|\\[^LUQFE])+)|(??{$_re_recur}))*
108             (?:\\E|\Z)
109             /xs;
110              
111             my $re_range = qr/
112             ((?:[^\\]|\\[^LUQF])*) # $1: pre
113             (?:
114             \\([LUQF]) # $2: marker
115             ((?:(?>(?:[^\\]|\\[^LUQFE])+)|(??{$_re_recur}))*) # $3: content
116             (?:\\E|\Z)
117             )*
118             /xs;
119              
120             my $apply_range;
121              
122             my $convert_range = sub {
123             my ($pre, $marker, $content) = @_;
124             return
125             (defined $pre ? $apply_single->($pre) : '').
126             (defined $marker ? $convp{$marker}($apply_range->($content)) : '');
127             };
128              
129             $apply_range = sub {
130             my $target = shift;
131             while($target =~ s/\G$re_range/$convert_range->($1, $2, $3)/gxse) {
132             last unless defined pos($target);
133             }
134             return $target;
135             };
136              
137             sub unescape
138             {
139 44 100 66 44 1 155798 shift if @_ && eval { $_[0]->isa(__PACKAGE__); };
  44         467  
140 44 50       687 croak 'No string is given' unless @_;
141 44 50       97 croak 'More than one argument are given' unless @_ == 1;
142              
143 44         131 return $apply_range->($_[0]);
144             }
145              
146             1;
147              
148             __END__