Преглед на файлове

Give every component template the directory of the theme

A theme overrides a bar of the page, such as title.tx or leftbar.tx, to
show its own images, and builds their links from theme_dir.  index.tx
calls those components with no arguments, so the template of the theme
was passed nothing.  Now that a template dies when it reads a variable
that its caller did not pass, every page of a themed site died in
title.tx with "Attempt to access disallowed key 'theme_dir' in a
restricted hash".

render_template gives each component template theme_dir, unless the
caller gives it a value, as the Header component already did for itself.
So the Header component no longer works it out, and its own copy is gone.
Trog::Themes loads nothing that loads Trog::Renderer, so a use at the top
does not close the load cycle that the POD warns about.

Index and the code that serves a post still strip /www from theme_dir in
the data of their routes.  Those are page data rather than component
data, and this change leaves them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
troglodyne-bot преди 2 дни
родител
ревизия
8d9d12dc5c
променени са 3 файла, в които са добавени 45 реда и са изтрити 15 реда
  1. 15 4
      lib/Trog/Component.pm
  2. 2 10
      lib/Trog/Component/Header.pm
  3. 28 1
      t/Trog-Component.t

+ 15 - 4
lib/Trog/Component.pm

@@ -6,6 +6,8 @@ use warnings FATAL => 'all';
 
 use Text::Xslate ();
 
+use Trog::Themes ();
+
 =head1 Trog::Component
 
 Dispatcher for the UI components living under the Trog::Component namespace.
@@ -17,9 +19,9 @@ string into their data hash:
     <: component('EmojiPicker') :>
     <: component('Gallery', { album => $post.id, cols => 3 }) :>
 
-Deliberately depends on nothing but Text::Xslate.  Trog::Renderer::Base uses
-this module, and components use Trog::Renderer, so anything heavier here would
-close a compile-time load cycle.  Components are loaded lazily, at call time,
+Deliberately depends on nothing that loads Trog::Renderer.  Trog::Renderer::Base
+uses this module, and components use Trog::Renderer, so such a dependency here
+would close a compile-time load cycle.  Components are loaded lazily, at call time,
 for the same reason.
 
 =head1 Termination Conditions
@@ -135,6 +137,11 @@ sub component ( $name, $args = {} ) {
 Render a component template and hand back the body, for the many components
 whose whole job is exactly that.
 
+The template gets theme_dir as well as $data, unless $data gives it a value: the
+active theme's directory as a path under the site root, such as
+C<themes/mytheme>, for building asset links.  It is empty when there is no
+theme.
+
 Dies naming the template if the render fails, rather than answering with the
 PSGI triplet Trog::Renderer::render() produces on failure.
 
@@ -147,11 +154,15 @@ sub render_template ( $template, $data = {} ) {
     # would close the cycle.  By the time anyone calls this it is already loaded.
     require Trog::Renderer;
 
+    # td() is rooted at /www, which is not in the URL space.
+    my %data = %$data;
+    $data{theme_dir} //= Trog::Themes::td() =~ s{\A/www/}{}r;
+
     my $out = Trog::Renderer->render(
         contenttype => 'text/html',
         component   => 1,
         template    => $template,
-        data        => $data,
+        data        => \%data,
     );
 
     die "Could not render component template '$template'\n" if ref $out;

+ 2 - 10
lib/Trog/Component/Header.pm

@@ -5,7 +5,6 @@ use re '/aasx';
 use warnings FATAL => 'all';
 
 use Trog::Component ();
-use Trog::Themes;
 
 =head1 Trog::Component::Header
 
@@ -28,14 +27,13 @@ It takes these arguments, all optional, and dies on any other:
                                           stylesheet, which is XSL rather than HTML
     title_select                          in place of title, the XPath of the
                                           title, for that same stylesheet
+    theme_dir                             as Trog::Component::render_template()
+                                          gives it, when it is not passed
 
 The list arguments default to empty rather than to the stock sheets, so that a
 caller which has none -- rss_style(), notably -- gets a header with none, as it
 did when the route rendered this itself.
 
-theme_dir is worked out here when it is not passed, as it is the same for every
-page and the template only wants it to build asset paths.
-
 =cut
 
 my @arguments = qw{title route lang author meta_desc meta_tags default_tags stylesheets print_styles scripts embed no_doctype title_select theme_dir};
@@ -52,12 +50,6 @@ sub render (%args) {
     $args{print_styles} //= [];
     $args{scripts}      //= [];
 
-    # td() answers a rooted URL path, which for a themed site starts /www/ --
-    # the template builds hrefs with it, and /www is not in the URL space.
-    my $theme_dir = Trog::Themes::td();
-    $theme_dir =~ s|^/www/||;
-    $args{theme_dir} //= $theme_dir;
-
     return Trog::Component::render_template( 'header.tx', \%args );
 }
 

+ 28 - 1
t/Trog-Component.t

@@ -4,7 +4,8 @@ use re '/aasx';
 use warnings FATAL => 'all';
 
 use Test::More;
-use Test::Fatal qw{exception};
+use Test::Fatal      qw{exception};
+use Test::MockModule qw{strict};
 
 use FindBin::libs;
 
@@ -186,6 +187,32 @@ subtest 'a failure inside a template is not swallowed' => sub {
     like( $err, qr/TestBoom/, "the first failure wins, not the last" );
 };
 
+subtest 'render_template' => sub {
+    require Trog::Renderer;
+
+    my %seen;
+    my $renderer = Test::MockModule->new('Trog::Renderer');
+    $renderer->redefine( render => sub ( $class, %args ) { %seen = %{ $args{data} }; return 'rendered' } );
+    my $themes = Test::MockModule->new('Trog::Themes');
+
+    $themes->redefine( td => sub { '/www/themes/bogus' } );
+    is( Trog::Component::render_template( 'bogus.tx', { title => 'x' } ), 'rendered', "the body of the render comes back" );
+    is_deeply( \%seen, { title => 'x', theme_dir => 'themes/bogus' }, "a template that is passed no theme_dir gets the path of the theme under the site root" );
+
+    Trog::Component::render_template( 'bogus.tx', { theme_dir => undef } );
+    is( $seen{theme_dir}, 'themes/bogus', "an undefined theme_dir is filled in, as the Header component passes one" );
+
+    Trog::Component::render_template( 'bogus.tx', { theme_dir => 'themes/other' } );
+    is( $seen{theme_dir}, 'themes/other', "a theme_dir that the caller passes wins" );
+
+    $themes->redefine( td => sub { '' } );
+    Trog::Component::render_template('bogus.tx');
+    is( $seen{theme_dir}, '', "theme_dir is empty when there is no theme" );
+
+    $renderer->redefine( render => sub { [ 500, [], ['boom'] ] } );
+    like( exception { Trog::Component::render_template('bogus.tx') }, qr/component[ ]template[ ]'bogus[.]tx'/, "a failed render dies naming the template" );
+};
+
 subtest 'through the real renderer' => sub {
     require Trog::Renderer::Base;