File Coverage

blib/lib/English/Script.pm
Criterion Covered Total %
statement 207 208 99.5
branch 93 114 81.5
condition 19 36 52.7
subroutine 32 32 100.0
pod 8 8 100.0
total 359 398 90.2


line stmt bran cond sub pod time code
1             package English::Script {
2             # ABSTRACT: Parse English subset and convert to data or code
3              
4 11     11   2530910 use 5.014;
  11         94  
5 11     11   5384 use exact;
  11         374375  
  11         46  
6              
7 11     11   43076 use Parse::RecDescent;
  11         386796  
  11         87  
8 11     11   6480 use YAML::XS 'Dump';
  11         32615  
  11         19625  
9              
10             our $VERSION = '1.04'; # VERSION
11              
12             sub new {
13 11     11 1 1317 my $self = shift;
14 11 50       111 $self = ( ref $self ) ? bless( { %$self, @_ }, ref $self ) : bless( {@_}, $self );
15              
16 11   50     167 $self->{grammar} //= q#
17             content :
18             ( comment | sentence )(s) /^\Z/
19             { +{ $item[0] => $item[1] } }
20             |
21              
22             comment :
23             /\([^\(\)]*\)/
24             {
25             $item[1] =~ /\(([^\)]+)\)/;
26             +{ $item[0] => ( $1 || '' ) };
27             }
28             |
29              
30             sentence :
31             command /[\.;]\s/
32             { pop @item; +{@item} }
33             |
34              
35             command :
36             (
37             say | set | append | add | subtract | multiply | divide |
38             otherwise_if | if | otherwise | for
39             )
40             { +{@item} }
41             |
42              
43             say : /\bsay\b/ ( list | expression )
44             { +{ $item[0] => $item[2] } }
45             |
46              
47             set : /\bset\b/ object '=' ( list | expression )
48             { +{ $item[0] => [ $item[2], $item[4] ] } }
49             |
50              
51             append : /\bappend\b/ ( list | expression ) '=' object
52             { +{ $item[0] => [ $item[2], $item[4] ] } }
53             |
54              
55             add : /\badd\b/ expression '=' object
56             { +{ $item[0] => [ $item[2], $item[4] ] } }
57             |
58              
59             subtract : /\bsubtract\b/ expression '`' object
60             { +{ $item[0] => [ $item[2], $item[4] ] } }
61             |
62              
63             multiply : /\bmultiply\b/ object '~' expression
64             { +{ $item[0] => [ $item[2], $item[4] ] } }
65             |
66              
67             divide : /\bdivide\b/ object '~' expression
68             { +{ $item[0] => [ $item[2], $item[4] ] } }
69             |
70              
71             otherwise_if : /\botherwise,\s*if\b/ expression '::' ( block | command )
72             { +{ $item[0] => { %{ $item[2] }, %{ $item[4] } } } }
73             |
74              
75             if : /\bif\b/ expression '::' ( block | command )
76             { +{ $item[0] => { %{ $item[2] }, %{ $item[4] } } } }
77             |
78              
79             otherwise : /\botherwise\b,?/ ( block | command )
80             { +{ $item[0] => $item[2] } }
81             |
82              
83             for : /\bfor(?:\s+each)?\b/ object '=^' object block
84             {
85             +{
86             $item[0] => {
87             item => $item[2],
88             list => $item[4],
89             %{ $item[5] },
90             }
91             };
92             }
93             |
94              
95             block : '{{{' ( comment | sentence )(s?) '}}}'
96             { +{ $item[0] => $item[2] } }
97             |
98              
99             list :
100             object ( list_item_seperator object )(s)
101             { +{ shift @item => [ shift @item, @{ $item[0] } ] } }
102             |
103              
104             list_item_seperator : /,\s*(&&\s+)?/
105             |
106              
107             expression:
108             object sub_expression(s?)
109             { +{ $item[0] => [ $item[1], map { @$_ } @{ $item[2] } ] } }
110             |
111              
112             sub_expression:
113             operator object
114             { [ $item[1], $item[2] ] }
115             |
116              
117             operator :
118             (
119             '+' | '-' | '/' | '*' |
120             '>=' | '>' | '<=' | '<' | '!@=' | '@=' | '!=' | '==' | '!^=' | '^=' |
121             '&&' | '||'
122             )
123             {
124             $item[1] =
125             ( $item[1] eq '!@=' ) ? 'not in' :
126             ( $item[1] eq '@=' ) ? 'in' :
127             ( $item[1] eq '!^=' ) ? 'not begins' :
128             ( $item[1] eq '^=' ) ? 'begins' : $item[1];
129             +{@item};
130             }
131             |
132              
133             object : call(s?) ( string | number | word | '=+' | '=-' )(s)
134             {
135             pop @{ $item[2] } while (
136             @{ $item[2] } > 1 and
137             $item[2][-1]{word} =~ /^(?:value|string|text|number|list|array)$/
138             );
139              
140             for ( @{ $item[2] } ) {
141             if ( $_ eq '=+' ) {
142             $_ = { boolean => 'true' };
143             }
144             elsif ( $_ eq '=-' ) {
145             $_ = { boolean => 'false' };
146             }
147             }
148              
149             my $data = {};
150             $data->{calls} = $item[1] if ( @{$item[1]} );
151             $data->{components} = $item[2] if ( @{$item[2]} );
152              
153             +{ $item[0] => $data };
154             }
155             |
156              
157             call :
158             ( '~=' | '$=' | /\[\d+\]/ )
159             {
160             $item[1] =
161             ( $item[1] =~ /\[(\d+)\]/ ) ? { 'item' => $1 } :
162             ( $item[1] eq '~=' ) ? 'length' :
163             ( $item[1] eq '$=' ) ? 'shift' : $item[1];
164             +{@item};
165             }
166             |
167              
168             string :
169             /"[^"]*"/
170             {
171             $item[1] =~ /"([^"]*)"/;
172             +{ $item[0] => $1 };
173             }
174             |
175              
176             number :
177             /\-?(?:\d+,)*(?:\d+\.)*\d+\b/
178             {
179             $item[1] =~ s/[^\d\.\-]//g;
180             +{@item};
181             }
182             |
183              
184             word :
185             /\w+(?:'s)?\b/
186             { +{@item} }
187             |
188             #;
189              
190 11   100     109 $self->renderer( $self->{renderer} // 'JavaScript', $self->{render_args} );
191              
192 11         44 return $self;
193             }
194              
195             sub grammar {
196 2     2 1 6 my ( $self, $grammar ) = @_;
197 2 100       9 $self->{grammar} = $grammar if ($grammar);
198 2         13 return $self->{grammar};
199             }
200              
201             sub append_grammar {
202 1     1 1 5 my ( $self, $grammar ) = @_;
203 1 50       11 $self->{grammar} .= "\n" . $grammar if ($grammar);
204 1         5 return $self;
205             }
206              
207             sub _instantiate_renderer {
208 11     11   44 my ( $self, $renderer, $render_args ) = @_;
209              
210 11         50 my $class = __PACKAGE__ . "::$renderer";
211 11         793 eval "require $class";
212              
213 11   100     229 return $class->new( $render_args || {} );
214             }
215              
216             sub renderer {
217 11     11 1 57 my ( $self, $renderer, $render_args ) = @_;
218 11         155 $self->{render_args} = $render_args;
219              
220 11 50 33     85 if (
      33        
221             $renderer and (
222             not $self->{renderer_obj} or
223             $self->{renderer} and $renderer ne $self->{renderer}
224             )
225             ) {
226 11         47 my $class = __PACKAGE__ . "::$renderer";
227 11         1025 eval "require $class";
228              
229 11         125 $self->{renderer} = $renderer;
230 11         70 $self->{renderer_obj} = $self->_instantiate_renderer( $self->{renderer}, $self->{render_args} );
231             }
232              
233 11         33 return $self->{renderer};
234             }
235              
236             sub _prepare_input {
237 42     42   158 my ( $self, $input ) = @_;
238              
239 42         99 my $bits;
240              
241 42         164 $input =~ s/\(([^\)]+)\)/
242 3         7 push( @{ $bits->{comments} }, $1 );
  3         16  
243 3         8 '(' . scalar @{ $bits->{comments} } - 1 . ')';
  3         18  
244             /ge;
245              
246 42         143 $input =~ s/"([^"]+)"/
247 5         15 push( @{ $bits->{strings} }, $1 );
  5         31  
248 5         12 '"' . scalar @{ $bits->{strings} } - 1 . '"';
  5         32  
249             /ge;
250              
251 42         162 $input = lc $input;
252 42         487 $input =~ s/\b(?:a|an|the|value\s+of|list\s+of|there\s+are|there\s+is)\b//g;
253              
254 42         959 for (
255             # call
256             [ 'length of' => '~=' ],
257             [ 'removed item from' => '$=' ],
258              
259             # operator
260             [ 'plus' => '+' ],
261             [ 'minus' => '-' ],
262             [ 'divided by' => '/' ],
263             [ 'times' => '*' ],
264             [ 'is greater than or equal to' => '>=' ],
265             [ 'is greater than' => '>' ],
266             [ 'is less than or equal to' => '<=' ],
267             [ 'is less than' => '<' ],
268             [ 'is not in' => '!@=' ],
269             [ 'is in' => '@=' ],
270             [ 'is not' => '!=' ],
271             [ 'is' => '==' ],
272             [ 'does not begin with' => '!^=' ],
273             [ 'begins with' => '^=' ],
274              
275             # assignment
276             [ 'to' => '=' ],
277             [ 'from' => '`' ],
278             [ 'by' => '~' ],
279              
280             # logical
281             [ 'and' => '&&' ],
282             [ 'or' => '||' ],
283              
284             # value
285             [ 'true' => '=+' ],
286             [ 'false' => '=-' ],
287              
288             # in
289             [ 'in' => '=^' ],
290             ) {
291 1008         3113 $_->[0] =~ s/\s/\\s+/g;
292 1008         8889 $input =~ s/\b($_->[0])\b/$_->[1]/g;
293             }
294              
295 42         392 $input =~ s/(?:,\s*)?\bthen\b/ ::/g;
296 42         136 $input =~ s/(?:,\s*)?\bapply\b[\w\s]+\bblock\b\s*\./ {{{ /g;
297 42         118 $input =~ s/[^\.\)]+\bend[\w\s]+\bblock\b/ }}} /g;
298              
299 42         117 $input =~ s/\bitem\s*([\d,\.]+)(?:\s*of)?/\[$1\]/g;
300 42         114 $input =~ s/\((\d+)\)/'(' . $bits->{comments}[$1] . ')'/ge;
  3         21  
301 42         113 $input =~ s/"(\d+)"/'"' . $bits->{strings}[$1] . '"'/ge;
  5         33  
302              
303 42         252 return $input . "\n";
304             }
305              
306             sub _parse_prepared_input {
307 42     42   146 my ( $self, $prepared_input ) = @_;
308              
309 42         125 my ( $stderr, $parse_tree );
310             {
311 42         85 local *STDERR;
  42         137  
312 11     11   87 open( STDERR, '>', \$stderr );
  11         24  
  11         83  
  42         1153  
313              
314 42         9127 local $::RD_ERRORS = 1;
315 42         100 local $::RD_WARN = 1;
316              
317 42         513 $parse_tree = Parse::RecDescent->new( $self->{grammar} )->content($prepared_input);
318             }
319 42 100       11195737 if ($stderr) {
320 1         879 $stderr =~ s/\r?\n[ ]{23}/ /g;
321 1         27 $stderr =~ s/(?:\r?\n){2,}/\n/g;
322 1         15 $stderr =~ s/^\s+//mg;
323              
324             my @errors = map {
325 1         10 /^\s*(?\w+)(?:\s+\(line\s+(?\d+)\))?:\s+(?.+)/s;
  15         50  
326 11     11   5846 my $error = {%+};
  11         4040  
  11         4651  
  15         140  
327 15         49 $error->{type} = ucfirst lc $error->{type};
328 15         29 $error;
329             } split( /\n/, $stderr );
330              
331 1         10 return { errors => \@errors };
332             }
333             else {
334 41         42756 return $parse_tree;
335             }
336             }
337              
338             sub parse {
339 42     42 1 98250 my ( $self, $input ) = @_;
340 42         237 $self->{data} = $self->_parse_prepared_input( $self->_prepare_input($input) );
341 42 100       518 croak('Failed to parse input') if ( exists $self->{data}{errors} );
342 41         301 return $self;
343             }
344              
345             sub data {
346 3     3 1 1659 my ($self) = @_;
347 3         36 return $self->{data};
348             }
349              
350             sub yaml {
351 1     1 1 5 my ($self) = @_;
352 1         165 return Dump( $self->{data} );
353             }
354              
355             sub render {
356 40     40 1 611 my ( $self, $renderer, $render_args ) = @_;
357              
358             my $renderer_obj = ( $renderer or $render_args )
359             ? $self->_instantiate_renderer(
360             $renderer // $self->{renderer},
361             $render_args // $self->{render_args},
362             )
363 40 50 33     367 : $self->{renderer_obj};
      0        
      0        
364              
365 40         251 return $renderer_obj->render( $self->{data} );
366             }
367             }
368              
369             package English::Script::JavaScript {
370 11     11   9735 use 5.014;
  11         45  
371 11     11   72 use exact;
  11         26  
  11         86  
372 11     11   17701 use JavaScript::Packer;
  11         111261  
  11         35072  
373              
374             our $VERSION = '1.04'; # VERSION
375              
376             sub new {
377 11     11   45 my ( $self, $args ) = @_;
378 11         78 return bless( { args => $args }, $self );
379             }
380              
381             sub render {
382 40     40   139 my ( $self, $data ) = @_;
383 40         248 $self->{objects} = {};
384 40         197 my $js = $self->content($data);
385              
386             return ( ref $self->{args} eq 'HASH' and %{ $self->{args} } )
387             ? JavaScript::Packer->init->minify( \$js, $self->{args} )
388 40 100 66     245 : $js;
389             }
390              
391             sub content {
392 40     40   197 my ( $self, $content ) = @_;
393              
394             my $text = join( '',
395             map {
396             ( exists $_->{comment} ) ? $self->comment( $_->{comment} ) :
397 46 50       341 ( exists $_->{sentence} ) ? $self->sentence( $_->{sentence} ) : ''
    100          
398 40         95 } @{ $content->{content} }
  40         159  
399             );
400              
401             return join( "\n", (
402             map {
403             'if ( typeof( ' . $_ . ' ) == "undefined" ) ' . ( (/\./) ? '' : 'var ' ) . $_ .
404 61 100       601 ( ( $self->{objects}{$_} ) ? ' = ' . $self->{objects}{$_} : ' = ""' ) . ';'
    100          
405 40         120 } sort keys %{ $self->{objects} }
  40         234  
406             ), $text );
407             }
408              
409             sub comment {
410 3     3   10 my ( $self, $comment ) = @_;
411 3         26 ( my $text = $_->{comment} ) =~ s|^|// |mg;
412 3         22 return $text . "\n";
413             }
414              
415             sub sentence {
416 46     46   200 my ( $self, $sentence ) = @_;
417 46         310 return $self->command( $sentence->{command} );
418             }
419              
420             sub command {
421 60     60   170 my ( $self, $command ) = @_;
422              
423 60         203 my ($command_name) = keys %$command;
424 60         157 my $tree = $command->{$command_name};
425              
426 60 100       665 if ( $command_name eq 'say' ) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
427             return join( ' ',
428             'console.log(', (
429             ( exists $tree->{list} ) ? $self->list( $tree->{list} ) :
430 13 50       81 ( exists $tree->{expression} ) ? $self->expression( $tree->{expression} ) : 'undefined'
    50          
431             ), ')',
432             ) . ";\n";
433             }
434             elsif ( $command_name eq 'set' ) {
435 24         141 my $object = $self->object( $tree->[0]{object} );
436 24 100       112 $self->{objects}{$object} = '[]' if ( exists $tree->[1]{list} );
437              
438             return join( ' ',
439             $object, '=', (
440             ( exists $tree->[1]{expression} )
441             ? $self->expression( $tree->[1]{expression} ) :
442             ( exists $tree->[1]{list} )
443 24 50       171 ? '[ ' . $self->list( $tree->[1]{list} ) . ' ]' : 'undefined'
    100          
444             )
445             ) . ";\n";
446             }
447             elsif ( $command_name eq 'append' ) {
448 2         8 my $object_core = $self->object_core( $tree->[1]{object} );
449 2         9 my $object = $self->object_calls( $tree->[1]{object}, $object_core );
450 2 100 66     13 my $obj_is_a_list = ( $self->{objects}{$object} and $self->{objects}{$object} eq '[]' ) ? 1 : 0;
451             my @predicate =
452             ( exists $tree->[0]{list} ) ? $self->list( $tree->[0]{list} ) :
453 2 50       14 ( exists $tree->[0]{expression} ) ? $self->expression( $tree->[0]{expression} ) : '';
    50          
454              
455 2 100       19 return join( ' ', (
456             ($obj_is_a_list)
457             ? ( $object . '.push(', @predicate, ')' )
458             : ( $object, '+=', @predicate )
459             ) ) . ";\n";
460             }
461             elsif ( $command_name eq 'add' ) {
462             return join( ' ',
463 2         10 $self->object( $tree->[1]{object} ), '+=', $self->expression( $tree->[0]{expression} ),
464             ) . ";\n";
465             }
466             elsif ( $command_name eq 'subtract' ) {
467             return join( ' ',
468 1         6 $self->object( $tree->[1]{object} ), '-=', $self->expression( $tree->[0]{expression} ),
469             ) . ";\n";
470             }
471             elsif ( $command_name eq 'multiply' ) {
472             return join( ' ',
473 1         6 $self->object( $tree->[0]{object} ), '*=', $self->expression( $tree->[1]{expression} ),
474             ) . ";\n";
475             }
476             elsif ( $command_name eq 'divide' ) {
477             return join( ' ',
478 1         6 $self->object( $tree->[0]{object} ), '/=', $self->expression( $tree->[1]{expression} ),
479             ) . ";\n";
480             }
481             elsif ( $command_name eq 'otherwise_if' ) {
482             return 'else if ( ' .
483             join( ' ', $self->expression( $tree->{expression} ) ) . " ) {\n" . join( ' ', (
484             ( exists $tree->{command} ) ? $self->command( $tree->{command} ) :
485 1 0       5 ( exists $tree->{block} ) ? $self->block( $tree->{block} ) : ''
    50          
486             ) ) . "}\n";
487             }
488             elsif ( $command_name eq 'if' ) {
489             return 'if ( ' .
490             join( ' ', $self->expression( $tree->{expression} ) ) . " ) {\n" . join( ' ', (
491             ( exists $tree->{command} ) ? $self->command( $tree->{command} ) :
492 13 50       67 ( exists $tree->{block} ) ? $self->block( $tree->{block} ) : ''
    100          
493             ) ) . "}\n";
494             }
495             elsif ( $command_name eq 'otherwise' ) {
496             return "else {\n" . join( ' ', (
497             ( exists $tree->{command} ) ? $self->command( $tree->{command} ) :
498 1 0       18 ( exists $tree->{block} ) ? $self->block( $tree->{block} ) : ''
    50          
499             ) ) . "}\n";
500             }
501             elsif ( $command_name eq 'for' ) {
502 1         6 my $item = $self->object( $tree->{item}{object} );
503 1         4 my $list = $self->object( $tree->{list}{object} );
504              
505             return 'for ( ' . $item . ' of ' . $list . " ) {\n" . join( ' ', (
506             $self->block( $tree->{block} )
507 1         7 ) ) . "}\n";
508             }
509              
510 0         0 return '';
511             }
512              
513             sub block {
514 2     2   9 my ( $self, $block ) = @_;
515             return join( '',
516             map {
517 2         5 ( exists $_->{comment} ) ? $self->comment( $_->{comment} ) :
518 3 50       22 ( exists $_->{sentence} ) ? $self->sentence( $_->{sentence} ) : ''
    100          
519             } @$block
520             );
521             }
522              
523             sub list {
524 5     5   19 my ( $self, $list ) = @_;
525 5         16 return join( ', ', map { $self->object( $_->{object} ) } @$list );
  15         47  
526             }
527              
528             sub expression {
529 53     53   126 my ( $self, $expression ) = @_;
530              
531             my @parts = map {
532 53 100       133 ( exists ( $_->{object} ) ) ? +{ object => $self->object( $_->{object} ) } : +{%$_};
  105         410  
533             } @$expression;
534              
535 53         226 for ( my $i = 0; $i < @parts; $i++ ) {
536 105 100       324 if ( exists $parts[$i]{operator} ) {
537 26 100       176 if ( $parts[$i]{operator} eq 'in' ) {
    100          
    100          
    100          
538             $parts[ $i - 1 ]{object} =
539 1         8 $parts[ $i + 1 ]{object} . '.indexOf( ' . $parts[ $i - 1 ]{object} . ' )';
540 1         3 $parts[$i]{operator} = '>';
541 1         4 $parts[ $i + 1 ]{object} = -1;
542             }
543             elsif ( $parts[$i]{operator} eq 'not in' ) {
544             $parts[ $i - 1 ]{object} =
545 1         7 $parts[ $i + 1 ]{object} . '.indexOf( ' . $parts[ $i - 1 ]{object} . ' )';
546 1         3 $parts[$i]{operator} = '==';
547 1         7 $parts[ $i + 1 ]{object} = -1;
548             }
549             elsif ( $parts[$i]{operator} eq 'begins' ) {
550             $parts[ $i - 1 ]{object} =
551 1         8 $parts[ $i - 1 ]{object} . '.indexOf( ' . $parts[ $i + 1 ]{object} . ' )';
552 1         12 $parts[$i]{operator} = '==';
553 1         5 $parts[ $i + 1 ]{object} = 0;
554             }
555             elsif ( $parts[$i]{operator} eq 'not begins' ) {
556             $parts[ $i - 1 ]{object} =
557 1         9 $parts[ $i - 1 ]{object} . '.indexOf( ' . $parts[ $i + 1 ]{object} . ' )';
558 1         4 $parts[$i]{operator} = '!=';
559 1         4 $parts[ $i + 1 ]{object} = 0;
560             }
561             }
562             }
563              
564 53 100       145 return map { ( exists $_->{object} ) ? $_->{object} : $_->{operator} } @parts;
  105         660  
565             }
566              
567             sub object {
568 125     125   249 my ( $self, $object ) = @_;
569 125         321 return $self->object_calls( $object, $self->object_core($object) );
570             }
571              
572             sub object_core {
573 127     127   316 my ( $self, $object ) = @_;
574              
575 127         230 my $text = '';
576 127 50       315 if ( exists $object->{components} ) {
577 127 100       481 if ( $object->{components}[0]{boolean} ) {
    100          
578 6         13 $text .= join( ' ', map { values %$_ } @{ $object->{components} } );
  6         22  
  6         16  
579             }
580             elsif ( not $object->{components}[0]{string} ) {
581 116         265 my $contains_non_number = grep { not exists $_->{number} } @{ $object->{components} };
  128         380  
  116         237  
582             $object->{components}[0] = { word => '_' . $object->{components}[0]{number} }
583 116 100 100     472 if ( $contains_non_number and exists $object->{components}[0]{number} );
584              
585 116         188 my @parts = map { values %$_ } @{ $object->{components} };
  128         447  
  116         222  
586 116         344 $text .= join( '.', @parts );
587              
588 116 100       278 if ($contains_non_number) {
589 57         206 for ( my $i = 0; $i < @parts; $i++ ) {
590             $self->{objects}{
591 69 100 100     653 join( '.', @parts[ 0 .. $i ] )
592             } //= ( $i == @parts - 1 ) ? '' : '{}';
593             }
594             }
595             }
596             else {
597 5         20 $text .= '"' . join( '', map { values %$_ } @{ $object->{components} } ) . '"';
  5         28  
  5         16  
598             }
599             }
600              
601 127         389 return $text;
602             }
603              
604             sub object_calls {
605 127     127   287 my ( $self, $object, $text ) = @_;
606              
607 127         208 my $object_text = $text;
608 127 100       306 if ( exists $object->{calls} ) {
609 5         11 for my $call ( reverse map { values %$_ } @{ $object->{calls} } ) {
  6         23  
  5         19  
610 6 100 33     37 if ( $call eq 'length' ) {
    100          
    50          
611 3         9 $text .= '.length';
612             }
613             elsif ( $call eq 'shift' ) {
614 1         3 $text .= '.shift';
615 1         5 $self->{objects}{$object_text} = '[]';
616             }
617             elsif ( ref $call eq 'HASH' and exists $call->{item} ) {
618 2         12 $text .= '[' . ( $call->{item} - 1 ) . ']';
619             }
620             }
621             }
622              
623 127         503 return $text;
624             }
625             }
626              
627             1;
628              
629             __END__