File Coverage

blib/lib/YATT/Lite/XHF.pm
Criterion Covered Total %
statement 170 195 87.1
branch 61 84 72.6
condition 25 39 64.1
subroutine 31 34 91.1
pod 4 20 20.0
total 291 372 78.2


line stmt bran cond sub pod time code
1             package YATT::Lite::XHF; sub MY () {__PACKAGE__}
2 30     30   1520 use strict;
  30         70  
  30         892  
3 30     30   153 use warnings qw(FATAL all NONFATAL misc);
  30         63  
  30         972  
4 30     30   153 use Carp;
  30         63  
  30         1419  
5 30     30   778 use utf8;
  30         12407  
  30         185  
6              
7             our $VERSION = "0.03";
8              
9 30     30   1616 use constant TRACE => $ENV{TRACE_XHF_PARSER};
  30         69  
  30         2161  
10              
11 30     30   195 use base qw(YATT::Lite::Object);
  30         73  
  30         3199  
12 30         160 use fields qw(cf_FH cf_filename cf_string cf_tokens
13             fh_configured
14             cf_allow_empty_name
15             cf_encoding cf_crlf
16             cf_nocr cf_subst
17             cf_first_lineno
18             _depth
19 30     30   197 cf_skip_comment cf_bytes);
  30         72  
20              
21 30     30   4425 use Exporter qw(import);
  30         67  
  30         1848  
22             our @EXPORT = qw(read_file_xhf);
23             our @EXPORT_OK = (@EXPORT, qw(parse_xhf $cc_name));
24              
25 30     30   7670 use YATT::Lite::Util;
  30         86  
  30         3384  
26 30     30   11644 use YATT::Lite::Util::Enum _ => [qw(NAME SIGIL VALUE LINENO)];
  30         80  
  30         251  
27              
28             our $cc_name = qr{[0-9A-Za-z_\.\-/~!]};
29             our $re_suffix= qr{\[$cc_name*\]};
30             our $cc_sigil = qr{[:\#,\-=\[\]\{\}]};
31             our $cc_tabsp = qr{[\ \t]};
32              
33             our %OPN = ('[' => \&organize_array, '{' => \&organize_hash
34             , '=' => \&organize_expr);
35             our %CLO = (']' => '[', '}' => '{');
36             our %NAME_LESS = (%CLO, '-' => 1);
37             our %ALLOW_EMPTY_NAME = (':' => 1);
38              
39             sub after_new {
40 94     94 1 198 (my MY $self) = @_;
41 94   50     504 $self->{cf_skip_comment} //= 1;
42             }
43              
44             sub read_file_xhf {
45 20     20 1 57 my ($pack, $fn, @rest) = @_;
46 20         74 MY->new(filename => $fn, encoding => 'utf8', @rest)->read;
47             }
48              
49             sub parse_xhf {
50 3     3 1 94 MY->new(string => @_)->read;
51             }
52              
53             *configure_file = \&configure_filename;
54             *configure_file = \&configure_filename;
55             sub configure_filename {
56 30     30 0 66 (my MY $self, my ($fn)) = @_;
57 30 50       1235 open $self->{cf_FH}, '<', $fn
58             or croak "Can't open file '$fn': $!";
59 30         93 $self->{fh_configured} = 0;
60 30         69 $self->{cf_filename} = $fn;
61 30         142 $self;
62             }
63              
64             sub configure_filename_for_error {
65 1     1 0 3 (my MY $self, my ($fn)) = @_;
66 1         4 $self->{cf_filename} = $fn;
67             }
68              
69             # To accept in-stream encoding spec.
70             # (See YATT::Lite::Test::XHFTest::load and t/lite_xhf.t)
71             sub configure_encoding {
72 21     21 0 50 (my MY $self, my $value) = @_;
73 21         36 $self->{fh_configured} = 0;
74 21         82 $self->{cf_encoding} = $value;
75             }
76              
77             sub configure_binary {
78 0     0 0 0 (my MY $self, my $value) = @_;
79 0         0 warnings::warnif(deprecated =>
80             "XHF option 'binary' is deprecated, use 'bytes' instead");
81 0         0 $self->{cf_bytes} = $value;
82             }
83              
84             sub configure_string {
85 64     64 0 126 my MY $self = shift;
86 64         159 ($self->{cf_string}) = @_;
87             open $self->{cf_FH}, '<', \ $self->{cf_string}
88 64 50       794 or croak "Can't create string stream: $!";
89 64         2365 $self;
90             }
91              
92             sub trace {
93 0     0 0 0 (my MY $reader, my ($msg, @desc)) = @_;
94 0         0 print STDERR " " x $reader->{_depth}, $msg, terse_dump(@desc), "\n";
95             }
96              
97             sub read_all {
98 0     0 0 0 (my MY $self) = @_;
99 0         0 my @res;
100 0         0 while (my @block = $self->read) {
101 0         0 push @res, @block;
102             }
103 0 0       0 wantarray ? @res : \@res;
104             }
105              
106             # XXX: Should I rename this to read_one()?
107             sub read {
108 283     283 1 543 my MY $self = shift;
109             $self->cf_let(\@_, sub {
110 283 100   283   645 if (my @tokens = $self->tokenize) {
111 259         836 $self->organize(@tokens);
112             } else {
113 24         188 return;
114             }
115 283         1813 });
116             }
117              
118             sub tokenize {
119 283     283 0 505 (my MY $self) = @_;
120 283         1040 local $/ = "";
121 283         555 my $fh = $$self{cf_FH};
122 283 100       785 unless ($self->{fh_configured}++) {
123 95 100 66     556 if (not $self->{cf_bytes} and not $self->{cf_string}
      100        
124             and $self->{cf_encoding}) {
125 21     3   234 binmode $fh, ":encoding($self->{cf_encoding})";
  3         24  
  3         7  
  3         22  
126             }
127 95 50       1988 if ($self->{cf_crlf}) {
128 0         0 binmode $fh, ":crlf";
129             }
130             }
131              
132 283         500 my @tokens;
133             LOOP: {
134 283         453 do {
135 283 100       2655 defined (my $para = <$fh>) or last LOOP;
136             $para = untaint_unless_tainted
137             ($self->{cf_filename} // $self->{cf_string}
138 266   66     1519 , $para);
139 266         708 @tokens = $self->tokenize_1($para);
140 283   66     402 } until (not $self->{cf_skip_comment} or @tokens);
141             }
142 283         1263 @tokens;
143             }
144              
145             sub tokenize_1 {
146 266     266 0 446 my MY $reader = shift;
147 266         1590 $_[0] =~ s{\n+$}{\n}s;
148 266 50       803 $_[0] =~ s{\r+}{}g if $reader->{cf_nocr};
149 266 50       667 if (my $sub = $reader->{cf_subst}) {
150 0         0 local $_;
151 0         0 *_ = \ $_[0];
152 0         0 $sub->($_);
153             }
154 266   100     988 my $lineno = $reader->{cf_first_lineno} // 1;
155 266         478 my ($pos, $ncomments, @tokens, @result);
156 266         3383 foreach my $token (@tokens = split /(?<=\n)(?=[^\ \t])/, $_[0]) {
157 1280         1909 $pos++;
158 1280 100       2833 if ($token =~ s{^(?:\#[^\n]*(?:\n|$))+}{}) {
159 25         44 $ncomments++;
160 25 50       68 next if $token eq '';
161             }
162              
163 1255 50       7454 unless ($token =~ s{^($cc_name*$re_suffix*) ($cc_sigil) (?:($cc_tabsp)|(\n|$))}{}x) {
164 0         0 croak "Invalid XHF token '$token' ".$reader->fileinfo_lineno($lineno)."\n";
165             }
166 1255         3834 my ($name, $sigil, $tabsp, $eol) = ($1, $2, $3, $4);
167              
168 1255 100       3120 if ($name eq '') {
    50          
169             croak "Invalid XHF token(name is empty for '$token') "
170             .$reader->fileinfo_lineno($lineno)."\n"
171 546 50 33     1307 if $sigil eq ':' and not $reader->{cf_allow_empty_name};
172             } elsif ($NAME_LESS{$sigil}) {
173 0         0 croak "Invalid XHF token('$sigil' should not be prefixed by name '$name') "
174             .$reader->fileinfo_lineno($lineno)."\n";
175             }
176              
177             # Comment fields are ignored.
178 1255 50       2500 $ncomments++, next if $sigil eq "#";
179              
180 1255 100       2721 if ($CLO{$sigil}) {
181 194         320 undef $name;
182             }
183              
184             # Line continuation.
185 1255         2631 $token =~ s/\n[\ \t]/\n/g;
186              
187 1255 100       2459 unless (defined $eol) {
188             # Values are trimmed unless $eol
189 589         2288 $token =~ s/^\s+|\s+$//gs;
190             } else {
191             # Deny: name{ foo
192             # Allow: name[ foo
193             croak "Invalid XHF token(container with value) "
194 666 50 66     1590 . join("", grep {defined $_} $name, $sigil, $tabsp, $token)
  0         0  
195             . $reader->fileinfo_lineno($lineno)."\n"
196             if $sigil eq '{' and $token ne "";
197              
198             # Trim leading space for $tabsp eq "\n".
199 666         1264 $token =~ s/^[\ \t]//;
200             }
201 1255         3626 push @result, [$name, $sigil, $token, $lineno];
202             } continue {
203 1280         2225 $lineno++;
204             }
205              
206             # Comment only paragraph should return nothing.
207 266 100 100     747 return if $ncomments && !@result;
208              
209 259 50       1917 wantarray ? @result : \@result;
210             }
211              
212             sub fileinfo {
213 6     6 0 13 (my MY $reader, my $desc) = @_;
214 6         14 $reader->fileinfo_lineno($desc->[_LINENO]);
215             }
216              
217             sub fileinfo_lineno {
218 6     6 0 12 (my MY $reader, my $lineno) = @_;
219             sprintf("at %s line %d"
220 6   50     803 , $reader->{cf_filename} // "(unknown)"
221             , $lineno);
222             }
223              
224             sub organize {
225 259     259 0 428 my MY $reader = shift;
226 259         584 local $reader->{_depth} = -1;
227 259         394 my $pos = 0;
228 259         386 my @result;
229 259         685 while ($pos < @_) {
230 710         1408 my $desc = $_[$pos++];
231 710 100       1603 unless (defined $desc->[_NAME]) {
232 2         9 croak "Invalid XHF: Field close '$desc->[_SIGIL]'"
233             ." (line $desc->[_LINENO]) without open! "
234             .$reader->fileinfo($desc)."\n";
235             }
236             push @result, $desc->[_NAME] if $desc->[_NAME] ne ''
237 708 100 66     2052 or $ALLOW_EMPTY_NAME{$desc->[_SIGIL]};
238 708 100       1544 if (my $sub = $OPN{$desc->[_SIGIL]}) {
239             # sigil がある時、value を無視して、良いのか?
240 154         413 push @result, $sub->($reader, \$pos, \@_, $desc);
241             } else {
242 554         1341 push @result, $desc->[_VALUE];
243             }
244             }
245 253 100       557 if (wantarray) {
246             @result
247 248         2742 } else {
248 5         18 my %hash = @result;
249 5         37 \%hash;
250             }
251             }
252              
253             # '[' block
254             sub organize_array {
255 174     174 0 353 (my MY $reader, my ($posref, $tokens, $first)) = @_;
256 174         379 local $reader->{_depth} = $reader->{_depth} + 1;
257 174         257 $reader->trace("> ", $first) if TRACE;
258 174         258 my @result;
259 174 50 33     724 push @result, $first->[_VALUE] if defined $first and $first->[_VALUE] ne '';
260 174         399 while ($$posref < @$tokens) {
261 486         741 my $desc = $tokens->[$$posref++];
262             # NAME
263 486 100 66     1095 unless (defined $desc->[_NAME]) {
264 173 100       377 if ($desc->[_SIGIL] ne ']') {
265 1         6 croak "Invalid XHF: paren mismatch. '['"
266             ." (line $first->[_LINENO]) is closed by '$desc->[_SIGIL]' "
267             .$reader->fileinfo($desc)."\n";
268             }
269 172         225 $reader->trace("< ", $first, $desc) if TRACE;
270 172         614 return \@result;
271             }
272             elsif ($desc->[_NAME] ne '') {
273             $reader->trace("| ", $desc) if TRACE;
274             push @result, $desc->[_NAME];
275             }
276             # VALUE
277 313 100       628 if (my $sub = $OPN{$desc->[_SIGIL]}) {
278             # sigil がある時、value があったらどうするかは、子供次第。
279 57         160 push @result, $sub->($reader, $posref, $tokens, $desc);
280             }
281             else {
282 256         347 $reader->trace("| ", $desc) if TRACE;
283 256         645 push @result, $desc->[_VALUE];
284             }
285             }
286 1         5 croak "Invalid XHF: Missing close ']' for '[' "
287             .$reader->fileinfo($first)."\n";
288             }
289              
290             # '{' block.
291             sub organize_hash {
292 20     20 0 48 (my MY $reader, my ($posref, $tokens, $first)) = @_;
293 20 50 33     99 croak "Invalid XHF hash block beginning! "
294             . join("", @$first).$reader->fileinfo($first)."\n"
295             if defined $first and $first->[_VALUE] ne '';
296 20         53 local $reader->{_depth} = $reader->{_depth} + 1;
297 20         32 $reader->trace("> ", $first) if TRACE;
298 20         31 my %result;
299 20         55 while ($$posref < @$tokens) {
300 52         87 my $desc = $tokens->[$$posref++];
301             # NAME
302 52 100       133 unless (defined $desc->[_NAME]) {
    100          
303 19 100       51 if ($desc->[_SIGIL] ne '}') {
304 1         6 croak "Invalid XHF: paren mismatch. '{'"
305             ." (line $first->[_LINENO]) is closed by '$desc->[_SIGIL]' "
306             .$reader->fileinfo($desc)."\n";
307             }
308 18         27 $reader->trace("< ", $first, $desc) if TRACE;
309 18         72 return \%result;
310             }
311 0         0 elsif ($desc->[_SIGIL] eq '-') {
312             # Should treat two lines as one key value pair.
313 7 50       19 unless ($$posref < @$tokens) {
314 0         0 croak "Invalid XHF hash:"
315             ." key '- $desc->[_VALUE]' doesn't have value! "
316             .$reader->fileinfo($desc)."\n";
317             }
318 7         13 my $valdesc = $tokens->[$$posref++];
319 7         12 my $value = do {
320 7 100       23 if (my $sub = $OPN{$valdesc->[_SIGIL]}) {
    50          
321 2         12 $sub->($reader, $posref, $tokens, $valdesc);
322             } elsif ($valdesc->[_SIGIL] eq '-') {
323 5         14 $valdesc->[_VALUE];
324             } else {
325 0         0 croak "Invalid XHF hash value:"
326             . " key '$desc->[_VALUE]' has invalid sigil '$valdesc->[_SIGIL]' "
327             .$reader->fileinfo($valdesc)."\n"
328             }
329             };
330 7         45 $reader->add_value($result{$desc->[_VALUE]}, $value);
331             } else {
332 26         38 $reader->trace("| ", $desc) if TRACE;
333 26 100       67 if (my $sub = $OPN{$desc->[_SIGIL]}) {
334             # sigil がある時、value を無視して、良いのか?
335 6         28 $desc->[_VALUE] = $sub->($reader, $posref, $tokens, $desc);
336             }
337 26         86 $reader->add_value($result{$desc->[_NAME]}, $desc->[_VALUE]);
338             }
339             }
340 1         4 croak "Invalid XHF: Missing close '}' for '{' "
341             .$reader->fileinfo($first)."\n";
342             }
343              
344             # '=' value
345 25     25   118 sub _undef {undef}
346             our %EXPR = (null => \&_undef, 'undef' => \&_undef);
347             sub organize_expr {
348 25     25 0 70 (my MY $reader, my ($posref, $tokens, $first)) = @_;
349 25 50       163 if ((my $val = $first->[_VALUE]) =~ s/^\#(\w+)\s*//) {
350 25 50       131 my $sub = $EXPR{$1}
351             or croak "Invalid XHF keyword: '= #$1'";
352 25         73 $sub->($reader, $val, $tokens);
353             } else {
354 0         0 croak "Not yet implemented XHF token: '@$first'";
355             }
356             }
357              
358             sub add_value {
359 33     33 0 55 my MY $reader = shift;
360 33 0       93 unless (defined $_[0]) {
    50          
361 33         159 $_[0] = $_[1];
362 0         0 } elsif (ref $_[0] ne 'ARRAY') {
363 0         0 $_[0] = [$_[0], $_[1]];
364             } else {
365 0         0 push @{$_[0]}, $_[1];
  0         0  
366             }
367             }
368              
369 30     30   10185 use YATT::Lite::Breakpoint;
  30         71  
  30         1959  
370             YATT::Lite::Breakpoint::break_load_xhf();
371              
372             1;
373              
374             __END__