File Coverage

blib/lib/YATT/Lite/XHF.pm
Criterion Covered Total %
statement 150 171 87.7
branch 63 84 75.0
condition 19 32 59.3
subroutine 27 29 93.1
pod 4 16 25.0
total 263 332 79.2


line stmt bran cond sub pod time code
1             package YATT::Lite::XHF; sub MY () {__PACKAGE__}
2 22     22   1978 use strict;
  22         44  
  22         722  
3 22     22   118 use warnings qw(FATAL all NONFATAL misc);
  22         37  
  22         844  
4 22     22   112 use Carp;
  22         37  
  22         1269  
5 22     22   974 use utf8;
  22         18169  
  22         201  
6              
7             our $VERSION = "0.03";
8              
9 22     22   1102 use base qw(YATT::Lite::Object);
  22         44  
  22         3219  
10 22         134 use fields qw(cf_FH cf_filename cf_string cf_tokens
11             fh_configured
12             cf_allow_empty_name
13             cf_encoding cf_crlf
14             cf_nocr cf_subst
15 22     22   117 cf_skip_comment cf_bytes);
  22         34  
16              
17 22     22   2681 use Exporter qw(import);
  22         60  
  22         1735  
18             our @EXPORT = qw(read_file_xhf);
19             our @EXPORT_OK = (@EXPORT, qw(parse_xhf $cc_name));
20              
21 22     22   8355 use YATT::Lite::Util;
  22         50  
  22         3107  
22 22     22   12168 use YATT::Lite::Util::Enum _ => [qw(NAME SIGIL VALUE)];
  22         55  
  22         176  
23              
24             our $cc_name = qr{[0-9A-Za-z_\.\-/~!]};
25             our $re_suffix= qr{\[$cc_name*\]};
26             our $cc_sigil = qr{[:\#,\-=\[\]\{\}]};
27             our $cc_tabsp = qr{[\ \t]};
28              
29             our %OPN = ('[' => \&organize_array, '{' => \&organize_hash
30             , '=' => \&organize_expr);
31             our %CLO = (']' => '[', '}' => '{');
32             our %NAME_LESS = (%CLO, '-' => 1);
33             our %ALLOW_EMPTY_NAME = (':' => 1);
34              
35             sub after_new {
36 87     87 1 132 (my MY $self) = @_;
37 87   50     511 $self->{cf_skip_comment} //= 1;
38             }
39              
40             sub read_file_xhf {
41 13     13 1 35 my ($pack, $fn, @rest) = @_;
42 13         52 MY->new(filename => $fn, encoding => 'utf8', @rest)->read;
43             }
44              
45             sub parse_xhf {
46 3     3 1 87 MY->new(string => @_)->read;
47             }
48              
49             *configure_file = \&configure_filename;
50             *configure_file = \&configure_filename;
51             sub configure_filename {
52 23     23 0 46 (my MY $self, my ($fn)) = @_;
53 23 50       1069 open $self->{cf_FH}, '<', $fn
54             or croak "Can't open file '$fn': $!";
55 23         51 $self->{fh_configured} = 0;
56 23         48 $self->{cf_filename} = $fn;
57 23         117 $self;
58             }
59              
60             # To accept in-stream encoding spec.
61             # (See YATT::Lite::Test::XHFTest::load and t/lite_xhf.t)
62             sub configure_encoding {
63 14     14 0 26 (my MY $self, my $value) = @_;
64 14         29 $self->{fh_configured} = 0;
65 14         71 $self->{cf_encoding} = $value;
66             }
67              
68             sub configure_binary {
69 0     0 0 0 (my MY $self, my $value) = @_;
70 0         0 warnings::warnif(deprecated =>
71             "XHF option 'binary' is deprecated, use 'bytes' instead");
72 0         0 $self->{cf_bytes} = $value;
73             }
74              
75             sub configure_string {
76 64     64 0 97 my MY $self = shift;
77 64         148 ($self->{cf_string}) = @_;
78             open $self->{cf_FH}, '<', \ $self->{cf_string}
79 64 50   2   721 or croak "Can't create string stream: $!";
  2         12  
  2         4  
  2         17  
80 64         2542 $self;
81             }
82              
83             # XXX: Should I rename this to read_one()?
84             sub read {
85 269     269 1 478 my MY $self = shift;
86             $self->cf_let(\@_, sub {
87 269 100   269   557 if (my @tokens = $self->tokenize) {
88 245         647 $self->organize(@tokens);
89             } else {
90 24         147 return;
91             }
92 269         1497 });
93             }
94              
95             sub tokenize {
96 269     269 0 337 (my MY $self) = @_;
97 269         888 local $/ = "";
98 269         458 my $fh = $$self{cf_FH};
99 269 100       694 unless ($self->{fh_configured}++) {
100 88 100 66     513 if (not $self->{cf_bytes} and not $self->{cf_string}
      66        
101             and $self->{cf_encoding}) {
102 14         141 binmode $fh, ":encoding($self->{cf_encoding})";
103             }
104 88 50       766 if ($self->{cf_crlf}) {
105 0         0 binmode $fh, ":crlf";
106             }
107             }
108              
109 269         310 my @tokens;
110             LOOP: {
111 269         303 do {
112 269 100       2333 defined (my $para = <$fh>) or last LOOP;
113             $para = untaint_unless_tainted
114             ($self->{cf_filename} // $self->{cf_string}
115 252   66     1245 , $para);
116 252         620 @tokens = $self->tokenize_1($para);
117 269   66     318 } until (not $self->{cf_skip_comment} or @tokens);
118             }
119 269         1387 @tokens;
120             }
121              
122             sub tokenize_1 {
123 252     252 0 320 my MY $reader = shift;
124 252         1427 $_[0] =~ s{\n+$}{\n}s;
125 252 50       654 $_[0] =~ s{\r+}{}g if $reader->{cf_nocr};
126 252 50       576 if (my $sub = $reader->{cf_subst}) {
127 0         0 local $_;
128 0         0 *_ = \ $_[0];
129 0         0 $sub->($_);
130             }
131 252         293 my ($pos, $ncomments, @tokens, @result);
132 252         4499 foreach my $token (@tokens = split /(?<=\n)(?=[^\ \t])/, $_[0]) {
133 1200         1544 $pos++;
134 1200 100       2627 if ($token =~ s{^(?:\#[^\n]*(?:\n|$))+}{}) {
135 25         29 $ncomments++;
136 25 50       76 next if $token eq '';
137             }
138              
139 1175 50       9060 unless ($token =~ s{^($cc_name*$re_suffix*) ($cc_sigil) (?:($cc_tabsp)|(\n|$))}{}x) {
140 0         0 croak "Invalid XHF token '$token': line " . token_lineno(\@tokens, $pos);
141             }
142 1175         3370 my ($name, $sigil, $tabsp, $eol) = ($1, $2, $3, $4);
143              
144 1175 100       2988 if ($name eq '') {
    50          
145             croak "Invalid XHF token(name is empty for '$token')"
146 503 50 33     1399 if $sigil eq ':' and not $reader->{cf_allow_empty_name};
147             } elsif ($NAME_LESS{$sigil}) {
148 0         0 croak "Invalid XHF token('$sigil' should not be prefixed by name '$name')"
149             }
150              
151             # Comment fields are ignored.
152 1175 50       2380 $ncomments++, next if $sigil eq "#";
153              
154 1175 100       2540 if ($CLO{$sigil}) {
155 180         256 undef $name;
156             }
157              
158             # Line continuation.
159 1175         2380 $token =~ s/\n[\ \t]/\n/g;
160              
161 1175 100       2177 unless (defined $eol) {
162             # Values are trimmed unless $eol
163 547         2762 $token =~ s/^\s+|\s+$//gs;
164             } else {
165             # Deny: name{ foo
166             # Allow: name[ foo
167             croak "Invalid XHF token(container with value): "
168 628 50 66     1583 . join("", grep {defined $_} $name, $sigil, $tabsp, $token)
  0         0  
169             if $sigil eq '{' and $token ne "";
170              
171             # Trim leading space for $tabsp eq "\n".
172 628         1216 $token =~ s/^[\ \t]//;
173             }
174 1175         4069 push @result, [$name, $sigil, $token];
175             }
176              
177             # Comment only paragraph should return nothing.
178 252 100 100     708 return if $ncomments && !@result;
179              
180 245 50       2224 wantarray ? @result : \@result;
181             }
182              
183             sub token_lineno {
184 0     0 0 0 my ($tokens, $pos) = @_;
185 0         0 my $lineno = 1;
186 0         0 $lineno += tr|\n|| for grep {defined} @$tokens[0 .. $pos];
  0         0  
187 0         0 $lineno;
188             }
189              
190             sub organize {
191 245     245 0 322 my MY $reader = shift;
192 245         308 my @result;
193 245         583 while (@_) {
194 679         860 my $desc = shift;
195 679 100       1526 unless (defined $desc->[_NAME]) {
196 2         385 croak "Invalid XHF: Field close '$desc->[_SIGIL]' without open!";
197             }
198             push @result, $desc->[_NAME] if $desc->[_NAME] ne ''
199 677 100 66     2226 or $ALLOW_EMPTY_NAME{$desc->[_SIGIL]};
200 677 100       1511 if (my $sub = $OPN{$desc->[_SIGIL]}) {
201             # sigil がある時、value を無視して、良いのか?
202 140         328 push @result, $sub->($reader, \@_, $desc);
203             } else {
204 537         1473 push @result, $desc->[_VALUE];
205             }
206             }
207 239 100       424 if (wantarray) {
208             @result
209 234         2068 } else {
210 5         19 my %hash = @result;
211 5         30 \%hash;
212             }
213             }
214              
215             # '[' block
216             sub organize_array {
217 160     160 0 293 (my MY $reader, my ($tokens, $first)) = @_;
218 160         214 my @result;
219 160 50 33     719 push @result, $first->[_VALUE] if defined $first and $first->[_VALUE] ne '';
220 160         352 while (@$tokens) {
221 437         595 my $desc = shift @$tokens;
222             # NAME
223 437 100       1124 unless (defined $desc->[_NAME]) {
    100          
224 159 100       340 if ($desc->[_SIGIL] ne ']') {
225 1         197 croak "Invalid XHF: paren mismatch. '[' is closed by '$desc->[_SIGIL]'";
226             }
227 158         543 return \@result;
228             }
229             elsif ($desc->[_NAME] ne '') {
230 20         40 push @result, $desc->[_NAME];
231             }
232             # VALUE
233 278 100       611 if (my $sub = $OPN{$desc->[_SIGIL]}) {
234             # sigil がある時、value があったらどうするかは、子供次第。
235 57         125 push @result, $sub->($reader, $tokens, $desc);
236             }
237             else {
238 221         630 push @result, $desc->[_VALUE];
239             }
240             }
241 1         245 croak "Invalid XHF: Missing close ']'";
242             }
243              
244             # '{' block.
245             sub organize_hash {
246 20     20 0 38 (my MY $reader, my ($tokens, $first)) = @_;
247 20 50 33     104 die "Invalid XHF hash block beginning! ". join("", @$first)
248             if defined $first and $first->[_VALUE] ne '';
249 20         25 my %result;
250 20         48 while (@$tokens) {
251 52         87 my $desc = shift @$tokens;
252             # NAME
253 52 100       147 unless (defined $desc->[_NAME]) {
    100          
254 19 100       51 if ($desc->[_SIGIL] ne '}') {
255 1         169 croak "Invalid XHF: paren mismatch. '{' is closed by '$desc->[_SIGIL]'";
256             }
257 18         70 return \%result;
258             }
259             elsif ($desc->[_SIGIL] eq '-') {
260             # Should treat two lines as one key value pair.
261 7 50       16 unless (@$tokens) {
262 0         0 croak "Invalid XHF hash:"
263             ." key '- $desc->[_VALUE]' doesn't have value!";
264             }
265 7         13 my $valdesc = shift @$tokens;
266 7         9 my $value = do {
267 7 100       26 if (my $sub = $OPN{$valdesc->[_SIGIL]}) {
    50          
268 2         4 $sub->($reader, $tokens, $valdesc);
269             } elsif ($valdesc->[_SIGIL] eq '-') {
270 5         13 $valdesc->[_VALUE];
271             } else {
272 0         0 croak "Invalid XHF hash value:"
273             . " key '$desc->[_VALUE]' has invalid sigil '$valdesc->[_SIGIL]'";
274             }
275             };
276 7         37 $reader->add_value($result{$desc->[_VALUE]}, $value);
277             } else {
278 26 100       76 if (my $sub = $OPN{$desc->[_SIGIL]}) {
279             # sigil がある時、value を無視して、良いのか?
280 6         24 $desc->[_VALUE] = $sub->($reader, $tokens, $desc);
281             }
282 26         98 $reader->add_value($result{$desc->[_NAME]}, $desc->[_VALUE]);
283             }
284             }
285 1         168 croak "Invalid XHF: Missing close '}'";
286             }
287              
288             # '=' value
289 25     25   94 sub _undef {undef}
290             our %EXPR = (null => \&_undef, 'undef' => \&_undef);
291             sub organize_expr {
292 25     25 0 44 (my MY $reader, my ($tokens, $first)) = @_;
293 25 50       127 if ((my $val = $first->[_VALUE]) =~ s/^\#(\w+)\s*//) {
294 25 50       82 my $sub = $EXPR{$1}
295             or croak "Invalid XHF keyword: '= #$1'";
296 25         52 $sub->($reader, $val, $tokens);
297             } else {
298 0         0 croak "Not yet implemented XHF token: '@$first'";
299             }
300             }
301              
302             sub add_value {
303 33     33 0 42 my MY $reader = shift;
304 33 50       85 unless (defined $_[0]) {
    0          
305 33         167 $_[0] = $_[1];
306             } elsif (ref $_[0] ne 'ARRAY') {
307 0         0 $_[0] = [$_[0], $_[1]];
308             } else {
309 0         0 push @{$_[0]}, $_[1];
  0         0  
310             }
311             }
312              
313 22     22   10102 use YATT::Lite::Breakpoint;
  22         54  
  22         1744  
314             YATT::Lite::Breakpoint::break_load_xhf();
315              
316             1;
317              
318             __END__