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   2166057 use 5.014;
  11         81  
5 11     11   5271 use exact;
  11         317238  
  11         46  
6              
7 11     11   35607 use Parse::RecDescent;
  11         350409  
  11         81  
8 11     11   5492 use YAML::XS 'Dump';
  11         27089  
  11         16100  
9              
10             our $VERSION = '1.05'; # VERSION
11              
12             sub new {
13 11     11 1 1063 my $self = shift;
14 11 50       59 $self = ( ref $self ) ? bless( { %$self, @_ }, ref $self ) : bless( {@_}, $self );
15              
16 11   50     131 $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     85 $self->renderer( $self->{renderer} // 'JavaScript', $self->{render_args} );
191              
192 11         39 return $self;
193             }
194              
195             sub grammar {
196 2     2 1 7 my ( $self, $grammar ) = @_;
197 2 100       6 $self->{grammar} = $grammar if ($grammar);
198 2         9 return $self->{grammar};
199             }
200              
201             sub append_grammar {
202 1     1 1 3 my ( $self, $grammar ) = @_;
203 1 50       5 $self->{grammar} .= "\n" . $grammar if ($grammar);
204 1         4 return $self;
205             }
206              
207             sub _instantiate_renderer {
208 11     11   40 my ( $self, $renderer, $render_args ) = @_;
209              
210 11         41 my $class = __PACKAGE__ . "::$renderer";
211 11         686 eval "require $class";
212              
213 11   100     191 return $class->new( $render_args || {} );
214             }
215              
216             sub renderer {
217 11     11 1 50 my ( $self, $renderer, $render_args ) = @_;
218 11         145 $self->{render_args} = $render_args;
219              
220 11 50 33     73 if (
      33        
221             $renderer and (
222             not $self->{renderer_obj} or
223             $self->{renderer} and $renderer ne $self->{renderer}
224             )
225             ) {
226 11         38 my $class = __PACKAGE__ . "::$renderer";
227 11         833 eval "require $class";
228              
229 11         101 $self->{renderer} = $renderer;
230 11         64 $self->{renderer_obj} = $self->_instantiate_renderer( $self->{renderer}, $self->{render_args} );
231             }
232              
233 11         26 return $self->{renderer};
234             }
235              
236             sub _prepare_input {
237 42     42   117 my ( $self, $input ) = @_;
238              
239 42         82 my $bits;
240              
241 42         141 $input =~ s/\(([^\)]+)\)/
242 3         6 push( @{ $bits->{comments} }, $1 );
  3         13  
243 3         7 '(' . scalar @{ $bits->{comments} } - 1 . ')';
  3         13  
244             /ge;
245              
246 42         115 $input =~ s/"([^"]+)"/
247 5         11 push( @{ $bits->{strings} }, $1 );
  5         24  
248 5         9 '"' . scalar @{ $bits->{strings} } - 1 . '"';
  5         30  
249             /ge;
250              
251 42         133 $input = lc $input;
252 42         333 $input =~ s/\b(?:a|an|the|value\s+of|list\s+of|there\s+are|there\s+is)\b//g;
253              
254 42         808 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         2539 $_->[0] =~ s/\s/\\s+/g;
292 1008         7327 $input =~ s/\b($_->[0])\b/$_->[1]/g;
293             }
294              
295 42         310 $input =~ s/(?:,\s*)?\bthen\b/ ::/g;
296 42         112 $input =~ s/(?:,\s*)?\bapply\b[\w\s]+\bblock\b\s*\./ {{{ /g;
297 42         98 $input =~ s/[^\.\)]+\bend[\w\s]+\bblock\b/ }}} /g;
298              
299 42         98 $input =~ s/\bitem\s*([\d,\.]+)(?:\s*of)?/\[$1\]/g;
300 42         92 $input =~ s/\((\d+)\)/'(' . $bits->{comments}[$1] . ')'/ge;
  3         13  
301 42         100 $input =~ s/"(\d+)"/'"' . $bits->{strings}[$1] . '"'/ge;
  5         26  
302              
303 42         210 return $input . "\n";
304             }
305              
306             sub _parse_prepared_input {
307 42     42   120 my ( $self, $prepared_input ) = @_;
308              
309 42         81 my ( $stderr, $parse_tree );
310             {
311 42         61 local *STDERR;
  42         124  
312 11     11   81 open( STDERR, '>', \$stderr );
  11         19  
  11         70  
  42         984  
313              
314 42         7832 local $::RD_ERRORS = 1;
315 42         82 local $::RD_WARN = 1;
316              
317 42         362 $parse_tree = Parse::RecDescent->new( $self->{grammar} )->content($prepared_input);
318             }
319 42 100       9331277 if ($stderr) {
320 1         727 $stderr =~ s/\r?\n[ ]{23}/ /g;
321 1         23 $stderr =~ s/(?:\r?\n){2,}/\n/g;
322 1         13 $stderr =~ s/^\s+//mg;
323              
324             my @errors = map {
325 1         8 /^\s*(?\w+)(?:\s+\(line\s+(?\d+)\))?:\s+(?.+)/s;
  15         38  
326 11     11   4822 my $error = {%+};
  11         3386  
  11         3781  
  15         115  
327 15         38 $error->{type} = ucfirst lc $error->{type};
328 15         23 $error;
329             } split( /\n/, $stderr );
330              
331 1         8 return { errors => \@errors };
332             }
333             else {
334 41         36935 return $parse_tree;
335             }
336             }
337              
338             sub parse {
339 42     42 1 86897 my ( $self, $input ) = @_;
340 42         184 $self->{data} = $self->_parse_prepared_input( $self->_prepare_input($input) );
341 42 100       439 croak('Failed to parse input') if ( exists $self->{data}{errors} );
342 41         253 return $self;
343             }
344              
345             sub data {
346 3     3 1 1121 my ($self) = @_;
347 3         30 return $self->{data};
348             }
349              
350             sub yaml {
351 1     1 1 3 my ($self) = @_;
352 1         117 return Dump( $self->{data} );
353             }
354              
355             sub render {
356 40     40 1 521 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     296 : $self->{renderer_obj};
      0        
      0        
364              
365 40         220 return $renderer_obj->render( $self->{data} );
366             }
367             }
368              
369             package English::Script::JavaScript {
370 11     11   8106 use 5.014;
  11         35  
371 11     11   58 use exact;
  11         21  
  11         70  
372 11     11   14606 use JavaScript::Packer;
  11         91463  
  11         28692  
373              
374             our $VERSION = '1.05'; # VERSION
375              
376             sub new {
377 11     11   38 my ( $self, $args ) = @_;
378 11         59 return bless( { args => $args }, $self );
379             }
380              
381             sub render {
382 40     40   117 my ( $self, $data ) = @_;
383 40         245 $self->{objects} = {};
384 40         162 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     225 : $js;
389             }
390              
391             sub content {
392 40     40   169 my ( $self, $content ) = @_;
393              
394             my $text = join( '',
395             map {
396             ( exists $_->{comment} ) ? $self->comment( $_->{comment} ) :
397 46 50       314 ( exists $_->{sentence} ) ? $self->sentence( $_->{sentence} ) : ''
    100          
398 40         80 } @{ $content->{content} }
  40         128  
399             );
400              
401             return join( "\n", (
402             map {
403             'if ( typeof( ' . $_ . ' ) == "undefined" ) ' . ( (/\./) ? '' : 'var ' ) . $_ .
404 61 100       499 ( ( $self->{objects}{$_} ) ? ' = ' . $self->{objects}{$_} : ' = ""' ) . ';'
    100          
405 40         93 } sort keys %{ $self->{objects} }
  40         176  
406             ), $text );
407             }
408              
409             sub comment {
410 3     3   7 my ( $self, $comment ) = @_;
411 3         22 ( my $text = $_->{comment} ) =~ s|^|// |mg;
412 3         19 return $text . "\n";
413             }
414              
415             sub sentence {
416 46     46   169 my ( $self, $sentence ) = @_;
417 46         246 return $self->command( $sentence->{command} );
418             }
419              
420             sub command {
421 60     60   126 my ( $self, $command ) = @_;
422              
423 60         177 my ($command_name) = keys %$command;
424 60         154 my $tree = $command->{$command_name};
425              
426 60 100       563 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       77 ( exists $tree->{expression} ) ? $self->expression( $tree->{expression} ) : 'undefined'
    50          
431             ), ')',
432             ) . ";\n";
433             }
434             elsif ( $command_name eq 'set' ) {
435 24         122 my $object = $self->object( $tree->[0]{object} );
436 24 100       103 $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       142 ? '[ ' . $self->list( $tree->[1]{list} ) . ' ]' : 'undefined'
    100          
444             )
445             ) . ";\n";
446             }
447             elsif ( $command_name eq 'append' ) {
448 2         7 my $object_core = $self->object_core( $tree->[1]{object} );
449 2         8 my $object = $self->object_calls( $tree->[1]{object}, $object_core );
450 2 100 66     11 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       11 ( exists $tree->[0]{expression} ) ? $self->expression( $tree->[0]{expression} ) : '';
    50          
454              
455 2 100       15 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         8 $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         5 $self->object( $tree->[0]{object} ), '*=', $self->expression( $tree->[1]{expression} ),
474             ) . ";\n";
475             }
476             elsif ( $command_name eq 'divide' ) {
477             return join( ' ',
478 1         5 $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       3 ( 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       60 ( 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       14 ( 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   5 my ( $self, $block ) = @_;
515             return join( '',
516             map {
517 2         6 ( exists $_->{comment} ) ? $self->comment( $_->{comment} ) :
518 3 50       20 ( exists $_->{sentence} ) ? $self->sentence( $_->{sentence} ) : ''
    100          
519             } @$block
520             );
521             }
522              
523             sub list {
524 5     5   17 my ( $self, $list ) = @_;
525 5         14 return join( ', ', map { $self->object( $_->{object} ) } @$list );
  15         40  
526             }
527              
528             sub expression {
529 53     53   120 my ( $self, $expression ) = @_;
530              
531             my @parts = map {
532 53 100       119 ( exists ( $_->{object} ) ) ? +{ object => $self->object( $_->{object} ) } : +{%$_};
  105         342  
533             } @$expression;
534              
535 53         182 for ( my $i = 0; $i < @parts; $i++ ) {
536 105 100       266 if ( exists $parts[$i]{operator} ) {
537 26 100       148 if ( $parts[$i]{operator} eq 'in' ) {
    100          
    100          
    100          
538             $parts[ $i - 1 ]{object} =
539 1         6 $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         2 $parts[$i]{operator} = '==';
547 1         3 $parts[ $i + 1 ]{object} = -1;
548             }
549             elsif ( $parts[$i]{operator} eq 'begins' ) {
550             $parts[ $i - 1 ]{object} =
551 1         15 $parts[ $i - 1 ]{object} . '.indexOf( ' . $parts[ $i + 1 ]{object} . ' )';
552 1         2 $parts[$i]{operator} = '==';
553 1         4 $parts[ $i + 1 ]{object} = 0;
554             }
555             elsif ( $parts[$i]{operator} eq 'not begins' ) {
556             $parts[ $i - 1 ]{object} =
557 1         6 $parts[ $i - 1 ]{object} . '.indexOf( ' . $parts[ $i + 1 ]{object} . ' )';
558 1         3 $parts[$i]{operator} = '!=';
559 1         4 $parts[ $i + 1 ]{object} = 0;
560             }
561             }
562             }
563              
564 53 100       109 return map { ( exists $_->{object} ) ? $_->{object} : $_->{operator} } @parts;
  105         538  
565             }
566              
567             sub object {
568 125     125   210 my ( $self, $object ) = @_;
569 125         278 return $self->object_calls( $object, $self->object_core($object) );
570             }
571              
572             sub object_core {
573 127     127   260 my ( $self, $object ) = @_;
574              
575 127         188 my $text = '';
576 127 50       274 if ( exists $object->{components} ) {
577 127 100       399 if ( $object->{components}[0]{boolean} ) {
    100          
578 6         10 $text .= join( ' ', map { values %$_ } @{ $object->{components} } );
  6         47  
  6         11  
579             }
580             elsif ( not $object->{components}[0]{string} ) {
581 116         207 my $contains_non_number = grep { not exists $_->{number} } @{ $object->{components} };
  128         339  
  116         232  
582             $object->{components}[0] = { word => '_' . $object->{components}[0]{number} }
583 116 100 100     381 if ( $contains_non_number and exists $object->{components}[0]{number} );
584              
585 116         147 my @parts = map { values %$_ } @{ $object->{components} };
  128         386  
  116         193  
586 116         256 $text .= join( '.', @parts );
587              
588 116 100       227 if ($contains_non_number) {
589 57         169 for ( my $i = 0; $i < @parts; $i++ ) {
590             $self->{objects}{
591 69 100 100     528 join( '.', @parts[ 0 .. $i ] )
592             } //= ( $i == @parts - 1 ) ? '' : '{}';
593             }
594             }
595             }
596             else {
597 5         12 $text .= '"' . join( '', map { values %$_ } @{ $object->{components} } ) . '"';
  5         26  
  5         13  
598             }
599             }
600              
601 127         323 return $text;
602             }
603              
604             sub object_calls {
605 127     127   226 my ( $self, $object, $text ) = @_;
606              
607 127         174 my $object_text = $text;
608 127 100       246 if ( exists $object->{calls} ) {
609 5         9 for my $call ( reverse map { values %$_ } @{ $object->{calls} } ) {
  6         24  
  5         13  
610 6 100 33     34 if ( $call eq 'length' ) {
    100          
    50          
611 3         8 $text .= '.length';
612             }
613             elsif ( $call eq 'shift' ) {
614 1         2 $text .= '.shift';
615 1         3 $self->{objects}{$object_text} = '[]';
616             }
617             elsif ( ref $call eq 'HASH' and exists $call->{item} ) {
618 2         18 $text .= '[' . ( $call->{item} - 1 ) . ']';
619             }
620             }
621             }
622              
623 127         413 return $text;
624             }
625             }
626              
627             1;
628              
629             __END__