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