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   527381 use v5.10;
  4         53  
6 4     4   2443 use Moo 1.006000;
  4         40228  
  4         23  
7              
8 4     4   6708 use Carp;
  4         9  
  4         221  
9 4     4   1930 use Devel::StrictMode;
  4         1665  
  4         240  
10 4     4   1575 use File::ShareDir 1.112 qw/ dist_file /;
  4         75732  
  4         237  
11 4     4   1752 use MooX::TypeTiny;
  4         1231  
  4         24  
12 4     4   53172 use List::Util 1.45 qw/ first uniqstr /;
  4         99  
  4         537  
13 4     4   3656 use Path::Tiny;
  4         47043  
  4         238  
14 4     4   2020 use Types::Path::Tiny qw/ Dir File Path /;
  4         518727  
  4         39  
15 4     4   5011 use Types::Common::Numeric qw/ PositiveOrZeroInt /;
  4         90565  
  4         35  
16 4     4   4249 use Types::Common::String qw/ NonEmptySimpleStr SimpleStr /;
  4         216203  
  4         55  
17 4     4   3044 use Types::Standard qw/ Bool CodeRef HashRef Maybe Tuple /;
  4         10  
  4         27  
18              
19             # RECOMMEND PREREQ: Type::Tiny::XS
20              
21 4     4   7183 use namespace::autoclean;
  4         60226  
  4         25  
22              
23             our $VERSION = 'v0.4.0';
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   1181 use constant PATH => 0;
  4         10  
  4         260  
70 4     4   33 use constant NAME => 1;
  4         16  
  4         215  
71 4     4   27 use constant SIZE => 2;
  4         18  
  4         6952  
72              
73             sub _build_css_files {
74 28     28   7240 my ($self) = @_;
75              
76 28         100 my $root = $self->css_root;
77 28         67 my $min = $self->prefer_min;
78              
79 28         49 my %files;
80 28         95 for my $name (keys %{ $self->aliases }) {
  28         122  
81 38         18255 my $base = $self->aliases->{$name};
82 38 100       163 if (!$base) {
    100          
83 6         21 $files{$name} = [ undef, undef, 0 ];
84             }
85             elsif ($base =~ m{^(\w+:)?//}) {
86 4         19 $files{$name} = [ undef, $base, 0 ];
87             }
88             else {
89 28 100       80 $base = $name if $base eq '1';
90 28         76 $base =~ s/(?:\.min)?\.css$//;
91 28 100       120 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         1408  
  84         2145  
96 28 100       848 unless ($file) {
97 4         27 $self->log->( error => "alias '$name' refers to a non-existent file" );
98 0         0 next;
99             }
100             # PATH NAME SIZE
101 24         114 $files{$name} = [ $file, $file->relative($root)->stringify, $file->stat->size ];
102             }
103             }
104              
105 24         25016 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   536 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   134 return sub { sprintf('<link rel="stylesheet" href="%s">', @_) },
157 26     26   9218 },
158             );
159              
160              
161              
162             has preload_template => (
163             is => 'lazy',
164             isa => CodeRef,
165             builder => sub {
166 2     2   21 my ($self) = @_;
167 2 100       8 if ($self->simple) {
168             return sub {
169 1     1   16 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         20 }
174             else {
175 1     1   20 return sub { sprintf('<link rel="preload" as="style" href="%s" onload="this.onload=null;this.rel=\'stylesheet\'">', $_[0]) };
  1         16  
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   30 my ($level, $message) = @_;
194 8 100       111 croak $message if ($level eq 'error');
195 2         31 carp $message;
196 26     26   686 };
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 3204 my ($self) = @_;
210              
211 5         123 my $files = $self->css_files;
212              
213 3 100       174 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 2729 my ($self, $name, $file) = @_;
222 28 50 66     113 $file //= $self->_get_file($name) or return;
223 28 100       75 if (defined $file->[PATH]) {
224 20         71 my $href = $self->url_base_path . $file->[NAME];
225 20 100       75 $href .= '?' . $self->asset_id if $self->has_asset_id;
226 20 100 66     367 if ($self->use_cdn_links && $self->has_cdn_links) {
227 1   33     33 return $self->cdn_links->{$name} // $href;
228             }
229 19         286 return $href;
230             }
231             else {
232 8         27 return $file->[NAME];
233             }
234             }
235              
236              
237             sub link_html {
238 21     21 1 3447 my ( $self, $name, $file ) = @_;
239 21 100       58 if (my $href = $self->href( $name, $file )) {
240 17         59 return $self->link_template->($href);
241             }
242             else {
243 4         15 return "";
244             }
245             }
246              
247              
248             sub inline_html {
249 9     9 1 865 my ( $self, $name, $file ) = @_;
250 9 50 66     40 $file //= $self->_get_file($name) or return;
251 9 100       39 if (my $path = $file->[PATH]) {
252 8 100       22 if ($file->[SIZE]) {
253 6         24 return "<style>" . $file->[PATH]->slurp_raw . "</style>";
254             }
255 2         10 $self->log->( warning => "empty file '$path'" );
256 2         934 return "";
257             }
258             else {
259 1         8 $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 2517 my ($self, @names ) = @_;
267 5         12 my $buffer = "";
268 5         21 foreach my $name (uniqstr @names) {
269 8 50       187 my $file = $self->_get_file($name) or next;
270 8 100 100     61 if ( $file->[PATH] && ($file->[SIZE] <= $self->inline_max)) {
271 3         9 $buffer .= $self->inline_html($name, $file);
272             }
273             else {
274 5         14 $buffer .= $self->link_html($name, $file);
275             }
276             }
277 5         186 return $buffer;
278             }
279              
280              
281             sub deferred_link_html {
282 6     6 1 2483 my ($self, @names) = @_;
283 6         13 my $buffer = "";
284 6         10 my @deferred;
285 6         24 for my $name (uniqstr @names) {
286 7 50       17 my $file = $self->_get_file($name) or next;
287 7 100 100     58 if ($file->[PATH] && $file->[SIZE] <= $self->inline_max) {
    100          
288 1         7 $buffer .= $self->inline_html($name, $file);
289             }
290             elsif ($self->defer_css) {
291 2         8 my $href = $self->href($name, $file);
292 2         7 push @deferred, $href;
293 2         34 $buffer .= $self->preload_template->($href);
294             }
295             else {
296 4         12 $buffer .= $self->link_html($name, $file);
297             }
298             }
299              
300 6 100       223 if (@deferred) {
301              
302             $buffer .= "<noscript>" .
303 2 100       36 join("", map { $self->link_template->($_) } @deferred ) .
  1         36  
304             "</noscript>" if $self->include_noscript;
305              
306 2 100       36 $buffer .= "<script>" .
307             $self->preload_script->slurp_raw .
308             "</script>" unless $self->simple;
309              
310             }
311              
312 6         201 return $buffer;
313             }
314              
315             sub _get_file {
316 37     37   76 my ($self, $name) = @_;
317 37 50       107 unless (defined $name) {
318 0         0 $self->log->( error => "missing name" );
319 0         0 return;
320             }
321 37 50       829 if (my $file = $self->css_files->{$name}) {
322 37         1098 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.0
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             =head1 METHODS
598              
599             =head2 check
600              
601             This method instantiates lazy attributes and performs some minimal
602             checks on the data. (This should be called instead of L</css_files>.)
603              
604             It will throw an error or return false (depending on L</log>) if there
605             is something wrong.
606              
607             This was added in v0.3.0.
608              
609             =head2 href
610              
611             my $href = $css->href( $alias );
612              
613             This returns this URL for an alias.
614              
615             =head2 link_html
616              
617             my $html = $css->link_html( $alias );
618              
619             This returns the link HTML markup for the stylesheet referred to by
620             C<$alias>.
621              
622             =head2 inline_html
623              
624             my $html = $css->inline_html( $alias );
625              
626             This returns an embedded stylesheet referred to by C<$alias>.
627              
628             =head2 link_or_inline_html
629              
630             my $html = $css->link_or_inline_html( @aliases );
631              
632             This returns either the link HTML markup, or the embedded stylesheet,
633             if the file size is not greater than L</inline_max>.
634              
635             Note that a stylesheet will be inlined, even if there is are
636             L</cdn_links>.
637              
638             =head2 deferred_link_html
639              
640             my $html = $css->deferred_link_html( @aliases );
641              
642             This returns the HTML markup for the stylesheets specified by
643             L</aliases>, as appropriate for each stylesheet.
644              
645             If the stylesheets are not greater than L</inline_max>, then it will
646             embed them. Otherwise it will return the appropriate markup,
647             depending on L</defer_css>.
648              
649             =for Pod::Coverage PATH NAME SIZE
650              
651             =head1 KNOWN ISSUES
652              
653             =head2 Content-Security-Policy (CSP)
654              
655             If a web site configures a
656             L<Content-Security-Policy|https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP>
657             setting to disable all inlined JavaScript, then the JavaScript shim will
658             not work.
659              
660             =head2 XHTML Support
661              
662             This module is written for HTML5.
663              
664             It does not support XHTML self-closing elements or embedding styles
665             and scripts in CDATA sections.
666              
667             =head2 Encoding
668              
669             All files are embedded as raw files.
670              
671             No URL encoding is done on the HTML links or L</asset_id>.
672              
673             =head2 It's spelled "Deferrable"
674              
675             It's also spelled "Deferable".
676              
677             =head1 SOURCE
678              
679             The development version is on github at L<https://github.com/robrwo/HTML-DeferableCSS>
680             and may be cloned from L<git://github.com/robrwo/HTML-DeferableCSS.git>
681              
682             =head1 BUGS
683              
684             Please report any bugs or feature requests on the bugtracker website
685             L<https://github.com/robrwo/HTML-DeferableCSS/issues>
686              
687             When submitting a bug or request, please include a test-file or a
688             patch to an existing test-file that illustrates the bug or desired
689             feature.
690              
691             Please report any bugs in F<cssrelpreload.js> to
692             L<https://github.com/filamentgroup/loadCSS/issues>.
693              
694             =head1 AUTHOR
695              
696             Robert Rothenberg <rrwo@cpan.org>
697              
698             This module was developed from work for Science Photo Library
699             L<https://www.sciencephoto.com>.
700              
701             F<reset.css> comes from L<http://meyerweb.com/eric/tools/css/reset/>.
702              
703             F<cssrelpreload.js> comes from L<https://github.com/filamentgroup/loadCSS/>.
704              
705             =head1 COPYRIGHT AND LICENSE
706              
707             This software is Copyright (c) 2020 by Robert Rothenberg.
708              
709             This is free software, licensed under:
710              
711             The MIT (X11) License
712              
713             =cut