File Coverage

blib/lib/JavaScript/Ectype.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package JavaScript::Ectype;
2 1     1   20349 use strict;
  1         2  
  1         35  
3 1     1   4 use warnings;
  1         2  
  1         46  
4             our $VERSION = q{0.01};
5              
6 1     1   6332 use JSON;
  1         15756  
  1         7  
7 1     1   1262 use File::Slurp;
  1         15510  
  1         73  
8 1     1   9 use File::Spec;
  1         2  
  1         22  
9 1     1   422 use UNIVERSAL::require;
  0            
  0            
10             use Carp qw/croak carp/;
11             use Scalar::Util qw/blessed/;
12              
13             use base qw/
14             Class::Accessor::Fast
15             /;
16              
17             sub _minify_javascript;
18             BEGIN{
19             if( JavaScript::Minifier::XS->require ){
20             *_minify_javascript = \&JavaScript::Minifier::XS::minify;
21             }elsif( JavaScript::Minifier->require ){
22             *_minify_javascript = sub{
23             JavaScript::Minifier::minify( input => $_[0] );
24             };
25             }else{
26             *_minify_javascript = sub{$_[0]};
27             }
28             }
29              
30             use constant FORMAT_SCOPE_WITH_NS => q|
31             /* %s is loaded */
32             "%s".namespace().using(function(_namespace_){%s});/*END_OF_SCOPE(%s)*/
33             |;
34              
35             use constant FORMAT_SCOPE_WITHOUT_NS => q|
36             /* %s is loaded */
37             (function(){
38             %s
39             })();/*END_OF_FILE_SCOPE(%s)*/
40             |;
41              
42             use constant FORMAT_IMPORT_HEADER
43             => q|"%s".namespace().within(%s,function(%s){|;
44            
45             use constant FORMAT_IMPORT_FOOTER
46             => q|});/*END_OF_IMPORT(%s)*/|;
47            
48             use constant FORMAT_IMPORT_AUTO_HEADER
49             => q|with("%s".namespace().stash()){|;
50              
51             use constant FORMAT_IMPORT_AUTO_FOOTER
52             => q|}/*END_OF_IMPORT(%s)*/|;
53              
54             use constant FORMAT_DEPENDS
55             => q|"%s".namespace().depends(%s);|;
56              
57             use constant FORMAT_DEPENDS_WITHOUT_CHECKER
58             => q|"%s".namespace().depends();|;
59              
60             __PACKAGE__->mk_accessors(qw/
61             path
62             target
63             parent
64             package
65             minify
66             _component
67             /);
68              
69             sub new {
70             my ($class,%option) = @_;
71             return bless { ( _component => {},minify => 1 ), %option }, $class;
72             }
73              
74             sub load {
75             my ( $proto, %args ) = @_;
76             my $self = blessed $proto ? $proto : $proto->new( %args );
77             $self->{_converted_data} = $self->convert;
78             return $self;
79             }
80              
81             sub is_converted {
82             my $self = shift;
83             return ( $self->{_converted_data} ) ? 1:0;
84             }
85             sub converted_data{
86             my $self = shift;
87             return $self->{_converted_data} ;
88             }
89              
90             sub file_path {
91             my $self = shift;
92             unless ( $self->{file_path} ) {
93             croak('target undefined') unless defined $self->target;
94             $self->{file_path} = _full_file_path( $self->path, $self->target );
95             }
96             return $self->{file_path};
97             }
98              
99             sub convert {
100             my $self = ( blessed $_[0] ) ? shift : shift->new(@_);
101             my $file_path = $self->file_path;
102              
103             return $self->converted_data if ( $self->is_converted );
104             return '' if( $self->_is_loaded_depends_file( $file_path ) );
105            
106             $self->_push_depends_file( $file_path );
107              
108             return '' unless $file_path;
109              
110             my $data = eval{ File::Slurp::read_file( $file_path ) };
111             if( $@ ){
112             croak("$file_path does not exist.");
113             }
114             my $result = $self->_execute(
115             data => $data,
116             filters => [qw/
117             _dispatch_command
118             _set_scope
119             _set_import
120             _set_depends
121             _set_require
122             _filter_minify
123             /]
124             );
125             }
126              
127             sub _command_require {
128             my ( $self,$argument ) = @_;
129             $self->_add_component(require => $self->_absolutize_namespace($argument) );
130             return '';
131             }
132              
133             sub _command_depends {
134             my ( $self, $argument ) = @_;
135             if ( $argument =~ m/->/ ) {
136             my ( $namespace, $text ) = split /\-\>/, $argument;
137             $namespace =~ s/\s//g;
138             $text =~ s/\s//g;
139             $self->_add_component(
140             depends => {
141             namespace => $self->_absolutize_namespace($namespace),
142             data => [ split /,/, $text ]
143             }
144             );
145             }
146             else {
147             $self->_add_component(
148             depends => $self->_absolutize_namespace($argument) );
149             }
150             return '';
151              
152             }
153              
154             sub _command_import {
155             my ( $self, $argument ) = @_;
156             if ( $argument =~ m/->/ ) {
157             my ( $namespace, $text ) = split /\-\>/, $argument;
158             $namespace =~ s/\s//g;
159             $text =~ s/\s//g;
160             $self->_add_component(
161             import => {
162             namespace => $self->_absolutize_namespace($namespace),
163             data => [
164             map {
165             my ( $from, $to ) = split /:/, $_;
166             ( "$from" => ( $to ? $to : $from ) )
167             } split /,/,
168             $text
169             ]
170             }
171             );
172             }
173             else {
174             $self->_add_component(
175             import => $self->_absolutize_namespace($argument) );
176             }
177             return '';
178             }
179              
180             sub _command_package {
181             my ( $self,$argument ) = @_;
182             carp "package redefined $argument" if defined $self->package;
183             $self->package($argument);
184             return '';
185              
186             }
187              
188             sub _command_include {
189             my ( $self, $argument ) = @_;
190             my $original = $argument;
191             $argument =~ s/\.\//$self->path/e;
192             $self->_push_depends_file( $argument );
193             return eval {
194             File::Slurp::read_file($argument) . qq|/* $original is included */|;
195             } || croak("$argument cannot include");
196             }
197              
198             sub _absolutize_namespace {
199             my ( $self, $fqn ) = @_;
200             if ( $fqn =~ m/^\_/ ) {
201             if ( defined $self->package ) {
202             $fqn =~ s/^_/$self->package/e;
203             }
204             else {
205             carp "package is not defined";
206             }
207             }
208             return $fqn;
209             }
210              
211             sub _fqn_to_path{
212             my $fqn = shift;
213             $fqn =~ s/([A-Z]+)([A-Z][a-z])/$1_$2/g;
214             $fqn =~ s/([a-z\d])([A-Z])/$1_$2/g;
215             File::Spec->catfile( split /\./, lc($fqn));
216             }
217              
218             sub _convert_json{
219             my ($perl_value ) = @_;
220             return JSON->new->utf8->encode( $perl_value );
221             }
222              
223             sub _create_child {
224             my ( $self, $target ) = @_;
225             my $class = ref $self;
226             return $class->convert(
227             parent => $self->parent || $self,
228             target => $_,
229             minify => 0,
230             path => $self->path
231             );
232             }
233              
234             sub _full_file_path {
235             my ( $path, $target ) = @_;
236             if ( $target =~ m/\.js$/ ) {
237             my $file_path = ( $path || '' ) . $target;
238             if ( -e $file_path ) {
239             return $file_path;
240             }
241             }
242             else {
243             my $file_path = ( $path || '' ) . _fqn_to_path($target);
244             if ( -e $file_path . '.ectype.js' ) {
245             return $file_path . '.ectype.js';
246             }
247             return $file_path . '.js';
248             }
249             }
250              
251             sub _push_depends_file{
252             my ( $self,$file_path ) = @_;
253             my $parent = $self->parent || $self;
254             $parent->_loaded_file_map->{$file_path} = 1;
255             }
256              
257             sub _is_loaded_depends_file {
258             my ( $self,$file_path ) = @_;
259             my $parent = $self->parent || $self;
260             return ( $parent->_loaded_file_map->{$file_path} ) ? 1 : 0;
261             }
262              
263             sub _loaded_file_map{
264             my ($self) = @_;
265             unless( $self->{_loaded_file_map} ) {
266             $self->{_loaded_file_map} = {};
267             }
268             return $self->{_loaded_file_map};
269             }
270              
271             sub related_files{
272             my ( $self, %args ) = @_;
273             keys %{ $self->_loaded_file_map };
274             }
275              
276             sub _add_component{
277             my ( $self,$type,@args ) = @_;
278              
279             $self->_component( {} ) unless ( defined $self->_component );
280             $self->_component->{$type} ||= [];
281              
282             push @{ $self->_component->{$type} }, @args;
283             }
284              
285             sub _execute {
286             my ( $self, %option ) = @_;
287             my $data_ref = \$option{data};
288              
289             foreach my $method( @{ $option{filters} } ) {
290             $self->$method($data_ref);
291             }
292             return $$data_ref;
293             }
294              
295             sub _set_scope{
296             my ($self,$text_ref) = @_;
297              
298             return if( $self->package and $self->package eq 'NONE' );
299              
300             my $file = $self->file_path;
301             my $path = $self->path;
302              
303             $file =~ s/$path//;
304             if( $self->package){
305             $$text_ref = sprintf( FORMAT_SCOPE_WITH_NS ,
306             $file,
307             $self->package,
308             $$text_ref,
309             $self->package
310             );
311             }
312             else{
313             $$text_ref = sprintf( FORMAT_SCOPE_WITHOUT_NS,
314             $file,
315             $$text_ref,
316             $file
317             );
318             }
319             }
320              
321             sub _set_import{
322             my($self,$text_ref) = @_;
323             my @header = ();
324             my @footer = ();
325             for ( @{ $self->_component->{import} || [] } ) {
326             if ( ref $_ eq 'HASH' ) {
327             my $namespace = $_->{namespace};
328             my %data = @{ $_->{data} };
329             my @real = keys %data;
330             my @alias = values %data;
331             push @header,
332             sprintf(
333             FORMAT_IMPORT_HEADER,
334             $namespace, _convert_json( \@real ),
335             join ',', @alias
336             );
337             unshift @footer, sprintf(FORMAT_IMPORT_FOOTER,$namespace);
338             }
339             else {
340             push @header, sprintf( FORMAT_IMPORT_AUTO_HEADER, $_ );
341             unshift @footer, sprintf(FORMAT_IMPORT_AUTO_FOOTER,$_);
342             }
343             }
344             $$text_ref = join '',(@header,$$text_ref,@footer);
345             }
346              
347             sub _set_require{
348             my ($self,$text_ref) = @_;
349             my $data = join '',map{
350             $self->_create_child($_);
351             }@{$self->_component->{require}};
352             $$text_ref = $data . $$text_ref;
353             }
354              
355              
356             sub _set_depends {
357             my ($self,$text_ref) = @_;
358             my $depends = join '', map {
359             if( ref $_ eq 'HASH' ){
360             my $namespace = $_->{namespace};
361             my $checker = _convert_json($_->{data});
362             sprintf( FORMAT_DEPENDS , $namespace, $checker );
363             }else{
364             sprintf( FORMAT_DEPENDS_WITHOUT_CHECKER ,$_ );
365             }
366             } @{ $self->_component->{depends} || [] };
367              
368             $$text_ref = $depends . $$text_ref;
369             }
370              
371             sub _filter_minify {
372             my ( $self, $text_ref ) = @_;
373              
374             $$text_ref = _minify_javascript($$text_ref)
375             if ( $self->minify );
376             }
377              
378             sub _dispatch_command{
379             my $self = shift;
380             my $text_ref = shift;
381              
382             $$text_ref =~ s|
383             ^ # top of the line
384             (?: # top of syntax
385             //\= # //=
386             ([\w_]+) # command = $1
387             (?:\s+ # splitter
388             ([\w\->\/\s\,\.\:]+) # argument = $2
389             )?
390             (?:[;]) # finished
391             ) # end of syntax
392             $ # end of the line
393             |$self->__dispatch_command($1,$2)|xgme;
394             }
395              
396             sub __dispatch_command {
397             my ( $self,$command,$argument ) = @_;
398             my $method = "_command_$command";
399             $self->$method( $argument );
400             }
401              
402              
403             1;
404             __END__