File Coverage

blib/lib/HTML/DeferableCSS.pm
Criterion Covered Total %
statement 133 139 95.6
branch 46 52 88.4
condition 13 18 72.2
subroutine 33 33 100.0
pod 6 6 100.0
total 231 248 93.1


line stmt bran cond sub pod time code
1             package HTML::DeferableCSS;
2              
3             # ABSTRACT: Simplify management of stylesheets in your HTML
4              
5 4     4   450361 use v5.10;
  4         49  
6 4     4   1866 use Moo 1.006000;
  4         34689  
  4         22  
7              
8 4     4   5931 use Carp;
  4         8  
  4         182  
9 4     4   1557 use Devel::StrictMode;
  4         1440  
  4         210  
10 4     4   1355 use File::ShareDir 1.112 qw/ dist_file /;
  4         65569  
  4         208  
11 4     4   1430 use MooX::TypeTiny;
  4         1002  
  4         21  
12 4     4   44153 use List::Util 1.45 qw/ first uniqstr /;
  4         77  
  4         424  
13 4     4   2886 use Path::Tiny;
  4         39546  
  4         225  
14 4     4   1783 use Types::Path::Tiny qw/ Dir File Path /;
  4         441380  
  4         37  
15 4     4   4710 use Types::Common::Numeric qw/ PositiveOrZeroInt /;
  4         76490  
  4         37  
16 4     4   3834 use Types::Common::String qw/ NonEmptySimpleStr SimpleStr /;
  4         186034  
  4         56  
17 4     4   2801 use Types::Standard qw/ Bool CodeRef HashRef Maybe Tuple /;
  4         11  
  4         26  
18              
19             # RECOMMEND PREREQ: Type::Tiny::XS
20              
21 4     4   6467 use namespace::autoclean;
  4         52206  
  4         24  
22              
23             our $VERSION = 'v0.4.2';
24              
25              
26              
27             has aliases => (
28             is => 'ro',
29             isa => STRICT ? HashRef [Maybe[SimpleStr]] : HashRef,
30             required => 1,
31             coerce => sub {
32             return { map { $_ => $_ } @{$_[0]} } if ref $_[0] eq 'ARRAY';
33             return $_[0];
34             },
35             );
36              
37              
38             has css_root => (
39             is => 'ro',
40             isa => Dir,
41             coerce => 1,
42             required => 1,
43             );
44              
45              
46             has url_base_path => (
47             is => 'ro',
48             isa => SimpleStr,
49             default => '/',
50             );
51              
52              
53             has prefer_min => (
54             is => 'ro',
55             isa => Bool,
56             default => 1,
57             );
58              
59              
60             has css_files => (
61             is => 'lazy',
62             isa => STRICT
63             ? HashRef [ Tuple [ Maybe[Path], Maybe[NonEmptySimpleStr], PositiveOrZeroInt ] ]
64             : HashRef,
65             builder => 1,
66             coerce => 1,
67             );
68              
69 4     4   958 use constant PATH => 0;
  4         8  
  4         209  
70 4     4   25 use constant NAME => 1;
  4         8  
  4         167  
71 4     4   22 use constant SIZE => 2;
  4         8  
  4         5755  
72              
73             sub _build_css_files {
74 28     28   6381 my ($self) = @_;
75              
76 28         112 my $root = $self->css_root;
77 28         68 my $min = $self->prefer_min;
78              
79 28         46 my %files;
80 28         50 for my $name (keys %{ $self->aliases }) {
  28         148  
81 38         16499 my $base = $self->aliases->{$name};
82 38 100       162 if (!$base) {
    100          
83 6         18 $files{$name} = [ undef, undef, 0 ];
84             }
85             elsif ($base =~ m{^(\w+:)?//}) {
86 4         18 $files{$name} = [ undef, $base, 0 ];
87             }
88             else {
89 28 100       88 $base = $name if $base eq '1';
90 28         70 $base =~ s/(?:\.min)?\.css$//;
91 28 100       124 my @bases = $min
92             ? ( "${base}.min.css", "${base}.css", $base )
93             : ( "${base}.css", "${base}.min.css", $base );
94              
95 28     45   135 my $file = first { $_->exists } map { path( $root, $_ ) } @bases;
  45         1268  
  84         1892  
96 28 100       751 unless ($file) {
97 4         28 $self->log->( error => "alias '$name' refers to a non-existent file" );
98 0         0 next;
99             }
100             # PATH NAME SIZE
101 24         112 $files{$name} = [ $file, $file->relative($root)->stringify, $file->stat->size ];
102             }
103             }
104              
105 24         21848 return \%files;
106             }
107              
108              
109             has cdn_links => (
110             is => 'ro',
111             isa => STRICT ? HashRef [NonEmptySimpleStr] : HashRef,
112             predicate => 1,
113             );
114              
115              
116             has use_cdn_links => (
117             is => 'lazy',
118             isa => Bool,
119             builder => 'has_cdn_links',
120             );
121              
122              
123             has inline_max => (
124             is => 'ro',
125             isa => PositiveOrZeroInt,
126             default => 1024,
127             );
128              
129              
130             has defer_css => (
131             is => 'ro',
132             isa => Bool,
133             default => 1,
134             );
135              
136              
137             has include_noscript => (
138             is => 'lazy',
139             isa => Bool,
140             builder => 'defer_css',
141             );
142              
143              
144             has preload_script => (
145             is => 'lazy',
146             isa => File,
147             coerce => 1,
148 1     1   397 builder => sub { dist_file( qw/ HTML-DeferableCSS cssrelpreload.min.js / ) },
149             );
150              
151              
152             has link_template => (
153             is => 'ro',
154             isa => CodeRef,
155             builder => sub {
156 18     18   113 return sub { sprintf('<link rel="stylesheet" href="%s">', @_) },
157 26     26   8712 },
158             );
159              
160              
161              
162             has preload_template => (
163             is => 'lazy',
164             isa => CodeRef,
165             builder => sub {
166 2     2   20 my ($self) = @_;
167 2 100       8 if ($self->simple) {
168             return sub {
169 1     1   13 sprintf('<link rel="preload" as="style" href="%s">' .
170             '<link rel="stylesheet" href="%s" media="print" onload="this.media=\'all\';this.onload=null;">',
171             $_[0], $_[0])
172             }
173 1         18 }
174             else {
175 1     1   17 return sub { sprintf('<link rel="preload" as="style" href="%s" onload="this.onload=null;this.rel=\'stylesheet\'">', $_[0]) };
  1         14  
176             }
177             },
178             );
179              
180              
181             has asset_id => (
182             is => 'ro',
183             isa => NonEmptySimpleStr,
184             predicate => 1,
185             );
186              
187              
188             has log => (
189             is => 'ro',
190             isa => CodeRef,
191             builder => sub {
192             return sub {
193 8     8   29 my ($level, $message) = @_;
194 8 100       130 croak $message if ($level eq 'error');
195 2         21 carp $message;
196 26     26   656 };
197             },
198             );
199              
200              
201             has simple => (
202             is => 'ro',
203             isa => Bool,
204             default => 0,
205             );
206              
207              
208             sub check {
209 5     5 1 2838 my ($self) = @_;
210              
211 5         133 my $files = $self->css_files;
212              
213 3 100       182 scalar(keys %$files) or
214             return $self->log->( error => "no aliases" );
215              
216 2         13 return 1;
217             }
218              
219              
220             sub href {
221 28     28 1 2146 my ($self, $name, $file) = @_;
222 28 50 66     92 $file //= $self->_get_file($name) or return;
223 28 100       58 if (defined $file->[PATH]) {
224 20         62 my $href = $self->url_base_path . $file->[NAME];
225 20 100       58 $href .= '?' . $self->asset_id if $self->has_asset_id;
226 20 100 66     299 if ($self->use_cdn_links && $self->has_cdn_links) {
227 1   33     23 return $self->cdn_links->{$name} // $href;
228             }
229 19         231 return $href;
230             }
231             else {
232 8         24 return $file->[NAME];
233             }
234             }
235              
236              
237             sub link_html {
238 21     21 1 3129 my ( $self, $name, $file ) = @_;
239 21 100       45 if (my $href = $self->href( $name, $file )) {
240 17         45 return $self->link_template->($href);
241             }
242             else {
243 4         14 return "";
244             }
245             }
246              
247              
248             sub inline_html {
249 9     9 1 630 my ( $self, $name, $file ) = @_;
250 9 50 66     29 $file //= $self->_get_file($name) or return;
251 9 100       33 if (my $path = $file->[PATH]) {
252 8 100       18 if ($file->[SIZE]) {
253 6         19 return "<style>" . $file->[PATH]->slurp_raw . "</style>";
254             }
255 2         8 $self->log->( warning => "empty file '$path'" );
256 2         753 return "";
257             }
258             else {
259 1         7 $self->log->( error => "'$name' refers to a URI" );
260 0         0 return;
261             }
262             }
263              
264              
265             sub link_or_inline_html {
266 5     5 1 2061 my ($self, @names ) = @_;
267 5         11 my $buffer = "";
268 5         19 foreach my $name (uniqstr @names) {
269 8 50       155 my $file = $self->_get_file($name) or next;
270 8 100 100     46 if ( $file->[PATH] && ($file->[SIZE] <= $self->inline_max)) {
271 3         9 $buffer .= $self->inline_html($name, $file);
272             }
273             else {
274 5         13 $buffer .= $self->link_html($name, $file);
275             }
276             }
277 5         151 return $buffer;
278             }
279              
280              
281             sub deferred_link_html {
282 6     6 1 2065 my ($self, @names) = @_;
283 6         11 my $buffer = "";
284 6         10 my @deferred;
285 6         21 for my $name (uniqstr @names) {
286 7 50       17 my $file = $self->_get_file($name) or next;
287 7 100 100     54 if ($file->[PATH] && $file->[SIZE] <= $self->inline_max) {
    100          
288 1         5 $buffer .= $self->inline_html($name, $file);
289             }
290             elsif ($self->defer_css) {
291 2         7 my $href = $self->href($name, $file);
292 2         5 push @deferred, $href;
293 2         31 $buffer .= $self->preload_template->($href);
294             }
295             else {
296 4         12 $buffer .= $self->link_html($name, $file);
297             }
298             }
299              
300 6 100       194 if (@deferred) {
301              
302             $buffer .= "<noscript>" .
303 2 100       31 join("", map { $self->link_template->($_) } @deferred ) .
  1         31  
304             "</noscript>" if $self->include_noscript;
305              
306 2 100       31 $buffer .= "<script>" .
307             $self->preload_script->slurp_raw .
308             "</script>" unless $self->simple;
309              
310             }
311              
312 6         175 return $buffer;
313             }
314              
315             sub _get_file {
316 37     37   65 my ($self, $name) = @_;
317 37 50       78 unless (defined $name) {
318 0         0 $self->log->( error => "missing name" );
319 0         0 return;
320             }
321 37 50       672 if (my $file = $self->css_files->{$name}) {
322 37         874 return $file;
323             }
324             else {
325 0           $self->log->( error => "invalid name '$name'" );
326 0           return;
327             }
328              
329             }
330              
331              
332              
333             1;
334              
335             __END__
336              
337             =pod
338              
339             =encoding UTF-8
340              
341             =head1 NAME
342              
343             HTML::DeferableCSS - Simplify management of stylesheets in your HTML
344              
345             =head1 VERSION
346              
347             version v0.4.2
348              
349             =head1 SYNOPSIS
350              
351             use HTML::DeferableCSS;
352              
353             my $css = HTML::DeferableCSS->new(
354             css_root => '/var/www/css',
355             url_base_path => '/css',
356             inline_max => 512,
357             simple => 1,
358             aliases => {
359             reset => 1,
360             jqui => 'jquery-ui',
361             site => 'style',
362             },
363             cdn => {
364             jqui => '//cdn.example.com/jquery-ui.min.css',
365             },
366             );
367              
368             $css->check or die "Something is wrong";
369              
370             ...
371              
372             print $css->deferred_link_html( qw[ jqui site ] );
373              
374             =head1 DESCRIPTION
375              
376             This is an experimental module for generating HTML-snippets for
377             deferable stylesheets.
378              
379             This allows the stylesheets to be loaded asynchronously, allowing the
380             page to be rendered faster.
381              
382             Ideally, this would be a simple matter of changing stylesheet links
383             to something like
384              
385             <link rel="preload" as="stylesheet" href="....">
386              
387             but this is not well supported by all web browsers. So a web page needs
388             to use some L<JavaScript|https://github.com/filamentgroup/loadCSS>
389             to handle this, as well as a C<noscript> block as a fallback.
390              
391             This module allows you to simplify the management of stylesheets for a
392             web application, from development to production by
393              
394             =over
395              
396             =item *
397              
398             declaring all stylesheets used by your web application;
399              
400             =item *
401              
402             specifying remote aliases for stylesheets, e.g. from a CDN;
403              
404             =item *
405              
406             enable or disable the use of minified stylesheets;
407              
408             =item *
409              
410             switch between local copies of stylesheets or CDN versions;
411              
412             =item *
413              
414             automatically inline small stylesheets;
415              
416             =item *
417              
418             use deferred-loading stylesheets, which requires embedding JavaScript
419             code as a workaround for web browsers that do not support these
420             natively.
421              
422             =back
423              
424             =head1 ATTRIBUTES
425              
426             =head2 aliases
427              
428             This is a required hash reference of names and their relative
429             filenames to L</css_root>.
430              
431             It is recommended that the F<.css> and F<.min.css> suffixes be
432             omitted.
433              
434             If the name is the same as the filename (without the extension) than
435             you can simply use C<1>. (Likewise, an empty string or C<0> disables
436             the alias:
437              
438             my $css = HTML::DeferableCSS->new(
439             aliases => {
440             reset => 1,
441             gone => 0, # "gone" will be silently ignored
442             one => "1.css", #
443             }
444             ...
445             );
446              
447             If all names are the same as their filenames, then an array reference
448             can be used:
449              
450             my $css = HTML::DeferableCSS->new(
451             aliases => [ qw( foo bar } ],
452             ...
453             );
454              
455             If an alias is disabled, then it will simply be ignored, e.g.
456              
457             $css->deferred_link_html('gone')
458              
459             Returns an empty string. This allows you to disable a stylesheet in
460             your configuration without having to remove all references to it.
461              
462             Absolute paths cannot be used.
463              
464             You may specify URLs instead of files, but this is not recommended,
465             except for cases when the files are not available locally.
466              
467             =head2 css_root
468              
469             This is the required root directory where all stylesheets can be
470             found.
471              
472             =head2 url_base_path
473              
474             This is the URL prefix for stylesheets.
475              
476             It can be a full URL prefix.
477              
478             =head2 prefer_min
479              
480             If true (default), then a file with the F<.min.css> suffix will be
481             preferred, if it exists in the same directory.
482              
483             Note that this does not do any minification. You will need separate
484             tools for that.
485              
486             =head2 css_files
487              
488             This is a hash reference used internally to translate L</aliases>
489             into the actual files or URLs.
490              
491             If files cannot be found, then it will throw an error. (See
492             L</check>).
493              
494             =head2 cdn_links
495              
496             This is a hash reference of L</aliases> to URLs. (Only one URL per
497             alias is supported.)
498              
499             When L</use_cdn_links> is true, then these URLs will be used instead
500             of local versions.
501              
502             =head2 has_cdn_links
503              
504             This is true when there are L</cdn_links>.
505              
506             =head2 use_cdn_links
507              
508             When true, this will prefer CDN URLs instead of local files.
509              
510             =head2 inline_max
511              
512             This specifies the maximum size of an file to inline.
513              
514             Local files under the size will be inlined using the
515             L</link_or_inline_html> or L</deferred_link_html> methods.
516              
517             Setting this to 0 disables the use of inline links, unless
518             L</inline_html> is called explicitly.
519              
520             =head2 defer_css
521              
522             True by default.
523              
524             This is used by L</deferred_link_html> to determine whether to emit
525             code for deferred stylesheets.
526              
527             =head2 include_noscript
528              
529             When true, a C<noscript> element will be included with non-deffered
530             links.
531              
532             This defaults to the same value as L</defer_css>.
533              
534             =head2 preload_script
535              
536             This is the pathname of the F<cssrelpreload.js> file that will be
537             embedded in the resulting code.
538              
539             The script comes from L<https://github.com/filamentgroup/loadCSS>.
540              
541             You do not need to modify this unless you want to use a different
542             script from the one included with this module.
543              
544             =head2 link_template
545              
546             This is a code reference for a subroutine that returns a stylesheet link.
547              
548             =head2 preload_template
549              
550             This is a code reference for a subroutine that returns a stylesheet
551             preload link.
552              
553             =head2 asset_id
554              
555             This is an optional static asset id to append to local links. It may
556             refer to a version number or commit-id, for example.
557              
558             This is useful to ensure that changes to stylesheets are picked up by
559             web browsers that would otherwise use cached copies of older versions
560             of files.
561              
562             =head2 has_asset_id
563              
564             True if there is an L</asset_id>.
565              
566             =head2 log
567              
568             This is a code reference for logging errors and warnings:
569              
570             $css->log->( $level => $message );
571              
572             By default, this is a wrapper around L<Carp> that dies when the level
573             is "error", and emits a warning for everything else.
574              
575             You can override this so that errors are treated as warnings,
576              
577             log => sub { warn $_[1] },
578              
579             or that warnings are fatal,
580              
581             log => sub { die $_[1] },
582              
583             or even integrate this with your own logging system:
584              
585             log => sub { $logger->log(@_) },
586              
587             =head2 simple
588              
589             When true, this enables a simpler method of using deferable CSS,
590             without the need for the C<loadCSS> script.
591              
592             It is false by default, for backwards compatability. But it is
593             recommended that you set this to true.
594              
595             See L<https://www.filamentgroup.com/lab/load-css-simpler/>.
596              
597             This attribute was added in v0.4.0.
598              
599             =head1 METHODS
600              
601             =head2 check
602              
603             This method instantiates lazy attributes and performs some minimal
604             checks on the data. (This should be called instead of L</css_files>.)
605              
606             It will throw an error or return false (depending on L</log>) if there
607             is something wrong.
608              
609             This was added in v0.3.0.
610              
611             =head2 href
612              
613             my $href = $css->href( $alias );
614              
615             This returns this URL for an alias.
616              
617             =head2 link_html
618              
619             my $html = $css->link_html( $alias );
620              
621             This returns the link HTML markup for the stylesheet referred to by
622             C<$alias>.
623              
624             =head2 inline_html
625              
626             my $html = $css->inline_html( $alias );
627              
628             This returns an embedded stylesheet referred to by C<$alias>.
629              
630             =head2 link_or_inline_html
631              
632             my $html = $css->link_or_inline_html( @aliases );
633              
634             This returns either the link HTML markup, or the embedded stylesheet,
635             if the file size is not greater than L</inline_max>.
636              
637             Note that a stylesheet will be inlined, even if there is are
638             L</cdn_links>.
639              
640             =head2 deferred_link_html
641              
642             my $html = $css->deferred_link_html( @aliases );
643              
644             This returns the HTML markup for the stylesheets specified by
645             L</aliases>, as appropriate for each stylesheet.
646              
647             If the stylesheets are not greater than L</inline_max>, then it will
648             embed them. Otherwise it will return the appropriate markup,
649             depending on L</defer_css>.
650              
651             =for Pod::Coverage PATH NAME SIZE
652              
653             =head1 KNOWN ISSUES
654              
655             =head2 Content-Security-Policy (CSP)
656              
657             If a web site configures a
658             L<Content-Security-Policy|https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP>
659             setting to disable all inlined JavaScript, then the JavaScript shim will
660             not work.
661              
662             =head2 XHTML Support
663              
664             This module is written for HTML5.
665              
666             It does not support XHTML self-closing elements or embedding styles
667             and scripts in CDATA sections.
668              
669             =head2 Encoding
670              
671             All files are embedded as raw files.
672              
673             No URL encoding is done on the HTML links or L</asset_id>.
674              
675             =head2 It's spelled "Deferrable"
676              
677             It's also spelled "Deferable".
678              
679             =head1 SOURCE
680              
681             The development version is on github at L<https://github.com/robrwo/HTML-DeferableCSS>
682             and may be cloned from L<git://github.com/robrwo/HTML-DeferableCSS.git>
683              
684             =head1 BUGS
685              
686             Please report any bugs or feature requests on the bugtracker website
687             L<https://github.com/robrwo/HTML-DeferableCSS/issues>
688              
689             When submitting a bug or request, please include a test-file or a
690             patch to an existing test-file that illustrates the bug or desired
691             feature.
692              
693             Please report any bugs in F<cssrelpreload.js> to
694             L<https://github.com/filamentgroup/loadCSS/issues>.
695              
696             =head1 AUTHOR
697              
698             Robert Rothenberg <rrwo@cpan.org>
699              
700             This module was developed from work for Science Photo Library
701             L<https://www.sciencephoto.com>.
702              
703             F<reset.css> comes from L<http://meyerweb.com/eric/tools/css/reset/>.
704              
705             F<cssrelpreload.js> comes from L<https://github.com/filamentgroup/loadCSS/>.
706              
707             =head1 COPYRIGHT AND LICENSE
708              
709             This software is Copyright (c) 2020 by Robert Rothenberg.
710              
711             This is free software, licensed under:
712              
713             The MIT (X11) License
714              
715             =cut