subtitle handling, prettified dates. bugfixes and better errors
[scpubgit/SCS.git] / lib / SCSite.pm
1 package SCSite;
2
3 use IO::All;
4 use SCSite::PageSet;
5 use Web::Simple;
6
7 has pages => (is => 'lazy');
8
9 has filters => (is => 'lazy');
10
11 has _layout_zoom => (is => 'lazy');
12
13 has _feed_configs => (is => 'lazy');
14
15 has _feed_generator => (
16   is => 'lazy',
17   handles => { _feed_http_response => 'feed_http_response' },
18 );
19
20 sub default_config {
21   (
22     pages_dir => 'share/pages',
23     template_dir => 'share/templates',
24     static_dir => 'share/static',
25     feed_id_prefix => 'http://shadow.cat',
26   )
27 }
28
29 sub _build_pages {
30   my ($self) = @_;
31   SCSite::PageSet->new(base_dir => io->dir($self->config->{pages_dir}))
32 }
33
34 sub _build_filters {
35   my ($self) = @_;
36   require SCSite::SubListFilter;
37   require SCSite::SidebarFilter;
38   +{
39     map +($_ => "SCSite::${_}Filter"->new_from_site($self)),
40       qw(SubList Sidebar)
41   }
42 }
43
44 sub _build__feed_configs {
45   my $f = +{
46     'blog' => {
47       title => 'All Shadowcat blogs',
48       entries => { min_depth => 2, max_depth => 3 },
49     },
50     'blog/matt-s-trout' => {
51       title => q{Matt S Trout (mst)'s blog},
52       entries => { at_depth => 1 },
53     },
54     'blog/mark-keating' => {
55       title => q{Mark Keating (mdk)'s blog},
56       entries => { min_depth => 1, max_depth => 2 },
57     },
58     'news' => {
59       title => 'Shadowcat News',
60       entries => { at_depth => 4 },
61     },
62   };
63   $f->{$_}{base} ||= $_ for keys %$f;
64   $f;
65 }
66
67 sub _build__feed_generator {
68   my ($self) = @_;
69   require SCSite::FeedGenerator;
70   SCSite::FeedGenerator->new(
71     pages => $self->pages,
72     id_prefix => $self->config->{feed_id_prefix},
73   );
74 }
75
76 sub dispatch_request {
77   my $self = shift;
78   sub (/feed/**/) {
79     if (my $conf = $self->_feed_configs->{$_[1]}) {
80       $self->_feed_http_response(200 => $conf);
81     }
82   },
83   sub (/) {
84     $self->_page_http_response(200 => $self->_find_page('index'));
85   },
86   sub (/**) {
87     [ 302, [ 'Location' => "/$_[1]/" ], [] ]
88   },
89   sub (/**/) {
90     my ($code, $page) = map {
91       $_ ? (200, $_) : (404, $self->_error_page(404))
92     } $self->_find_page($_[1]);
93     $self->_page_http_response($code => $page);
94   }
95 }
96
97 sub _error_page {
98   my ($self, $code) = @_;
99   $self->_find_page("error_${code}");
100 }
101
102 sub _find_page {
103   my ($self, $path) = @_;
104   $self->pages->get({ path => $path });
105 }
106
107 sub _http_response {
108   my ($self, $code, $type, $content) = @_;
109   [ $code, [ 'Content-type' => $type ], [ $content ] ];
110 }
111
112 sub _page_http_response {
113   my ($self, $code, $page) = @_;
114   [ $code, [ 'Content-type' => 'text/html' ], $self->_render_page($page) ];
115 }
116
117 sub _render_page {
118   my ($self, $page) = @_;
119   my $zoom = $self->_layout_zoom;
120   my %filters = %{$self->filters};
121   $zoom->select('.page.title')->replace_content($page->title)
122        ->select('.page.subtitle')->${\sub {
123            $page->subtitle
124              ? $_[0]->replace_content($page->subtitle)
125              : $_[0]->replace('')
126          }}
127        ->select('.page.published_at')->replace_content($page->published_at)
128        ->select('meta[name=description]')
129          ->set_attribute(content => $page->description)
130        ->select('meta[name=keywords]')
131          ->set_attribute(content => $page->keywords)
132        ->select('meta[name=created]')
133          ->set_attribute(content => $page->created)
134        ->select('.page.body')->replace_content(\$page->body)
135        ->apply(sub {
136            foreach my $fname (sort keys %filters) {
137              my $cb = $filters{$fname}->callback_for($page);
138              $_ = $_->select(".${fname}")->collect({
139                         filter => $cb, passthrough => 1
140                       });
141            }
142            $_
143          })
144        ->to_fh
145 }
146
147 sub _build__layout_zoom {
148   my ($self) = @_;
149   HTML::Zoom->from_file(
150     io->dir($self->config->{template_dir})->catfile('layout.html')
151   )->memoize;
152 }
153
154 sub run_if_script {
155   return $_[0]->to_psgi_app if caller(1);
156   my $class = shift;
157   my @config_keys = keys %{{$class->default_config}};
158   require Getopt::Long;
159   my %config;
160   Getopt::Long::GetOptions(
161     map +("$_=s" => \$config{$_}), @config_keys
162   );
163   delete $config{$_} for grep !defined($config{$_}), keys %config;
164   my $new = $class->new(config => \%config);
165   $new->run(@_)
166 }
167
168 around _run_cli => sub {
169   my ($orig, $self) = (shift, shift);
170   if (@_ >= 2 and $_[0] eq 'dev' and $_[1] eq 'server') {
171     require SCSite::DevMode;
172     Moo::Role->apply_roles_to_object($self, 'SCSite::DevMode');
173     return $self->_run_dev_server(@_[2..$#_]);
174   }
175   return $self->$orig(@_);
176 };
177
178 __PACKAGE__->run_if_script;