+package Plack::Middleware::BasicStyle;
+
+use 5.014000;
+use strict;
+use warnings;
+
+use parent qw/Plack::Middleware/;
+
+use HTML::Parser;
+use Plack::Request;
+use Plack::Util;
+use Plack::Util::Accessor qw/style any_content_type even_if_styled use_link_header/;
+
+our $VERSION = '0.001';
+our $DEFAULT_STYLE = <<EOF =~ y/\n\t //rd;
+<style>
+ body {
+ margin:40px auto;
+ max-width: 650px;
+ line-height: 1.6;
+ font-size:18px;
+ color:#444;
+ padding:0 10px
+ }
+
+ h1,h2,h3 {
+ line-height:1.2
+ }
+</style>
+EOF
+
+sub prepare_app {
+ my ($self) = @_;
+ $self->{link_header} =
+ sprintf '<%s>; rel=stylesheet', $self->use_link_header
+ if $self->use_link_header;
+ $self->style($self->style // $DEFAULT_STYLE);
+}
+
+sub _content_type_ok {
+ my ($self, $hdrs) = @_;
+ return 1 if $self->any_content_type;
+ my $content_type =
+ Plack::Util::header_get($hdrs, 'Content-Type');
+ return '' unless $content_type;
+ $content_type =~ m,text/html,i;
+}
+
+sub call {
+ my ($self, $env) = @_;
+ if ($self->use_link_header) {
+ my $req = Plack::Request->new($env);
+ if (lc $req->path eq lc $self->use_link_header) {
+ my $days30 = 30 * 86400;
+ my @hdrs = (
+ 'Content-Length' => length $self->style,
+ 'Content-Type' => 'text/css',
+ 'Cache-Control' => "max-age=$days30",
+ );
+ return [200, \@hdrs, [$self->style]]
+ }
+ }
+
+ my $res = $self->app->($env);
+ if (ref $res ne 'ARRAY'
+ || @$res < 3
+ || ref $res->[2] ne 'ARRAY' ) {
+ $res
+ } elsif (!$self->_content_type_ok($res->[1])) {
+ $res
+ } else {
+ my ($styled, $html_end, $head_end, $doctype_end);
+ my $parser_callback = sub {
+ my ($tagname, $offset_end, $attr) = @_;
+ $html_end //= $offset_end if $tagname eq 'html';
+ $head_end //= $offset_end if $tagname eq 'head';
+ $doctype_end //= $offset_end if $tagname eq 'doctype';
+ $styled = 1 if $tagname eq 'style';
+ $styled = 1 if $tagname eq 'link'
+ && ($attr->{rel} // '') =~ /stylesheet/i;
+ };
+
+ my $p = HTML::Parser->new(api_version => 3);
+ $p->report_tags(qw/style link html head/);
+ $p->handler(start => $parser_callback, 'tagname,offset_end,attr');
+ $p->handler(declaration => $parser_callback, 'tagname,offset_end,attr');
+ $p->parse($_) for @{$res->[2]};
+ $p->eof;
+
+ return $res if $styled && !$self->even_if_styled;
+
+ if ($self->use_link_header) {
+ push @{$res->[1]}, 'Link', $self->{link_header};
+ } else {
+ # If there's a <head>, put the style right after it
+ # Otherwise, if there's a <html>, put the style right after it
+ # Otherwise, if there's a <!DOCTYPE ...>, put the style right after it
+ # Otherwise, put the style at the very beginning of the body
+ if ($head_end || $html_end || $doctype_end) {
+ my $body = join '', @{$res->[2]};
+ my $pos = $head_end // $html_end // $doctype_end;
+ substr $body, $pos, 0, $self->style;
+ $res->[2] = [$body]
+ } else {
+ unshift @{$res->[2]}, $self->style
+ }
+ }
+
+ $res
+ }
+}
+
+1;
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+Plack::Middleware::BasicStyle - Add a basic <style> element to pages that don't have one
+
+=head1 SYNOPSIS
+
+ # Basic usage (all default options)
+ use Plack::Builder;
+ builder {
+ enable 'BasicStyle';
+ ...
+ }
+
+ # Default options set explicitly
+ use Plack::Builder;
+ builder {
+ enable 'BasicStyle',
+ style => $Plack::Middleware::BasicStyle::DEFAULT_STYLE,
+ any_content_type => '',
+ even_if_styled => '',
+ use_link_header => '';
+ ...
+ }
+
+ # Custom options
+ use Plack::Builder;
+ builder {
+ enable 'BasicStyle',
+ style => '<style>body { background-color: #ddd }</style>',
+ any_content_type => 1,
+ even_if_styled => 1,
+ use_link_header => '/basic-style.css';
+ ...
+ }
+
+=head1 DESCRIPTION
+
+Plack::Middleware::BasicStyle is a Plack middleware that adds a basic
+<style> element to HTML pages that do not have a stylesheet.
+
+The default style, taken from
+L<http://bettermotherfuckingwebsite.com>, is (before minification):
+
+ <style>
+ body {
+ margin:40px auto;
+ max-width: 650px;
+ line-height: 1.6;
+ font-size:18px;
+ color:#444;
+ padding:0 10px
+ }
+
+ h1,h2,h3 {
+ line-height:1.2
+ }
+ </style>
+
+The middleware takes the following arguments:
+
+=over
+
+=item B<style>
+
+This is the HTML fragment that will be added to unstyled pages.
+
+It defaults to the value of
+C<< $Plack::Middleware::BasicStyle::DEFAULT_STYLE >>.
+
+=item B<any_content_type>
+
+If true, don't check whether Content-Type contains C<text/html>.
+
+If false (default), passes the response through unchanged if the
+Content-Type header is unset or does not contain the case-insensitive
+substring C<text/html>.
+
+=item B<even_if_styled>
+
+If true, don't check whether the response already includes a <style>
+or <link ... rel="stylesheet"> element.
+
+If false (default), passes the response through unchanged if the
+response includes a <style> or <link ... rel="stylesheet"> element.
+
+=item B<use_link_header>
+
+If false or unset (default), the given HTML fragment will be added
+right after the <head> start tag (if this exists), right after the
+<html> start tag (if this exists but <head> doesn't), or at the
+beginning of the document (if neither <html> nor <head> exists).
+
+If set, its value is interpreted as an URL path. The body of the
+response will not be modified, instead a C<Link:> HTTP header will be
+added to unstyled pages. Additionally, the middleware will intercept
+requests to that exact URL path and return the style (with status 200,
+a Content-Type of C<text/css>, a correct Content-Length header, and a
+Cache-Control header instructing the browser to cache the style for 30
+days).
+
+Setting this makes the module more resilient to bugs and more
+efficient at the cost of asking the client to make an extra request.
+Therefore setting this argument is B<recommended>.
+
+=back
+
+=head1 CAVEATS
+
+This middleware only works with simple (non-streaming) responses,
+where the body is an arrayref.
+
+In other words, responses where the body is an IO::Handle, or
+streaming/delayed responses are NOT supported and will be passed
+through unchanged by this middleware.
+
+=head1 SEE ALSO
+
+L<http://bettermotherfuckingwebsite.com>
+
+=head1 AUTHOR
+
+Marius Gavrilescu, E<lt>marius@ieval.roE<gt>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (C) 2016 by Marius Gavrilescu
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.24.0 or,
+at your option, any later version of Perl 5 you may have available.
+
+
+=cut