File Coverage

blib/lib/Text/TokenStream.pm
Criterion Covered Total %
statement 77 79 97.4
branch 11 18 61.1
condition 3 7 42.8
subroutine 18 20 90.0
pod 9 9 100.0
total 118 133 88.7


line stmt bran cond sub pod time code
1             package Text::TokenStream;
2              
3 1     1   84155 use v5.12;
  1         9  
4 1     1   616 use Moo;
  1         11793  
  1         8  
5              
6             our $VERSION = '0.04';
7              
8 1     1   1694 use List::Util qw(max);
  1         3  
  1         117  
9 1     1   552 use Types::Path::Tiny qw(Path);
  1         124347  
  1         9  
10 1     1   427 use Types::Standard qw(ArrayRef Int Maybe ScalarRef Str);
  1         3  
  1         6  
11 1     1   1630 use Text::TokenStream::Token;
  1         56  
  1         53  
12 1     1   9 use Text::TokenStream::Types qw(Lexer Position TokenClass);
  1         2  
  1         11  
13              
14 1     1   756 use namespace::clean;
  1         3  
  1         8  
15              
16             has input_name => (is => 'ro', isa => Maybe[Path], coerce => 1, default => undef);
17              
18             has input => (is => 'ro', isa => Str, required => 1);
19              
20             has lexer => (
21             is => 'ro',
22             isa => Lexer,
23             required => 1,
24             handles => { next_lexer_token => 'next_token' },
25             );
26              
27             has token_class => (
28             is => 'lazy',
29             isa => TokenClass,
30 0     0   0 builder => sub { 'Text::TokenStream::Token' },
31             );
32              
33             has _pending => (is => 'ro', isa => ArrayRef, default => sub { [] });
34              
35             has _input_ref => (is => 'lazy', isa => ScalarRef[Str], builder => sub {
36 1     1   14 my ($self) = @_;
37 1         10 my $copy = $self->input;
38 1         18 return \$copy;
39             });
40              
41             has current_position => (
42             is => 'ro',
43             writer => '_set_current_position',
44             isa => Position,
45             default => 0,
46             init_arg => undef,
47             );
48              
49             with qw(Text::TokenStream::Role::Stream);
50              
51             sub next {
52 8     8 1 1843 my ($self) = @_;
53 8 50       19 $self->fill(1) or return undef;
54 8         13 my $tok = shift @{ $self->_pending };
  8         22  
55 8         157 $self->_set_current_position( $tok->position + length($tok->text) );
56 8         227 return $tok;
57             }
58              
59             sub fill {
60 23     23 1 1440 my ($self, $n) = @_;
61              
62 23         477 my $input_ref = $self->_input_ref;
63 23         221 my $input_len = length($self->input);
64              
65 23         49 my $pending = $self->_pending;
66 23         62 while (@$pending < $n) {
67 11   50     263 my $tok = $self->next_lexer_token($input_ref) // return 0;
68 11         28 my $position = $input_len - length($$input_ref) - length($tok->{text});
69 11         46 push @$pending, $self->create_token(%$tok, position => $position);
70             }
71              
72 23         312 return 1;
73             }
74              
75             sub create_token {
76 11     11 1 43 my ($self, %data) = @_;
77 11         200 return $self->token_class->new(%data);
78             }
79              
80             sub peek {
81 12     12 1 1464 my ($self) = @_;
82 12 50       27 $self->fill(1) or return undef;
83 12         50 return $self->_pending->[0];
84             }
85              
86             sub skip_optional {
87 2     2 1 7 my ($self, $target) = @_;
88 2   50     6 my $tok = $self->peek // return 0;
89 2 100       8 return 0 if !$tok->matches($target);
90 1         5 $self->next; # ignore return
91 1         7 return 1;
92             }
93              
94             sub looking_at {
95 2     2 1 1393 my ($self, @targets) = @_;
96              
97 2 50       8 $self->fill(scalar @targets) or return 0;
98              
99 2         7 my $pending = $self->_pending;
100 2         8 for my $i (0 .. $#targets) {
101 3 50       15 return 0 if !$pending->[$i]->matches($targets[$i]);
102             }
103              
104 2         20 return 1;
105             }
106              
107             sub next_of {
108 3     3 1 3239 my ($self, $target, $where) = @_;
109 3   33     9 my $tok = $self->peek
110             // $self->err(join ' ', "Missing token", grep defined, $where);
111 3 100       12 $self->token_err($tok, join ' ', "Unexpected", $tok->type, "token", grep defined, $where)
112             if !$tok->matches($target);
113 1         4 return $self->next;
114             }
115              
116             sub _err {
117 2     2   6 my ($self, $token, @message) = @_;
118 2 50       26 my $position = $token ? $token->position : $self->current_position;
119 2         19 my $marker = '^' x max(6, map length($_->text), grep defined, $token);
120 2         5 my $input = $self->input;
121 2         6 my $prefix = substr $input, 0, $position;
122 2         11 (my $line_prefix = $prefix) =~ s/^.*\n//s;
123 2         8 (my $space_prefix = $line_prefix) =~ tr/\t/ /c;
124 2         12 (my $line_suffix = substr $input, $position) =~ s/\r?\n.*//s;
125 2         14 my $line_number = 1 + ($prefix =~ tr/\n//);
126 2         5 my $column_number = 1 + length $line_prefix;
127 2         7 my $input_name = $self->input_name;
128 2 50       7 my $file_line = defined $input_name ? "File $input_name, line" : "Line";
129 2 50       6 @message = q[Something's wrong] if !@message;
130 2         10 my $message = join '', (
131             "SORRY! $file_line $line_number, column $column_number: ", @message, "\n",
132             $line_prefix, $line_suffix, "\n",
133             $space_prefix, $marker, "\n",
134             );
135 2         16 die $message;
136             }
137              
138 2     2 1 6 sub token_err { shift->_err( @_) }
139 0     0 1   sub err { shift->_err(undef, @_) }
140              
141             1;
142             __END__