1package File::Assets;
2
3use warnings;
4use strict;
5
6=head1 NAME
7
8File::Assets - Manage .css and .js assets for a web page or application
9
10=head1 VERSION
11
12Version 0.064
13
14=cut
15
16our $VERSION = '0.064';
17
18=head1 SYNOPSIS
19
20    use File::Assets
21
22    my $assets = File::Assets->new( base => [ $uri_root, $dir_root ] )
23
24    # Put minified files in $dir_root/built/... (the trailing slash is important)
25    $assets->set_output_path("built/")
26
27    # File::Assets will automatically detect the type based on the extension
28    $assets->include("/static/style.css")
29
30    # You can also include external assets:
31    $assets->include("http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js");
32
33    # This asset won't get included twice, as File::Assets will ignore repeats of a path
34    $assets->include("/static/style.css")
35
36    # And finally ...
37    $assets->export
38
39    # Or you can iterate (in order)
40    for my $asset ($assets->exports) {
41
42        print $asset->uri, "\n";
43
44    }
45
46In your .tt (Template Toolkit) files:
47
48    [% WRAPPER page.tt %]
49
50    [% assets.include("/static/special-style.css", 100) %] # The "100" is the rank, which makes sure it is exported after other assets
51
52    [% asset = BLOCK %]
53    <style media="print">
54    body { font: serif; }
55    </style>
56    [% END %]
57    [% assets.include(asset) %] # This will include the css into an inline asset with the media type of "print"
58
59    # ... finally, in your "main" template:
60
61    [% CLEAR -%]
62    <html>
63
64        <head>
65            [% assets.export("css") %]
66        </head>
67
68        <body>
69
70            [% content %]
71
72            <!-- Generally, you want to include your JavaScript assets at the bottom of your html -->
73
74            [% assets.export("js") %]
75
76        </body>
77
78    </html>
79
80Use the minify option to perform minification before export
81
82    my $assets = File::Assets->new( minify => 1, ... )
83
84=head1 DESCRIPTION
85
86File::Assets is a tool for managing JavaScript and CSS assets in a (web) application. It allows you to "publish" assests in one place after having specified them in different parts of the application (e.g. throughout request and template processing phases).
87
88This package has the added bonus of assisting with minification and filtering of assets. Support is built-in for YUI Compressor (L<http://developer.yahoo.com/yui/compressor/>), L<JavaScript::Minifier>, L<CSS::Minifier>, L<JavaScript::Minifier::XS>, and L<CSS::Minifier::XS>.
89
90File::Assets was built with L<Catalyst> in mind, although this package is framework agnostic. Look at L<Catalyst::Plugin::Assets> for an easy way to integrate File::Assets with Catalyst.
91
92=head1 USAGE
93
94=head2 Cascading style sheets and their media types
95
96A cascading style sheet can be one of many different media types. For more information, look here: L<http://www.w3.org/TR/REC-CSS2/media.html>
97
98This can cause a problem when minifying, since, for example, you can't bundle a media type of screen with a media type of print. File::Assets handles this situation by treating .css files of different media types separately.
99
100To control the media type of a text/css asset, you can do the following:
101
102    $assets->include("/path/to/printstyle.css", ..., { media => "print" }); # The asset will be exported with the print-media indicator
103
104    $assets->include_content($content, "text/css", ..., { media => "screen" }); # Ditto, but for the screen type
105
106=head2 Including assets in the middle of processing a Template Toolkit template
107
108Sometimes, in the middle of a TT template, you want to include a new asset. Usually you would do something like this:
109
110    [% assets.include("/include/style.css") %]
111
112But then this will show up in your output, because ->include returns an object:
113
114    File::Assets::Asset=HASH(0x99047e4)
115
116The way around this is to use the TT "CALL" directive, as in the following:
117
118    [% CALL assets.include("/include/style.css") %]
119
120=head2 Avoid minifying assets on every request (if you minify)
121
122By default, File::Assets will avoid re-minifying assets if nothing in the files have changed. However, in a web application, this can be a problem if you serve up two web pages that have different assets. That's because File::Assets will detect different assets being served in page A versus assets being served in page B (think AJAX interface vs. plain HTML with some CSS). The way around this problem is to name your assets object with a unique name per assets bundle. By default, the name is "assets", but can be changed with $assets->name(<a new name>):
123
124    my $assets = File::Assets->new(...);
125    $assets->name("standard");
126
127You can change the name of the assets at anytime before exporting.
128
129=head2 YUI Compressor 2.2.5 is required
130
131If you want to use the YUI Compressor, you should have version 2.2.5 or above.
132
133YUI Compressor 2.1.1 (and below) will *NOT WORK*
134
135To use the compressor for minification specify the path to the .jar like so:
136
137    my $assets = File::Assets->new( minify => "/path/to/yuicompressor.jar", ... )
138
139=head2 Specifying an C<output_path> pattern
140
141When aggregating or minifying assets, you need to put the result in a new file.
142
143You can use the following directives when crafting a path/filename pattern:
144
145    %n      The name of the asset, "assets" by default
146    %e      The extension of the asset (e.g. css, js)
147    %f      The fingerprint of the asset collection (a hexadecimal digest of the concatenated digest of each asset in the collection)
148    %k      The kind of the asset (e.g. css-screen, css, css-print, js)
149    %h      The kind head-part of the asset (e.g. css, js)
150    %l      The kind tail-part of the asset (e.g. screen, print) (essentially the media type of a .css asset)
151
152In addition, in each of the above, a ".", "/" or "-" can be placed in between the "%" and directive character.
153This will result in a ".", "/", or "-" being prepended to the directive value.
154
155The default pattern is:
156
157    %n%-l%-f.%e
158
159A pattern of C<%n%-l.%e> can result in the following:
160
161    assets.css          # name of "assets", no media type, an asset type of CSS (.css)
162    assets-screen.css   # name of "assets", media type of "screen", an asset type of CSS (.css)
163    assets.js           # name of "assets", an asset type of JavaScript (.js)
164
165If the pattern ends with a "/", then the default pattern will be appended
166
167    xyzzy/          => xyzzy/%n%-l-%f.%e
168
169If the pattern does not have an extension-like ending, then "%.e" will be appended
170
171    xyzzy           => xyzzy.%e
172
173=head2 Strange output or "sticky" content
174
175File::Assets uses built-in caching to share content across different objects (via File::Assets::Cache). If you're having problems
176try disabling the cache by passing "cache => 0" to File::Assets->new
177
178=head1 METHODS
179
180=cut
181
182# If the pattern does NOT begin with a "/", then the base dir will be prepended
183
184use strict;
185use warnings;
186
187use Object::Tiny qw/cache registry _registry_hash rsc filter_scheme output_path_scheme output_asset_scheme/;
188use File::Assets::Carp;
189
190use Tie::LLHash;
191use Path::Resource;
192use Scalar::Util qw/blessed refaddr/;
193use HTML::Declare qw/LINK SCRIPT STYLE/;
194
195use File::Assets::Asset;
196use File::Assets::Cache;
197use File::Assets::Kind;
198use File::Assets::Bucket;
199
200=head2 File::Assets->new( base => <base>, output_path => <output_path>, minify => <minify> )
201
202Create and return a new File::Assets object.
203
204You can configure the object with the following:
205
206    base            # A hash reference with a "uri" key/value and a "dir" key/value.
207                      For example: { uri => http://example.com/assets, dir => /var/www/htdocs/assets }
208
209                    # A URI::ToDisk object
210
211                    # A Path::Resource object
212
213    minify          # "1" or "best" - Will either use JavaScript::Minifier::XS> & CSS::Minifier::XS or
214                                      JavaScript::Minifier> & CSS::Minifier (depending on availability)
215                                      for minification
216
217                    # "0" or "" or undef - Don't do any minfication (this is the default)
218
219                    # "./path/to/yuicompressor.jar" - Will use YUI Compressor via the given .jar for minification
220
221                    # "minifier" - Will use JavaScript::Minifier & CSS::Minifier for minification
222
223                    # "xs" or "minifier-xs" - Will use JavaScript::Minifier::XS & CSS::Minifier::XS for minification
224
225    output_path     # Designates the output path for minified .css and .js assets
226                      The default output path pattern is "%n%-l%-d.%e" (rooted at the dir of <base>)
227                      See above in "Specifying an output_path pattern" for details
228
229=cut
230
231sub new {
232    my $self = bless {}, shift;
233    local %_ = @_;
234
235    $self->set_base($_{rsc} || $_{base_rsc} || $_{base});
236    $self->set_base_uri($_{uri} || $_{base_uri}) if $_{uri} || $_{base_uri};
237    $self->set_base_dir($_{dir} || $_{base_dir}) if $_{dir} || $_{base_dir};
238    $self->set_base_path($_{base_path}) if $_{base_path};
239
240    $self->set_output_path($_{output_path} || $_{output_path_scheme} || []);
241
242    $self->name($_{name});
243
244    $_{cache} = 1 unless exists $_{cache};
245    $self->set_cache($_{cache}) if $_{cache};
246
247#    my $rsc = File::Assets::Util->parse_rsc($_{rsc} || $_{base_rsc} || $_{base});
248#    $rsc->uri($_{uri} || $_{base_uri}) if $_{uri} || $_{base_uri};
249#    $rsc->dir($_{dir} || $_{base_dir}) if $_{dir} || $_{base_dir};
250#    $rsc->path($_{base_path}) if $_{base_path};
251#    $self->{rsc} = $rsc;
252
253    my %registry;
254    $self->{registry} = tie(%registry, qw/Tie::LLHash/, { lazy => 1 });
255    $self->{_registry_hash} = \%registry;
256
257    $self->{filter_scheme} = {};
258    my $filter_scheme = $_{filter} || $_{filters} || $_{filter_scheme} || [];
259    for my $rule (@$filter_scheme) {
260        $self->filter(@$rule);
261    }
262
263    if (my $minify = $_{minify}) {
264        if      ($minify eq 1 || $minify =~ m/^\s*(?:minifier-)?best\s*$/i)  { $self->filter("minifier-best") }
265        elsif   ($minify =~ m/^\s*yui-?compressor:/)                         { $self->filter($minify) }
266        elsif   ($minify =~ m/\.jar/i)                                       { $self->filter("yuicompressor:$minify") }
267        elsif   ($minify =~ m/^\s*(?:minifier-)?xs\s*$/i)                    { $self->filter("minifier-xs") }
268        elsif   ($minify =~ m/^\s*minifier\s*$/i)                            { $self->filter("minifier") }
269        elsif   ($minify =~ m/^\s*concat\s*$/i)                              { $self->filter("concat") }
270        else                                                                 { croak "Don't understand minify option ($minify)" }
271    }
272
273    return $self;
274}
275
276=head2 $asset = $assets->include(<path>, [ <rank>, <type>, { ... } ])
277
278=head2 $asset = $assets->include_path(<path>, [ <rank>, <type>, { ... } ])
279
280First, if <path> is a scalar reference or "looks like" some HTML (starts with a angle bracket, e.g.: <script></script>), then
281it will be treated as inline content.
282
283Otherwise, this will include an asset located at "<base.dir>/<path>" for processing. The asset will be exported as "<base.uri>/<path>"
284
285Optionally, you can specify a rank, where a lower number (i.e. -2, -100) causes the asset to appear earlier in the exports
286list, and a higher number (i.e. 6, 39) causes the asset to appear later in the exports list. By default, all assets start out
287with a neutral rank of 0.
288
289Also, optionally, you can specify a type override as the third argument.
290
291By default, the newly created $asset is NOT inline.
292
293Returns the newly created asset.
294
295NOTE: See below for how the extra hash on the end is handled
296
297=head2 $asset = $assets->include({ ... })
298
299Another way to invoke include is by passing in a hash reference.
300
301The hash reference should contain the follwing information:
302
303    path        # The path to the asset file, relative to base
304    content     # The content of the asset
305
306    type        # Optional if a path is given, required for content
307    rank        # Optional, 0 by default (Less than zero is earlier, greater than zero is later)
308    inline      # Optional, by default true if content was given, false is a path was given
309    base        # Optional, by default the base of $assets
310
311You can also pass extra information through the hash. Any extra information will be bundled in the ->attributes hash of $asset.
312For example, you can control the media type of a text/css asset by doing something like:
313
314    $assets->include("/path/to/printstyle.css", ..., { media => "print" }) # The asset will be exported with the print-media indicator
315
316NOTE: The order of <rank> and <type> doesn't really matter, since we can detect whether something looks like a rank (number) or
317not, and correct for it (and it does).
318
319=cut
320
321sub include_path {
322    my $self = shift;
323    return $self->include(@_);
324}
325
326my $rankish = qr/^[\-\+]?[\.\d]+$/; # A regular expression for a string that looks like a rank
327sub _correct_for_proper_rank_and_type_order ($) {
328    my $asset = shift;
329    if (defined $asset->{type} && $asset->{type} =~ $rankish ||
330        defined $asset->{rank} && $asset->{rank} !~ $rankish) {
331        # Looks like someone entered a rank as the type or vice versa, so we'll switch them
332        my $rank = delete $asset->{type};
333        my $type = delete $asset->{rank};
334        $asset->{type} = $type if defined $type;
335        $asset->{rank} = $rank if defined $rank;
336    }
337}
338
339sub include {
340    my $self = shift;
341
342    my (@asset, $path);
343    if (ref $_[0] ne "HASH") {
344        $path = shift;
345        croak "Don't have a path to include" unless defined $path && length $path;
346        if (ref $path eq "SCALAR" || $path =~ m/^\s*</) {
347            push @asset, content => $path;
348        }
349        else {
350            return $self->fetch($path) if $self->exists($path);
351            push @asset, path => $path;
352        }
353    }
354
355    for (qw/rank type/) {
356        last if ! @_ || ref $_[0] eq "HASH";
357        push @asset, $_ => shift;
358    }
359    push @asset, %{ $_[0] } if @_ && ref $_[0] eq "HASH";
360    my %asset = @asset;
361    _correct_for_proper_rank_and_type_order \%asset;
362
363    my $asset = File::Assets::Asset->new(base => $self->rsc, cache => $self->cache, %asset);
364
365    return $self->fetch_or_store($asset);
366}
367
368=head2 $asset = $assets->include_content(<content>, [ <type>, <rank>, { ... } ])
369
370Include an asset with some content and of the supplied type. The value of <content> can be a "plain" string or a scalar reference.
371
372You can include content that looks like HTML:
373
374    <style media="print">
375    body {
376        font: serif;
377    }
378    </style>
379
380In the above case, <type> is optional, as File::Assets can detect from the tag that you're supplying a style sheet. Furthermore,
381the method will find all the attributes in the tag and put them into the asset. So the resulting asset from including the above
382will have a type of "text/css" and media of "print".
383
384For now, only <style> and <script> will map to types (.css and .js, respectively)
385
386See ->include for more information on <rank>.
387
388By default, the newly created $asset is inline.
389
390Returns the newly created asset.
391
392NOTE: The order of the <type> and <rank> arguments are reversed from ->include and ->include_path
393Still, the order of <rank> and <type> doesn't really matter, since we can detect whether something looks like a rank (number) or
394not, and correct for it (and it does).
395
396=cut
397
398sub include_content {
399    my $self = shift;
400
401    my @asset;
402    for (qw/content type rank/) {
403        last if ! @_ || ref $_[0] eq "HASH";
404        push @asset, $_ => shift;
405    }
406    push @asset, %{ $_[0] } if @_ && ref $_[0] eq "HASH";
407    my %asset = @asset;
408    _correct_for_proper_rank_and_type_order \%asset;
409
410    my $asset = File::Assets::Asset->new(%asset);
411
412    $self->store($asset);
413
414    return $asset;
415}
416
417=head2 $name = $assets->name([ <name> ])
418
419Retrieve and/or change the "name" of $assets; by default it is "assets"
420
421This is useful for controlling the name of minified assets files.
422
423Returns the name of $assets
424
425=cut
426
427sub name {
428    my $self = shift;
429    $self->{name} = shift if @_;
430    my $name = $self->{name};
431    return defined $name && length $name ? $name : "assets";
432}
433
434=head2 $html = $assets->export([ <type> ])
435
436Generate and return HTML for the assets of <type>. If no type is specified, then assets of every type are exported.
437
438$html will be something like this:
439
440    <link rel="stylesheet" type="text/css" href="http://example.com/assets.css">
441    <script src="http://example.com/assets.js" type="text/javascript"></script>
442
443=cut
444
445sub export {
446    my $self = shift;
447    my $type = shift;
448    my $format = shift;
449    $format = "html" unless defined $format;
450    my @assets = $self->exports($type);
451
452    if ($format eq "html") {
453        return $self->_export_html(\@assets);
454    }
455    else {
456        croak "Don't know how to export for format ($format)";
457    }
458}
459
460sub _export_html {
461    my $self = shift;
462    my $assets = shift;
463
464    my @content;
465    for my $asset (@$assets) {
466        my %attributes = %{ $asset->attributes };
467        if ($asset->type->type eq "text/css") {
468#        if ($asset->kind->extension eq "css") {
469            if (! $asset->inline) {
470                push @content, LINK({ rel => "stylesheet", type => $asset->type->type, href => $asset->uri, %attributes });
471            }
472            else {
473                push @content, STYLE({ type => $asset->type->type, %attributes, _ => [ "\n${ $asset->content }" ] });
474            }
475        }
476#        elsif ($asset->kind->extension eq "js") {
477        elsif ($asset->type->type eq "application/javascript" ||
478                $asset->type->type eq "application/x-javascript" || # Handle different MIME::Types versions.
479                $asset->type->type =~ m/\bjavascript\b/) {
480            if (! $asset->inline) {
481                push @content, SCRIPT({ type => "text/javascript", src => $asset->uri, _ => "", %attributes });
482            }
483            else {
484                push @content, SCRIPT({ type => "text/javascript", %attributes, _ => [ "\n${ $asset->content }" ] });
485            }
486        }
487
488        else {
489            croak "Don't know how to handle asset $asset" unless ! $asset->inline;
490            push @content, LINK({ type => $asset->type->type, href => $asset->uri });
491        }
492    }
493    return join "\n", @content;
494}
495
496=head2 @assets = $assets->exports([ <type> ])
497
498Returns a list of assets, in ranking order, that are exported. If no type is specified, then assets of every type are exported.
499
500You can use this method to generate your own HTML, if necessary.
501
502=cut
503
504sub exports {
505    my $self = shift;
506    my @assets = sort { $a->rank <=> $b->rank } $self->_exports(@_);
507    return @assets;
508}
509
510=head2 $assets->empty
511
512Returns 1 if no assets have been included yet, 0 otherwise.
513
514=cut
515
516sub empty {
517    my $self = shift;
518    return keys %{ $self->_registry_hash } ? 0 : 1;
519}
520
521=head2 $assets->exists( <path> )
522
523Returns true if <path> has been included, 0 otherwise.
524
525=cut
526
527sub exists {
528    my $self = shift;
529    my $key = shift;
530
531    return exists $self->_registry_hash->{$key} ? 1 : 0;
532}
533
534=head2 $assets->store( <asset> )
535
536Store <asset> in $assets
537
538=cut
539
540sub store {
541    my $self = shift;
542    my $asset = shift;
543
544    return $self->_registry_hash->{$asset->key} = $asset;
545}
546
547=head2 $asset = $assets->fetch( <path> )
548
549Fetch the asset located at <path>
550
551Returns undef if nothing at <path> exists yet
552
553=cut
554
555sub fetch {
556    my $self = shift;
557    my $key = shift;
558
559    return $self->_registry_hash->{$key};
560}
561
562sub fetch_or_store {
563    my $self = shift;
564    my $asset = shift;
565
566    return $self->fetch($asset->key) if $self->exists($asset->key);
567
568    return $self->store($asset);
569}
570
571sub kind {
572    my $self = shift;
573    my $asset = shift;
574    my $type = $asset->type;
575
576    my $kind = File::Assets::Util->type_extension($type);
577    if (File::Assets::Util->same_type("css", $type)) {
578#        my $media = $asset->attributes->{media} || "screen"; # W3C says to assume screen by default, so we'll do the same.
579        my $media = $asset->attributes->{media};
580        $kind = "$kind-$media" if defined $media && length $media;
581    }
582
583    return File::Assets::Kind->new($kind, $type);
584}
585
586sub _exports {
587    my $self = shift;
588    my $type = shift;
589    $type = File::Assets::Util->parse_type($type);
590    my $hash = $self->_registry_hash;
591    my @assets;
592    if (defined $type) {
593        @assets = grep { $type->type eq $_->type->type } values %$hash;
594    }
595    else {
596        @assets = values %$hash;
597    }
598
599    my %bucket;
600    for my $asset (@assets) {
601        my $kind = $self->kind($asset);
602        my $bucket = $bucket{$kind->kind} ||= File::Assets::Bucket->new($kind, $self);
603        $bucket->add_asset($asset);
604    }
605
606    my $filter_scheme = $self->{filter_scheme};
607    my @global = @{ $filter_scheme->{'*'} || [] };
608    my @bucket;
609    for my $kind (sort keys %bucket) {
610        push @bucket, my $bucket = $bucket{$kind};
611        $bucket->add_filter($_) for @global;
612        my $head = $bucket->kind->head;
613        for my $category (sort grep { ! m/^$head-/ } keys %$filter_scheme) {
614            next if length $category > length $kind; # Too specific
615            next unless 0 == index $kind, $category;
616            $bucket->add_filter($_) for (@{ $filter_scheme->{$category} });
617        }
618    }
619
620    return map { $_->exports } @bucket;
621}
622
623=head2 $assets->set_name( <name> )
624
625Set the name of $assets
626
627This is exactly the same as
628
629    $assets->name( <name> )
630
631=cut
632
633
634=head2 $assets->set_base( <base> )
635
636Set the base uri, dir, and path for assets
637
638<base> can be a L<Path::Resource>, L<URI::ToDisk>, or a hash reference of the form:
639
640    { uri => ..., dir => ..., path => ... }
641
642Given a dir of C</var/www/htdocs>, a uri of C<http://example.com/static>, and a
643path of C<assets> then:
644
645    $assets will look for files in "/var/www/htdocs/assets"
646
647    $assets will "serve" files with "http://example.com/static/assets"
648
649=cut
650
651sub set_base {
652    my $self = shift;
653    croak "No base given" unless @_;
654    my $base = 1 == @_ ? shift : { @_ };
655    croak "No base given" unless $base;
656
657    $self->{rsc} = File::Assets::Util->parse_rsc($base);
658}
659
660=head2 $assets->set_base_uri( <uri> )
661
662Set the base uri for assets
663
664=cut
665
666sub set_base_uri {
667    my $self = shift;
668    croak "No base uri given" unless defined $_[0];
669
670    $self->{rsc}->base->uri(shift);
671}
672
673=head2 $assets->set_base_dir( <dir> )
674
675Set the base dir for assets
676
677=cut
678
679sub set_base_dir {
680    my $self = shift;
681    croak "No base dir given" unless defined $_[0];
682
683    $self->{rsc}->base->dir(shift);
684}
685
686=head2 $assets->set_base_path( <path> )
687
688Set the base path for assets
689
690Passing an undefined value for <path> will clear/get-rid-of the path
691
692=cut
693
694sub set_base_path {
695    my $self = shift;
696    my $path;
697    $path = defined $_[0] ? Path::Abstract->new(shift) :  Path::Abstract->new;
698    # TODO-b This is very bad
699    $self->{rsc}->_path($path);
700}
701
702sub set_output_path_scheme {
703    my $self = shift;
704    my $scheme = shift;
705
706    if ($scheme && ref $scheme ne "ARRAY") {
707        $scheme = [ [ qw/*/ => $scheme ] ];
708    }
709
710    $self->{output_path_scheme} = $scheme;
711}
712
713=head2 $assets->set_output_path( <path> )
714
715Set the output path for assets generated by $assets
716
717See "Specifying an C<output_path> pattern" above
718
719=cut
720
721sub set_output_path {
722    my $self = shift;
723    $self->set_output_path_scheme(@_);
724}
725
726=head2 $assets->set_cache( <cache> )
727
728Specify the cache object or cache name to use
729
730=cut 
731
732sub set_cache {
733    my $self = shift;
734    my $cache = shift;
735
736    if ($cache) {
737        $cache = File::Assets::Cache->new(name => $cache) unless blessed $cache && $cache->isa("File::Assets::Cache");
738        $self->{cache} = $cache;
739    }
740    else {
741        delete $self->{cache};
742    }
743}
744
745sub filter {
746    my $self = shift;
747    my ($kind, $filter);
748    if (@_ == 1) {
749        $filter = shift;
750    }
751    else {
752        $kind = File::Assets::Kind->new(shift);
753        $filter = shift;
754    }
755
756    my $name = $kind ? $kind->kind : '*';
757
758    my $category = $self->{filter_scheme}->{$name} ||= [];
759
760    my $_filter = $filter;
761    unless (blessed $_filter) {
762        croak "Couldn't find filter for ($filter)" unless $_filter = File::Assets::Util->parse_filter($_filter, @_, assets => $self);
763    }
764
765    push @$category, $_filter;
766
767    return $_filter;
768}
769
770sub filter_clear {
771    my $self = shift;
772    if (blessed $_[0] && $_[0]->isa("File::Assets::Filter")) {
773        my $target = shift;
774        while (my ($name, $category) = each %{ $self->{filter_scheme} }) {
775            my @filters = grep { $_ != $target } @$category;
776            $self->{filter_scheme}->{$name} = \@filters;
777        }
778        return;
779    }
780    carp __PACKAGE__, "::filter_clear(\$type) is deprecated, nothing happens" and return if @_;
781    $self->{filter_scheme} = {};
782}
783
784sub _calculate_best {
785    my $self = shift;
786    my $scheme = shift;
787    my $kind = shift;
788    my $signature = shift;
789    my $handler = shift;
790    my $default = shift;
791
792    my $key = join ":", $kind->kind, $signature;
793
794    my ($best_kind, %return);
795    %return = %$default if $default;
796
797    # TODO-f Cache the result of this
798    for my $rule (@$scheme) {
799        my ($condition, $action, $flags) = @$rule;
800
801        my $result; # 1 - A better match; -1 - A match, but worse; undef - Skip, not a match!
802
803        if (ref $condition eq "CODE") {
804            next unless defined ($result = $condition->($kind, $signature, $best_kind));
805        }
806        elsif (ref $condition eq "") {
807            if ($condition eq $key) {
808                # Best possible match
809                $result = 1;
810                $best_kind = $kind;
811            }
812            elsif ($condition eq "*" || $condition eq "default") {
813                $result = $best_kind ? -1 : 1;
814            }
815        }
816
817        my ($condition_kind, $condition_signature) = split m/:/, $condition, 2;
818
819        unless (defined $result) {
820
821            # No exact match, try to find the best fit...
822
823            # Signature doesn't match or is not a wildcard, so move on to the next rule
824            next if defined $condition_signature && $condition_signature ne '*' && $condition_signature ne $signature;
825
826            if (length $condition_kind && $condition_kind ne '*') {
827                $condition_kind = File::Assets::Kind->new($condition_kind);
828
829                # Type isn't the same as the asset (or whatever) kind, so move on to the next rule
830                next unless File::Assets::Util->same_type($condition_kind->type, $kind->type);
831            }
832        }
833
834        # At this point, we have a match, but is it a better match then one we already have?
835        if (! $best_kind || ($condition_kind && $condition_kind->is_better_than($best_kind))) {
836            $result = 1;
837        }
838
839        next unless defined $result;
840
841        my %action;
842        %action = $handler->($action);
843
844        if ($result > 0) {
845            $return{$_} = $action{$_} for keys %action;
846        }
847        else {
848            for (keys %action) {
849                $return{$_} = $action{$_} unless defined $action{$_};
850            }
851        }
852    }
853
854    return \%return;
855}
856
857sub output_path {
858    my $self = shift;
859    my $filter = shift;
860
861    my $result = $self->_calculate_best($self->{output_path_scheme}, $filter->kind, $filter->signature, sub {
862        my $action = shift;
863        return ref $action eq "CODE" ? %$action : path => $action;
864    });
865
866    return $result;
867}
868
869sub output_asset {
870    my $self = shift;
871    my $filter = shift;
872
873    if (0) {
874        my $result = $self->_calculate_best($self->{output_asset_scheme}, $filter->kind, $filter->signature, sub {
875            my $action = shift;
876            return %$action;
877        });
878    }
879
880    my $kind = $filter->kind;
881    my $output_path = $self->output_path($filter) or croak "Couldn't get output path for ", $kind->kind;
882    $output_path = File::Assets::Util->build_output_path($output_path, $filter);
883
884    my $asset = File::Assets::Asset->new(path => $output_path, base => $self->rsc, type => $kind->type);
885    return $asset;
886}
887
8881;
889
890=head1 AUTHOR
891
892Robert Krimen, C<< <rkrimen at cpan.org> >>
893
894=head1 SEE ALSO
895
896L<Catalyst::Plugin::Assets>
897
898L<Google::AJAX::Library>
899
900L<JS::YUI::Loader>
901
902L<JS::jQuery::Loader>
903
904=head1 SOURCE
905
906You can contribute or fork this project via GitHub:
907
908L<http://github.com/robertkrimen/file-assets/tree/master>
909
910    git clone git://github.com/robertkrimen/file-assets.git File-Assets
911
912=head1 BUGS
913
914Please report any bugs or feature requests to C<bug-file-assets at rt.cpan.org>, or through
915the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=File-Assets>.  I will be notified, and then you'll
916automatically be notified of progress on your bug as I make changes.
917
918=head1 SUPPORT
919
920You can find documentation for this module with the perldoc command.
921
922    perldoc File::Assets
923
924
925You can also look for information at:
926
927=over 4
928
929=item * RT: CPAN's request tracker
930
931L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=File-Assets>
932
933=item * AnnoCPAN: Annotated CPAN documentation
934
935L<http://annocpan.org/dist/File-Assets>
936
937=item * CPAN Ratings
938
939L<http://cpanratings.perl.org/d/File-Assets>
940
941=item * Search CPAN
942
943L<http://search.cpan.org/dist/File-Assets>
944
945=back
946
947
948=head1 ACKNOWLEDGEMENTS
949
950
951=head1 COPYRIGHT & LICENSE
952
953Copyright 2008 Robert Krimen
954
955This program is free software; you can redistribute it and/or modify it
956under the same terms as Perl itself.
957
958
959=cut
960
9611; # End of File::Assets
962
963__END__
964
965#    if (my $cache = $self->cache) {
966#        return 1 if $cache->exists($self->rsc->dir, $key);
967#    }
968
969#    if (my $cache = $self->cache) {
970#        $cache->store($self->rsc->dir, $asset);
971#    }
972
973#    if (my $cache = $self->cache) {
974#        if ($asset = $cache->fetch($self->rsc->dir, $key)) {
975#            return $self->store($asset);
976#        }
977#    }
978
979