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