package HTML::DeferableCSS; # ABSTRACT: Simplify management of stylesheets in your HTML use v5.10; use Moo 1.006000; use Carp; use Devel::StrictMode; use File::ShareDir 1.112 qw/ dist_file /; use MooX::TypeTiny; use List::Util 1.45 qw/ first uniqstr /; use Path::Tiny; use Types::Path::Tiny qw/ Dir File Path /; use Types::Common::Numeric qw/ PositiveOrZeroInt /; use Types::Common::String qw/ NonEmptySimpleStr SimpleStr /; use Types::Standard qw/ Bool CodeRef HashRef Maybe Tuple /; # RECOMMEND PREREQ: Type::Tiny::XS use namespace::autoclean; our $VERSION = 'v0.4.2'; has aliases => ( is => 'ro', isa => STRICT ? HashRef [Maybe[SimpleStr]] : HashRef, required => 1, coerce => sub { return { map { $_ => $_ } @{$_[0]} } if ref $_[0] eq 'ARRAY'; return $_[0]; }, ); has css_root => ( is => 'ro', isa => Dir, coerce => 1, required => 1, ); has url_base_path => ( is => 'ro', isa => SimpleStr, default => '/', ); has prefer_min => ( is => 'ro', isa => Bool, default => 1, ); has css_files => ( is => 'lazy', isa => STRICT ? HashRef [ Tuple [ Maybe[Path], Maybe[NonEmptySimpleStr], PositiveOrZeroInt ] ] : HashRef, builder => 1, coerce => 1, ); use constant PATH => 0; use constant NAME => 1; use constant SIZE => 2; sub _build_css_files { my ($self) = @_; my $root = $self->css_root; my $min = $self->prefer_min; my %files; for my $name (keys %{ $self->aliases }) { my $base = $self->aliases->{$name}; if (!$base) { $files{$name} = [ undef, undef, 0 ]; } elsif ($base =~ m{^(\w+:)?//}) { $files{$name} = [ undef, $base, 0 ]; } else { $base = $name if $base eq '1'; $base =~ s/(?:\.min)?\.css$//; my @bases = $min ? ( "${base}.min.css", "${base}.css", $base ) : ( "${base}.css", "${base}.min.css", $base ); my $file = first { $_->exists } map { path( $root, $_ ) } @bases; unless ($file) { $self->log->( error => "alias '$name' refers to a non-existent file" ); next; } # PATH NAME SIZE $files{$name} = [ $file, $file->relative($root)->stringify, $file->stat->size ]; } } return \%files; } has cdn_links => ( is => 'ro', isa => STRICT ? HashRef [NonEmptySimpleStr] : HashRef, predicate => 1, ); has use_cdn_links => ( is => 'lazy', isa => Bool, builder => 'has_cdn_links', ); has inline_max => ( is => 'ro', isa => PositiveOrZeroInt, default => 1024, ); has defer_css => ( is => 'ro', isa => Bool, default => 1, ); has include_noscript => ( is => 'lazy', isa => Bool, builder => 'defer_css', ); has preload_script => ( is => 'lazy', isa => File, coerce => 1, builder => sub { dist_file( qw/ HTML-DeferableCSS cssrelpreload.min.js / ) }, ); has link_template => ( is => 'ro', isa => CodeRef, builder => sub { return sub { sprintf('', @_) }, }, ); has preload_template => ( is => 'lazy', isa => CodeRef, builder => sub { my ($self) = @_; if ($self->simple) { return sub { sprintf('' . '', $_[0], $_[0]) } } else { return sub { sprintf('', $_[0]) }; } }, ); has asset_id => ( is => 'ro', isa => NonEmptySimpleStr, predicate => 1, ); has log => ( is => 'ro', isa => CodeRef, builder => sub { return sub { my ($level, $message) = @_; croak $message if ($level eq 'error'); carp $message; }; }, ); has simple => ( is => 'ro', isa => Bool, default => 0, ); sub check { my ($self) = @_; my $files = $self->css_files; scalar(keys %$files) or return $self->log->( error => "no aliases" ); return 1; } sub href { my ($self, $name, $file) = @_; $file //= $self->_get_file($name) or return; if (defined $file->[PATH]) { my $href = $self->url_base_path . $file->[NAME]; $href .= '?' . $self->asset_id if $self->has_asset_id; if ($self->use_cdn_links && $self->has_cdn_links) { return $self->cdn_links->{$name} // $href; } return $href; } else { return $file->[NAME]; } } sub link_html { my ( $self, $name, $file ) = @_; if (my $href = $self->href( $name, $file )) { return $self->link_template->($href); } else { return ""; } } sub inline_html { my ( $self, $name, $file ) = @_; $file //= $self->_get_file($name) or return; if (my $path = $file->[PATH]) { if ($file->[SIZE]) { return ""; } $self->log->( warning => "empty file '$path'" ); return ""; } else { $self->log->( error => "'$name' refers to a URI" ); return; } } sub link_or_inline_html { my ($self, @names ) = @_; my $buffer = ""; foreach my $name (uniqstr @names) { my $file = $self->_get_file($name) or next; if ( $file->[PATH] && ($file->[SIZE] <= $self->inline_max)) { $buffer .= $self->inline_html($name, $file); } else { $buffer .= $self->link_html($name, $file); } } return $buffer; } sub deferred_link_html { my ($self, @names) = @_; my $buffer = ""; my @deferred; for my $name (uniqstr @names) { my $file = $self->_get_file($name) or next; if ($file->[PATH] && $file->[SIZE] <= $self->inline_max) { $buffer .= $self->inline_html($name, $file); } elsif ($self->defer_css) { my $href = $self->href($name, $file); push @deferred, $href; $buffer .= $self->preload_template->($href); } else { $buffer .= $self->link_html($name, $file); } } if (@deferred) { $buffer .= "" if $self->include_noscript; $buffer .= "" unless $self->simple; } return $buffer; } sub _get_file { my ($self, $name) = @_; unless (defined $name) { $self->log->( error => "missing name" ); return; } if (my $file = $self->css_files->{$name}) { return $file; } else { $self->log->( error => "invalid name '$name'" ); return; } } 1; __END__ =pod =encoding UTF-8 =head1 NAME HTML::DeferableCSS - Simplify management of stylesheets in your HTML =head1 VERSION version v0.4.2 =head1 SYNOPSIS use HTML::DeferableCSS; my $css = HTML::DeferableCSS->new( css_root => '/var/www/css', url_base_path => '/css', inline_max => 512, simple => 1, aliases => { reset => 1, jqui => 'jquery-ui', site => 'style', }, cdn => { jqui => '//cdn.example.com/jquery-ui.min.css', }, ); $css->check or die "Something is wrong"; ... print $css->deferred_link_html( qw[ jqui site ] ); =head1 DESCRIPTION This is an experimental module for generating HTML-snippets for deferable stylesheets. This allows the stylesheets to be loaded asynchronously, allowing the page to be rendered faster. Ideally, this would be a simple matter of changing stylesheet links to something like but this is not well supported by all web browsers. So a web page needs to use some L to handle this, as well as a C