package Trog::Component; use 5.040; use re '/aasx'; use warnings FATAL => 'all'; use Text::Xslate (); use Trog::Themes (); =head1 Trog::Component Dispatcher for the UI components living under the Trog::Component namespace. Injected into the template processor as component() by Trog::Renderer::Base, so templates ask for what they want rather than having a route thread a rendered string into their data hash: <: component('EmojiPicker') :> <: component('Gallery', { album => $post.id, cols => 3 }) :> 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 Dies if the component can't be named, found, loaded or rendered. There is no sensible half-rendered page. Dying is necessary but not sufficient: Xslate catches exceptions thrown out of a function call, hands them to its warn handler, and carries on with an empty string where the call was -- so a broken component would otherwise vanish from the page with nothing but a line on stderr to say so. The failure is therefore also recorded in $Trog::Component::error, which Trog::Renderer::Base::render() localizes and checks, turning it into the 500 it deserves. =head1 FUNCTIONS =cut # A component whose template asks for itself would otherwise recurse until the # stack gives out, which says nothing about which component did it. our $MAX_DEPTH = 10; our $depth = 0; # The first component failure of the current render. See above -- dying alone # does not stop an Xslate render. Always read through a local() in the caller. our $error; # Resolved at call time rather than compile time, so that components are free to # use Trog::Renderer without cycling back through us. require() memoizes in # %INC; %loaded only saves us the repeat eval and string munging. my sub load ($name) { ## no critic (ProhibitSingleUseLexicalSubs) -- 22 lines, which would overcomplicate _component() state %loaded; die "Component name must be given\n" unless $name; die "Invalid component name '$name'\n" unless $name =~ m/^[[:alpha:]]\w*$/; my $module = "Trog::Component::$name"; return $module if $loaded{$module}; local $@; eval { require "Trog/Component/$name.pm"; ## no critic (RequireBarewordIncludes) -- $name is validated above 1; } or do { die "Could not load component '$name': $@"; }; die "Component '$name' has no render()\n" unless $module->can('render'); $loaded{$module} = 1; return $module; } my sub _component ( $name, $args ) { ## no critic (ProhibitSingleUseLexicalSubs) -- 25 lines, which would overcomplicate component() my $module = load($name); die "Component '$name' recursed more than $MAX_DEPTH deep\n" if $depth >= $MAX_DEPTH; local $Trog::Component::depth = $depth + 1; $args = {} unless ref $args eq 'HASH'; my $out; local $@; eval { $out = $module->can('render')->(%$args); 1; } or do { die "Component '$name' failed to render: $@"; }; # Trog::Renderer::render() answers failure with a PSGI triplet from _yeet(), # which is meaningless halfway through a template. Catching it here is what # saves every call site from the 'return $it if ref $it eq ARRAY' dance. die "Component '$name' returned a " . ref($out) . " rather than a string\n" if ref $out; die "Component '$name' rendered nothing\n" if !defined $out; return Text::Xslate::mark_raw($out); } =head2 component($name, $args) = STRING Render the named component, marked raw so that callers need no C<| mark_raw>. $name is one flat namespace level under Trog::Component -- word characters only. It becomes a path handed to require(), so this is what stops a template naming its way out of Trog/Component/. $args is an optional HASHREF, flattened into the component's render(). Nullary components simply ignore it. =cut sub component ( $name, $args = {} ) { my ( $out, $err ); { local $@; $out = eval { _component( $name, $args ) }; $err = $@; } return $out if defined $out; # Record as well as rethrow: the die on its own gets swallowed by Xslate. # First failure wins, as the ones after it tend to be consequences of it. # The throw uses our own error rather than $error, so a direct caller is # told what *it* did even when something earlier left a record behind. $err ||= "Component '$name' failed for no stated reason\n"; $error //= $err; die $err; } =head2 render_template($template, $data) = STRING 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, 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. =cut sub render_template ( $template, $data = {} ) { # Required rather than used, for the same reason components are loaded # lazily: Trog::Renderer::Base uses this module, so a compile-time use here # 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, ); die "Could not render component template '$template'\n" if ref $out; return $out; } 1;