+# User-generated content (UGC) may have excessively long lines
+# and screw up rendering on some browsers, so we use pre-wrap.
+#
+# We also force everything to the same scaled font-size because GUI
+# browsers (tested both Firefox and surf (webkit)) uses a larger font
+# for the Search <form> element than the rest of the page. Font size
+# uniformity is important to people who rely on gigantic fonts.
+# Finally, we use monospace to ensure the Search field and button
+# has the same size and spacing as everything else which is
+# <pre>-formatted anyways.
+our $STYLE = 'pre{white-space:pre-wrap}*{font-size:100%;font-family:monospace}';
+
+sub stylesheets_prepare ($$) {
+ my ($self, $upfx) = @_;
+ my $mini = eval {
+ require CSS::Minifier;
+ sub { CSS::Minifier::minify(input => $_[0]) };
+ } || eval {
+ require CSS::Minifier::XS;
+ sub { CSS::Minifier::XS::minify($_[0]) };
+ } || sub { $_[0] };
+
+ my $css_map = {};
+ my $stylesheets = $self->{pi_cfg}->{css} || [];
+ my $links = [];
+ my $inline_ok = 1;
+
+ foreach my $s (@$stylesheets) {
+ my $attr = {};
+ local $_ = $s;
+ foreach my $k (qw(media title href)) {
+ if (s/\s*$k='([^']+)'// || s/\s*$k=(\S+)//) {
+ $attr->{$k} = $1;
+ }
+ }
+
+ if (defined $attr->{href}) {
+ $inline_ok = 0;
+ } else {
+ my $fn = $_;
+ my ($key) = (m!([^/]+?)(?:\.css)?\z!i);
+ if ($key !~ /\A[a-zA-Z0-9_\-\.]+\z/) {
+ warn "ignoring $fn, non-ASCII word character\n";
+ next;
+ }
+ open(my $fh, '<', $fn) or do {
+ warn "failed to open $fn: $!\n";
+ next;
+ };
+ my $ctime = 0;
+ my $local = do { local $/; <$fh> };
+ if ($local =~ /\S/) {
+ $ctime = sprintf('%x',(stat($fh))[10]);
+ $local = $mini->($local);
+ }
+
+ # do not let BOFHs override userContent.css:
+ if ($local =~ /!\s*important\b/i) {
+ warn "ignoring $fn since it uses `!important'\n";
+ next;
+ }
+
+ $css_map->{$key} = $local;
+ $attr->{href} = "$upfx$key.css?$ctime";
+ if (defined($attr->{title})) {
+ $inline_ok = 0;
+ } elsif (($attr->{media}||'screen') eq 'screen') {
+ $attr->{-inline} = $local;
+ }
+ }
+ push @$links, $attr;
+ }
+
+ my $buf = "<style>$STYLE";
+ if ($inline_ok) {
+ my @ext; # for media=print and whatnot
+ foreach my $attr (@$links) {
+ if (defined(my $str = delete $attr->{-inline})) {
+ $buf .= $str;
+ } else {
+ push @ext, $attr;
+ }
+ }
+ $links = \@ext;
+ }
+ $buf .= '</style>';
+
+ if (@$links) {
+ foreach my $attr (@$links) {
+ delete $attr->{-inline};
+ $buf .= "<link\ntype=text/css\nrel=stylesheet";
+ while (my ($k, $v) = each %$attr) {
+ $v = qq{"$v"} if $v =~ /[\s=]/;
+ $buf .= qq{\n$k=$v};
+ }
+ $buf .= ' />';
+ }
+ $self->{"-style-$upfx"} = $buf;
+ } else {
+ $self->{-style_inline} = $buf;
+ }
+ $self->{-css_map} = $css_map;
+}
+
+# returns an HTML fragment with <style> or <link> tags in them
+# Called by WwwStream by nearly every HTML page
+sub style {
+ my ($self, $upfx) = @_;
+ $self->{-style_inline} || $self->{"-style-$upfx"} || do {
+ stylesheets_prepare($self, $upfx);
+ $self->{-style_inline} || $self->{"-style-$upfx"}
+ };
+}
+
+# /$INBOX/$KEY.css and /+/$KEY.css endpoints
+# CSS is configured globally for all inboxes, but we access them on
+# a per-inbox basis. This allows administrators to setup per-inbox
+# static routes to intercept the request before it hits PSGI
+# inbox == undef => top-level WwwListing
+sub get_css ($$$) {
+ my ($ctx, $inbox, $key) = @_;
+ my $r404 = defined($inbox) ? invalid_inbox($ctx, $inbox) : undef;
+ return $r404 if $r404;
+ my $self = $ctx->{www};
+ my $css_map = $self->{-css_map} ||
+ stylesheets_prepare($self, defined($inbox) ? '' : '+/');
+ my $css = $css_map->{$key};
+ if (!defined($css) && defined($inbox) && $key eq 'userContent') {
+ my $env = $ctx->{env};
+ $css = PublicInbox::UserContent::sample($ctx->{ibx}, $env);
+ }
+ defined $css or return r404();
+ my $h = [ 'Content-Length', length($css), 'Content-Type', 'text/css' ];
+ PublicInbox::GitHTTPBackend::cache_one_year($h);
+ [ 200, $h, [ $css ] ];
+}
+
+sub get_description {
+ my ($ctx, $inbox) = @_;
+ invalid_inbox($ctx, $inbox) || do {
+ my $d = $ctx->{ibx}->description . "\n";
+ utf8::encode($d);
+ [ 200, [ 'Content-Length', length($d),
+ 'Content-Type', 'text/plain' ], [ $d ] ];
+ };
+}
+
+sub event_step { # called via requeue
+ my ($self) = @_;
+ # gzf = PublicInbox::GzipFilter == $ctx
+ my $gzf = shift(@{$self->{-low_prio_q}}) // return;
+ PublicInbox::DS::requeue($self) if scalar(@{$self->{-low_prio_q}});
+ my $http = $gzf->{env}->{'psgix.io'}; # PublicInbox::HTTP
+ $http->next_step($gzf->can('async_next'));
+}
+