Просмотр исходного кода

Let a theme name the scripts of every page

A route names the scripts of its page, and a template may not write a
<script src> of its own, since the scripts of a page are one bundle
that only a route can declare.  But a theme can name scripts only on
the routes that it ships, and a script that a theme puts in a bar of
the page, which every page has, belongs to every route.  So such a
theme had nowhere to put it but a <script src> in its template.

A theme now names those scripts in @Theme::scripts.  build_routes puts
them before the scripts of every route, so they are in each bundle, and
it declares those bundles at startup as it does the others.  It does
this after _routes, which keeps one table for the life of the process,
so that each build sees the theme as it is.  Each route is copied
rather than changed, as a route and its /secure copy can share one
hash, and a shared one would get the scripts twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
troglodyne-bot 2 дней назад
Родитель
Сommit
e4370fe152
4 измененных файлов с 31 добавлено и 2 удалено
  1. 3 2
      design.md
  2. 8 0
      lib/TCMS.pm
  3. 8 0
      lib/Theme.pm
  4. 12 0
      t/TCMS.t

+ 3 - 2
design.md

@@ -390,8 +390,9 @@ override a handful of rules without restating the sheet. A theme may ship a
 
 
 A script resolves as a template does. A route names the scripts of its page in
 A script resolves as a template does. A route names the scripts of its page in
 its definition: `scripts` for every reader, and `authenticated_scripts` for a
 its definition: `scripts` for every reader, and `authenticated_scripts` for a
-reader who is logged in as well. A template does not write a `<script src>` tag
-of its own. The route of a post gets the scripts of a post page, from
+reader who is logged in as well. A theme names the scripts of every page in
+`@Theme::scripts`, which go before those of the route. A template does not
+write a `<script src>` tag of its own. The route of a post gets the scripts of a post page, from
 `%Trog::Routes::HTML::post_page_scripts`. `finish_render` links the lists as one
 `%Trog::Routes::HTML::post_page_scripts`. `finish_render` links the lists as one
 bundle, which `Trog::Bundle` builds and `/bundle/{version}/{names}.js` serves.
 bundle, which `Trog::Bundle` builds and `/bundle/{version}/{names}.js` serves.
 That route serves only a list that a route declares, so a request cannot make
 That route serves only a list that a route declares, so a request cannot make

+ 8 - 0
lib/TCMS.pm

@@ -123,6 +123,7 @@ C<nocache>, C<noindex>, C<nomap>, C<robot_name> and C<totp_exempt> become
 x-tcms-auth and so on, which the callback finds in C<< $query->{dispatcher} >>.
 x-tcms-auth and so on, which the callback finds in C<< $query->{dispatcher} >>.
 C<parameters> are OpenAPI parameter objects, and each variable of a template
 C<parameters> are OpenAPI parameter objects, and each variable of a template
 that they leave out is a required string.  A route with no method is a GET.
 that they leave out is a required string.  A route with no method is a GET.
+Every route links the scripts of @Theme::scripts before its own C<scripts>.
 
 
 A route that does not declare its parameters takes the query or the form that
 A route that does not declare its parameters takes the query or the form that
 %Trog::Routes::HTML::inputs names for its callback, less what its C<data> fixes and
 %Trog::Routes::HTML::inputs names for its callback, less what its C<data> fixes and
@@ -148,6 +149,13 @@ sub build_routes {
     my $data   = Trog::Data->new($conf);
     my $data   = Trog::Data->new($conf);
     my %routes = %{ _routes($data) };
     my %routes = %{ _routes($data) };
 
 
+    # The theme's scripts go on every page, before the page's own.  A copy of
+    # each route, since a route and its /secure copy share one hash.
+    my @theme_scripts = do { no warnings 'once'; @Theme::scripts };
+    if (@theme_scripts) {
+        $routes{$_} = { %{ $routes{$_} }, scripts => [ @theme_scripts, @{ $routes{$_}{scripts} // [] } ] } for keys(%routes);
+    }
+
     my $default_route = $routes{default}{callback};
     my $default_route = $routes{default}{callback};
 
 
     my %paths;
     my %paths;

+ 8 - 0
lib/Theme.pm

@@ -42,6 +42,12 @@ Fallback meta description, used when the page has no post body to summarize.
 
 
 Comma separated keywords for the meta keywords tag.
 Comma separated keywords for the meta keywords tag.
 
 
+=item @scripts
+
+The names of scripts that every page links, before the scripts of the page
+itself.  Each is a file in the theme's scripts/ directory or in www/scripts,
+as the scripts of a route are.
+
 =item $twitter_account, $fb_app_id
 =item $twitter_account, $fb_app_id
 
 
 Set these if you want the relevant twitter: and fb: meta tags emitted.
 Set these if you want the relevant twitter: and fb: meta tags emitted.
@@ -57,6 +63,8 @@ our $display_name  = 'tCMS';
 our $description   = 'tCMS is a content management system written in perl.';
 our $description   = 'tCMS is a content management system written in perl.';
 our $default_tags  = 'tcms';
 our $default_tags  = 'tcms';
 
 
+our @scripts = ();
+
 our $twitter_account = '';
 our $twitter_account = '';
 our $fb_app_id       = '';
 our $fb_app_id       = '';
 
 

+ 12 - 0
t/TCMS.t

@@ -138,6 +138,18 @@ subtest 'the scripts of a route' => sub {
     ok( $declared{'highlight.min+reveal.min+post+post_relations'}, 'and the one for a reader who is logged in' );
     ok( $declared{'highlight.min+reveal.min+post+post_relations'}, 'and the one for a reader who is logged in' );
     ok( $declared{'chart.min'},                                    'and the bundle of every other route' );
     ok( $declared{'chart.min'},                                    'and the bundle of every other route' );
     ok( !$declared{'reveal.min+highlight.min'},                    'but no other list' );
     ok( !$declared{'reveal.min+highlight.min'},                    'but no other list' );
+
+    local @Theme::scripts = qw{theme.js};
+    my $themed = { TCMS::build_routes() }->{paths};
+    is_deeply( $themed->{'/login'}{get}{'x-tcms-scripts'},                     [qw{theme.js}],                                "a theme's scripts go on a route that names none" );
+    is_deeply( $themed->{'/metrics'}{get}{'x-tcms-scripts'},                   [qw{theme.js chart.min.js}],                   "and before the scripts of a route that names its own" );
+    is_deeply( $themed->{'/posts/p-one'}{get}{'x-tcms-scripts'},               [qw{theme.js highlight.min.js reveal.min.js}], "and before the scripts of a post page" );
+    is_deeply( $themed->{'/secure/posts/p-one'}{get}{'x-tcms-scripts'},        [qw{theme.js highlight.min.js reveal.min.js}], "once, on a /secure copy that shares the route of its post" );
+    is_deeply( $themed->{'/posts/p-one'}{get}{'x-tcms-authenticated_scripts'}, [qw{post.js post_relations.js}],               "and not among the scripts for a reader who is logged in, who gets both lists" );
+    ok( $Trog::Bundle::declared{'theme+chart.min'}, 'the bundle of a themed route is declared' );
+
+    @Theme::scripts = ();
+    is_deeply( { TCMS::build_routes() }->{paths}{'/metrics'}{get}{'x-tcms-scripts'}, ['chart.min.js'], "and building the routes again does not keep the theme's scripts of the last build" );
 };
 };
 
 
 subtest 'validation' => sub {
 subtest 'validation' => sub {