File Coverage

blib/lib/Locale/Maketext/Utils/Phrase.pm
Criterion Covered Total %
statement 81 83 97.5
branch 46 52 88.4
condition 56 71 78.8
subroutine 16 16 100.0
pod 9 9 100.0
total 208 231 90.0


line stmt bran cond sub pod time code
1             package Locale::Maketext::Utils::Phrase;
2              
3 7     7   96156 use strict;
  7         14  
  7         417  
4 7     7   41 use warnings;
  7         16  
  7         196  
5 7     7   38 use Carp ();
  7         14  
  7         124  
6              
7 7     7   2931 use Module::Want ();
  7         4860  
  7         16571  
8              
9             $Locale::Maketext::Utils::Phrase::VERSION = '0.1';
10              
11             my $closing_bn = qr/(?
12             my $opening_bn = qr/(?
13             my $bn_delimit = qr/(?
14             my $bn_var_arg = qr/(?
15              
16             sub get_bn_var_regexp {
17 188     188 1 5251 return qr/(?
18             }
19              
20             sub get_non_translatable_type_regexp {
21 1     1 1 24 return qr/(?:var|meth|basic_var)/;
22             }
23              
24             sub string_has_opening_or_closing_bracket {
25 5   100 5 1 21084 return $_[0] =~ m/$opening_bn/ || $_[0] =~ m/$closing_bn/;
26             }
27              
28             sub phrase2struct {
29 120     120 1 28177 my ($phrase) = @_;
30              
31             # Makes parsing (via code or mentally) unnecessarily difficult.
32             # ? TODO ? s/~~/_TILDES_/g (yes w/ an S so _TILDE_ still works) then restore them inside the while loop and don’t croak() here (maybe carp()) ?
33 120 50       386 Carp::croak("Consecutive tildes are ambiguous (use the special placeholder _TILDE_ instead): “$phrase”") if $phrase =~ m/~~/;
34              
35 120 100       1343 return [$phrase] unless $phrase =~ m/(?:$opening_bn|$closing_bn)/;
36              
37 100         144 my @struct;
38 100         1500 while (
39             $phrase =~ m{
40             ( # Capture chunk of …
41             # bracket notation …
42             (?:
43             $opening_bn
44             ( # Capture bracket pair contents
45             (?:
46             \~\]
47             |
48             [^\]]
49             )*
50             )
51             $closing_bn
52             )
53             |
54             # … or non-bracket notation
55             (?:
56             \~[\[\]]
57             |
58             [^\[\]]
59             )+
60             ) # /Capture chunk of …
61             }gx
62             ) {
63 407         1002 my ( $match, $bn_inside ) = ( $1, $2 );
64              
65 407 100       754 if ( defined $bn_inside ) {
66 172 100       1498 if ( $bn_inside =~ m/(?:$closing_bn|$opening_bn)/ ) {
67 6         667 Carp::croak("Unbalanced bracket: “[$bn_inside]”");
68             }
69              
70 166         499 my $list = [ _split_bn_cont($bn_inside) ];
71 166         433 my $type = _get_bn_type_from_list($list);
72              
73 166         2814 push @struct,
74             {
75             'orig' => $match,
76             'cont' => $bn_inside,
77             'list' => $list,
78             'type' => $type,
79             };
80             }
81             else {
82              
83             # probably won't trip but for good measure
84 235 50       1659 if ( $match =~ m/(?:$opening_bn|$closing_bn)/ ) {
85 0         0 Carp::croak("Unbalanced bracket: “$match”");
86             }
87              
88 235         2697 push @struct, $match;
89             }
90             }
91              
92 94 50       245 return if !@struct;
93              
94             # if the structure rebuilds differently it means unbalanced [ or ] existed in $phrase that were masked out in @struct
95 94 100       305 if ( struct2phrase( \@struct ) ne $phrase ) {
96 10         1028 Carp::croak("Unbalanced bracket: “$phrase”");
97             }
98              
99 84         1258 return \@struct;
100             }
101              
102             sub struct2phrase {
103 98     98 1 2495 my ($struct) = @_;
104              
105 399 100       1417 return join(
106             '',
107 98         146 map { ref($_) ? $_->{'orig'} : $_ } @{$struct}
  98         232  
108             );
109             }
110              
111             sub phrase_has_bracket_notation {
112 8 100   8 1 3665 return 1 if $_[0] =~ m/$opening_bn/;
113 2         11 return;
114             }
115              
116             sub struct_has_bracket_notation {
117 4     4 1 7 my $len = @{ $_[0] };
  4         10  
118 4 100 100     59 return 1 if ( $len == 1 && ref( $_[0]->[0] ) ) || $len > 1;
      100        
119 1         6 return;
120             }
121              
122             sub phrase_is_entirely_bracket_notation {
123 8 100   8 1 1578 return 1 if $_[0] =~ m{\A$opening_bn(?:\~[\[\]]|[^\[\]])+$closing_bn\z}x;
124 6         28 return;
125             }
126              
127             sub struct_is_entirely_bracket_notation {
128 102 100 100 102 1 139 return 1 if @{ $_[0] } == 1 && ref( $_[0]->[0] );
  102         437  
129 93         632 return;
130             }
131              
132             sub _split_bn_cont {
133 389     389   134784 my ( $cont, $limit ) = @_;
134 389   100     1956 $limit = abs( int( $limit || 0 ) );
135 389 100       4483 return $limit ? split( $bn_delimit, $cont, $limit ) : split( $bn_delimit, $cont );
136             }
137              
138             my %meth = (
139             'numf' => 'Should be passing in an unformatted number.',
140             '#' => 'Should be passing in an unformatted number (numf alias).',
141             'format_bytes' => 'Should be passing in the unformatted number of bytes.',
142             'output' => sub {
143             return 'Should be passing in character identifier.' if $_[0]->[1] eq 'chr';
144             return 'Displayed without modification.' if $_[0]->[1] eq 'asis' || $_[0]->[1] eq 'asis_for_tests';
145             return 'No args, character.' if $_[0]->[1] =~ m/^(?:nbsp|amp|quot|apos|shy|lt|gt)/;
146             return 'Domain should be passed in. Hardcoded domain that needs translated should just be a string.' if $_[0]->[1] eq 'encode_puny' || $_[0]->[1] eq 'decode_puny';
147             return;
148             },
149             'datetime' => sub {
150             return 'format has no translatable components' if !$_[0]->[2] # there is no format (FWIW, 0 is not a valid format)
151             || $_[0]->[2] =~ m/\A(?:date|time|datetime)_format_(:full|long|medium|short|default)\z/ # it is a format method name
152             || $_[0]->[2] =~ m/\A[GgyYQqMmwWdDEeaAhHKkSszZvVuLFcj]+(?:{[0-9],?([0-9])?})?\z/; # is only CLDR Pattern codes …
153              
154             # … i.e. which includes values for format_for() AKA $loc->available_formats(),
155             # http://search.cpan.org/perldoc?DateTime#CLDR_Patterns says:
156             # If you want to include any lower or upper case ASCII characters as-is, you can surround them with single quotes (').
157             # If you want to include a single quote, you must escape it as two single quotes ('').
158             # Spaces and any non-letter text will always be passed through as-is.
159              
160             return;
161             },
162             'current_year' => 'Takes no args.',
163             'asis' => 'Displayed without modification.',
164             'comment' => 'Not displayed.',
165             'join' => 'Arbitrary args.',
166             'sprintf' => 'Arbitrary args.',
167             'convert' => 'Converts arbitrary units and identifiers.', # ? technically USD -> GBP, not critical ATM ?
168             'list_and' => 'Arbitrary args.',
169             'list_or' => 'Arbitrary args.',
170             'list_and_quoted' => 'Arbitrary args.',
171             'list_or_quoted' => 'Arbitrary args.',
172             'list' => 'Deprecated. Arbitrary args.',
173             );
174              
175             my %basic = (
176             'output' => 'has possible translatable parts',
177             'datetime' => 'has possible translatable components in format',
178             );
179              
180             my %complex = (
181             'url' => 'should have translatable parts',
182             'boolean' => 'should have translatable parts',
183             'is_defined' => 'should have translatable parts',
184             'is_future' => 'should have translatable parts',
185             'quant' => 'should have translatable parts',
186             '*' => 'should have translatable parts (quant alias)',
187             'numerate' => 'should have translatable parts',
188             );
189              
190             my $ns_regexp = Module::Want::get_ns_regexp();
191              
192             sub _get_attr_hash_from_list {
193 179     179   302 my ( $list, $start_idx ) = @_;
194              
195 179         209 my $last_list_idx = @{$list} - 1;
  179         380  
196              
197 179         264 my %attr;
198 179         201 my $skip_to = 0;
199 179         401 for my $i ( $start_idx .. $last_list_idx ) {
200 373 100       1133 next if $i < $skip_to;
201 193 100       962 next if $list->[$i] =~ m/\A$bn_var_arg\z/;
202              
203 180         519 $attr{ $list->[$i] } = $list->[ $i + 1 ];
204 180         327 $skip_to = $i + 2;
205             }
206              
207 179         918 return %attr;
208             }
209              
210             sub _get_bn_type_from_list {
211 382     382   178706 my ($list) = @_;
212 382         492 my $len = @{$list};
  382         700  
213              
214 382 100 100     2735 return 'var' if $len == 1 && $list->[0] =~ m/\A$bn_var_arg\z/;
215              
216             # recommend to carp/croak
217 275 100 66     4114 return '_invalid' if !defined $list->[0] || $list->[0] !~ m/\A(?:$ns_regexp|\*|\#)\z/;
218 261 100 66     3358 return '_invalid' if $list->[0] eq 'output' && ( !defined $list->[1] || $list->[1] !~ m/\A$ns_regexp\z/ );
      100        
219              
220             # should not be anything translatable
221 260 100 100     2055 return 'meth' if exists $meth{ $list->[0] } && ( ref( $meth{ $list->[0] } ) ne 'CODE' || $meth{ $list->[0] }->($list) );
      66        
222              
223 213 50 33     1269 if ( exists $basic{ $list->[0] } && ( ref( $basic{ $list->[0] } ) ne 'CODE' || $basic{ $list->[0] }->($list) ) ) {
      66        
224              
225             # check for 'basic_var' (might be basic except there are not any translatable parts)
226              
227 206 50       615 if ( $list->[0] eq 'output' ) {
228 206 100       878 if ( $list->[1] =~ m/\A(?:underline|strong|em|class|attr|inline|block|sup|sub)\z/ ) {
229 131         300 my %attr = _get_attr_hash_from_list( $list, 3 );
230              
231 131 100 100     1468 if ( $list->[2] =~ m/\A$bn_var_arg\z/
      66        
      100        
      66        
232             && ( !exists $attr{'title'} || $attr{'title'} =~ m/\A$bn_var_arg\z/ )
233             && ( !exists $attr{'alt'} || $attr{'alt'} =~ m/\A$bn_var_arg\z/ ) ) {
234 36         335 return 'basic_var';
235             }
236             }
237              
238 170 100       613 if ( $list->[1] =~ m/\A(?:img|abbr|acronym)\z/ ) {
239 42         95 my %attr = _get_attr_hash_from_list( $list, 4 );
240              
241             # if any of these are true (except maybe $list->[2]) w/ these functions
242             # then the caller is probably doing something wrong, the class/methods
243             # will help find those sort of things better.
244 42 100 66     806 if ( $list->[2] =~ m/\A$bn_var_arg\z/
      100        
      66        
      100        
      66        
245             && $list->[3] =~ m/\A$bn_var_arg\z/
246             && ( !exists $attr{'title'} || $attr{'title'} =~ m/\A$bn_var_arg\z/ )
247             && ( !exists $attr{'alt'} || $attr{'alt'} =~ m/\A$bn_var_arg\z/ ) ) {
248 12         115 return 'basic_var';
249             }
250             }
251             }
252              
253 158         1060 return 'basic';
254             }
255              
256 7 50 33     87 return 'complex' if exists $complex{ $list->[0] } && ( ref( $complex{ $list->[0] } ) ne 'CODE' || $complex{ $list->[0] }->($list) );
      33        
257 0           return '_unknown'; # recommend to treat like 'basic' unless its one you know about that your class defines or if it's a show stopper
258             }
259              
260             1;
261              
262             __END__