Mojolicious-Plugin-Foil/lib/Mojolicious/Plugin/Foil.pm
package Mojolicious::Plugin::Foil;
$Mojolicious::Plugin::Foil::VERSION = '0.006';
# ABSTRACT: Mojolicious Plugin for CSS theming
use Mojo::Base 'Mojolicious::Plugin';
use common::sense;
use JSON::MaybeXS;
use Path::Tiny;
use File::ShareDir;
use File::Slurper 'read_binary';
use Image::Size;
use HTML::LinkList;
use Config::Context;
sub register {
my ( $self, $app, $conf ) = @_;
# Append class
push @{$app->renderer->classes}, __PACKAGE__;
push @{$app->static->classes}, __PACKAGE__;
# Append directories
# Find the Foil shared directory
# It could be relative to the app home,
# it could be relative to this current file,
# it could be in a FileShared location.
my $app_home = path($app->home);
my $foilshared = $app_home->child("foil");
if (!-d $foilshared)
{
my $mydir = path(__FILE__)->parent; # lib/Mojolicious/Plugin
my $top = $mydir->parent->parent->parent;
$foilshared = $top->child("foil");
if (!-d $foilshared)
{
# use File::ShareDir with the distribution name
my $dist = __PACKAGE__;
$dist =~ s/::/-/g;
my $dist_dir = path(File::ShareDir::dist_dir($dist));
$foilshared = $dist_dir;
}
}
push @{$app->static->paths}, $foilshared;
$self->{foilshared} = $foilshared;
$self->_get_themes($app);
$self->_setup_concon($app,$conf);
$app->helper( 'foil_navbar' => sub {
my $c = shift;
my %args = @_;
return $self->_make_navbar($c,%args);
} );
$app->helper( 'foil_breadcrumb' => sub {
my $c = shift;
my %args = @_;
return $self->_make_breadcrumb($c,%args);
} );
$app->helper( 'foil_referrer' => sub {
my $c = shift;
my %args = @_;
return $self->_make_referrer($c,%args);
} );
$app->helper( 'foil_logo' => sub {
my $c = shift;
my %args = @_;
return $self->_make_logo_css($c,%args);
} );
$app->helper( 'foil_theme_id' => sub {
my $c = shift;
my %args = @_;
return $self->_get_theme_id($c,%args);
} );
$app->helper( 'foil_theme_selector' => sub {
my $c = shift;
my %args = @_;
return $self->_make_theme_selector($c,%args);
} );
# add routes for setting the theme
# and for delivering the logo
$self->{set_route} = '/foil/set';
$self->{logo_route} = '/foil/logo/:logo';
$app->routes->get($self->{set_route} => sub {
my $c = shift;
$self->_set_theme($c);
});
if (exists $conf->{add_prefixes}
and defined $conf->{add_prefixes})
{
my @prefixes = @{$conf->{add_prefixes}};
$self->{prefixes} = [];
foreach my $rp (@prefixes)
{
$rp =~ s!/$!!; # remove trailing slash, if any
push @{$self->{prefixes}}, $rp;
$app->routes->get(${rp} . $self->{set_route} => sub {
my $c = shift;
$self->_set_theme($c);
});
}
} # prefixes
} # register
sub _get_themes {
my $self = shift;
my $app = shift;
my $theme_file = $self->{foilshared}->child("styles/themes/themes.json");
if (!-f $theme_file)
{
die "'$theme_file' not found";
}
my $theme_txt = path($theme_file)->slurp;
$self->{themes} = decode_json($theme_txt);
if (!defined $self->{themes})
{
die "'$theme_file' not parsed";
}
if (!defined $self->{themes}->{themes})
{
die "'$theme_file' not themes->themes";
}
if (ref $self->{themes}->{themes} ne 'HASH')
{
die "'$theme_file' themes->themes not HASH " . ref $self->{themes}->{themes};
}
} # _get_themes
sub _setup_concon {
my $self = shift;
my $app = shift;
if (!exists $app->config->{foil}->{context})
{
return;
}
my %cc_opts = (
driver => 'ConfigGeneral',
match_sections => [
{
name => 'Page',
section_type => 'page',
match_type => 'path',
},
{
name => 'PageMatch',
section_type => 'page',
match_type => 'regex',
},
{
name => 'File',
section_type => 'file',
match_type => 'path',
},
{
name => 'FileMatch',
section_type => 'file',
match_type => 'regex',
},
],
);
$self->{concon} = Config::Context->new
(
string => $app->config->{foil}->{context},
%cc_opts
);
} # _setup_concon
sub _get_prefix {
my $self = shift;
my $c = shift;
my %args = @_;
my $route_prefix = '';
my $curr_url = $c->url_for('current');
# check if this matches one of the extra routes instead
# Note that we remember the prefix when we make the extra route
if (exists $self->{prefixes}
and defined $self->{prefixes})
{
foreach my $prefix (@{$self->{prefixes}})
{
if ($curr_url =~ /^\Q$prefix\E\//)
{
$route_prefix = $prefix;
last;
}
}
}
return $route_prefix;
} # _get_prefix
sub _make_theme_selector {
my $self = shift;
my $c = shift;
my %args = @_;
my $curr_theme = $self->_get_theme_id($c,%args);
my $curr_url = $c->url_for('current');
my $opt_url = $c->url_for($self->{set_route});
my $prefix = $self->_get_prefix($c);
if ($prefix)
{
$opt_url = $c->url_for(${prefix} . $self->{set_route});
}
my @out = ();
push @out, "<div class='themes'>";
push @out, "<form action='$opt_url'>";
push @out, '<input type="submit" value="Select theme"/>';
push @out, '<select name="theme">';
my @themes = sort keys %{$self->{themes}->{themes}};
for (my $i=0; $i < @themes; $i++)
{
my $th = $themes[$i];
if ($th eq $curr_theme)
{
push @out, "<option value='$th' selected>$th</option>";
}
else
{
push @out, "<option value='$th'>$th</option>";
}
}
push @out, '</select>';
push @out, '</form>';
push @out, '</div>';
my $out = join("\n", @out);
return $out;
} # _make_theme_selector
sub _make_navbar {
my $self = shift;
my $c = shift;
my %args = @_;
my $rhost = $c->req->headers->host;
my $foilconf = $self->_get_foilconf($c);
my $nb_host = $rhost;
if (exists $foilconf->{navbar_host})
{
$nb_host = $foilconf->{navbar_host};
}
my @out = ();
push @out, '<nav>';
push @out, '<ul>';
# we start always with Home
push @out, "<li><a href='http://$nb_host/'>Home</a></li>";
if (exists $foilconf->{navbar_links})
{
foreach my $link (@{$foilconf->{navbar_links}})
{
my $name = $link;
if ($link =~ m{(\w+)/?$})
{
$name = ucfirst(lc($1));
}
if ($link =~ /^http/)
{
push @out, "<li><a href='${link}'>$name</a></li>";
}
else
{
push @out, "<li><a href='http://${nb_host}${link}'>$name</a></li>";
}
}
}
push @out, '</ul>';
push @out, '</nav>';
my $out = join("\n", @out);
return $out;
} # _make_navbar
sub _make_breadcrumb {
my $self = shift;
my $c = shift;
my %args = @_;
my $breadcrumb = '';
my $url = $c->req->url;
if (defined $url and $url)
{
$breadcrumb = HTML::LinkList::breadcrumb_trail(current_url=>$url,
links_head=>'', links_foot=>'');
}
else
{
my $rhost = $c->req->headers->host;
my $foilconf = $self->_get_foilconf($c);
my $hostname = $rhost;
if (exists $foilconf->{name})
{
$hostname = $foilconf->{name};
}
$breadcrumb = "<b>$hostname</b> <a href='/'>Home</a>";
}
return $breadcrumb;
} # _make_breadcrumb
sub _make_referrer {
my $self = shift;
my $c = shift;
my %args = @_;
my $rhost = $c->req->headers->host;
my $hostname = $rhost;
my $foilconf = $self->_get_foilconf($c);
if (exists $foilconf->{name})
{
$hostname = $foilconf->{name};
}
my $referrer = "<b>$hostname</b>";
my $url = $c->req->headers->referrer;
if (defined $url)
{
$referrer .= " <a href='$url'>$url</a>";
}
return $referrer;
} # _make_referrer
sub _make_logo_css {
my $self = shift;
my $c = shift;
my %args = @_;
my $curr_theme = $self->_get_theme_id($c,%args);
my $logo_type = $self->{themes}->{themes}->{$curr_theme};
my $logo_url = $c->url_for("/styles/themes/foil_${logo_type}.png");
my $rhost = $c->req->headers->host;
my $foilconf = $self->_get_foilconf($c);
if ($foilconf->{"${logo_type}_url"})
{
$logo_url = $foilconf->{"${logo_type}_url"};
}
my $logo_css =<<"EOT";
<div class="logo"><a href="/"><img src="$logo_url" alt="Home"/></a></div>
EOT
return $logo_css;
} # _make_logo_css
sub _get_theme_id {
my $self = shift;
my $c = shift;
my %args = @_;
my $rhost = $c->req->headers->host;
my $theme = $c->session("theme_${rhost}");
my $path = $c->req->url->to_abs->path;
if (!$theme and defined $self->{concon}) # try context theme
{
my $cc = $self->{concon}->context(page=>$path);
if (exists $cc->{theme})
{
$theme = $cc->{theme};
}
}
if (!$theme) # try default theme
{
my $rhost = $c->req->headers->host;
my $foilconf = $self->_get_foilconf($c);
if ($foilconf->{default_theme})
{
$theme = $foilconf->{default_theme};
}
}
$theme = 'silver' if !$theme; # fall back on silver
return $theme;
} # _get_theme_id
sub _set_theme {
my $self = shift;
my $c = shift;
my $rhost = $c->req->headers->host;
my $theme = $c->param('theme');
if ($theme)
{
$c->session->{"theme_${rhost}"} = $theme;
}
my $referrer = $c->req->headers->referrer;
my $out =<<"EOT";
<p>Current theme is "$theme".</p>
<p>Back to: <a href='$referrer'>$referrer</a></p>
EOT
$c->render(template=>'foil/settings',
foil_settings=>$out);
} # _set_theme
sub _get_foilconf {
my $self = shift;
my $c = shift;
my $rhost = $c->req->headers->host;
my $foilconf;
if (exists $c->config->{foil}->{$rhost})
{
$foilconf = $c->config->{foil}->{$rhost};
}
else
{
$foilconf = $c->config->{foil}->{default};
}
return $foilconf;
} # _get_foilconf
1; # End of Mojolicious::Plugin::Foil
=pod
=encoding UTF-8
=head1 NAME
Mojolicious::Plugin::Foil - Mojolicious Plugin for CSS theming
=head1 VERSION
version 0.006
=head1 SYNOPSIS
use Mojolicious::Plugin::Foil;
=head1 DESCRIPTION
Pretty themes; putting them in the application
instead of in javascript; it's faster this way.
Also other looks like breadcrumbs and other header stuff.
=head1 NAME
Mojolicious::Plugin::Foil - looks for app
=head1 VERSION
version 0.006
=head1 REGISTER
=head1 Helper Functions
These are functions which are NOT exported by this plugin.
=head2 _get_themes
Get the list of themes from the themes.json file.
=head2 _setup_concon
Set up the Config::Context stuff.
=head2 _get_prefix
Get the "prefix" part of the current route, if it has one
=head2 _make_theme_selector
For selecting themes.
=head2 _make_navbar
Top-level navigation.
The difficulty with this is that using a reverse-proxy means that
all relative-ish URLs will be rewritten to be relative to this app.
So we need to take account of the host the request is coming from.
Absolute full URLs shouldn't be re-written.
=head2 _make_breadcrumb
Make breadcrumb showing the current page.
=head2 _make_referrer
Make link showing the previous page.
=head2 _make_logo_css
Make logo-link which points to the Home page.
=head2 _get_theme_id
Get the ID of the current theme.
=head2 _set_theme
For remembering themes.
=head2 _get_foilconf
Get the appropriate config for this rhost.
=head1 AUTHOR
Kathryn Andersen <perlkat@katspace.org>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2016 by Kathryn Andersen.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut
__DATA__
@@ foil/settings.html.ep
% layout 'foil';
<h1>Theme</h1>
<div>
<%== $foil_settings %>
</div>
@@ foil/header.html.ep
<div id="header_top">
<%== foil_logo %>
<%== foil_navbar %>
</div> <!-- /header_top -->
<div class="breadcrumb"><%== foil_breadcrumb %></div>
@@ foil/common_css.html.ep
<link rel="stylesheet" href="<%= url_for('/styles') %>/layout/layout_flex.css" type="text/css" />
<link rel="stylesheet" type="text/css" title="default" href="<%= url_for('/styles') %>/themes/theme_<%= foil_theme_id %>.css"/>
@@ layouts/foil.html.ep
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
%= include 'foil/common_css'
%= content 'head_extra'
</head>
<body>
<div id="page-wrap">
<header>
%= include 'foil/header'
</header>
<div id="inner">
<div id="main-wrap">
<main>
<%== content %>
</main>
</div> <!-- /main-wrap -->
<div class="verso-wrap">
<div class="side">
%= content 'verso'
</div> <!-- /side -->
</div> <!-- /verso-wrap -->
<div class="recto-wrap">
<div class="side">
%= content 'recto'
</div> <!-- /side -->
</div> <!-- /recto-wrap -->
</div> <!-- /inner -->
<div id="footer-wrap">
<footer>
%= content 'footer'
</footer>
</div> <!-- /footer-wrap -->
</div> <!-- /page-wrap -->
</body>
</html>