Component.pm 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. package Trog::Component;
  2. use v5.36;
  3. use re '/aa';
  4. use Text::Xslate ();
  5. =head1 Trog::Component
  6. Dispatcher for the UI components living under the Trog::Component namespace.
  7. Injected into the template processor as component() by Trog::Renderer::Base, so
  8. templates ask for what they want rather than having a route thread a rendered
  9. string into their data hash:
  10. <: component('EmojiPicker') :>
  11. <: component('Gallery', { album => $post.id, cols => 3 }) :>
  12. Deliberately depends on nothing but Text::Xslate. Trog::Renderer::Base uses
  13. this module, and components use Trog::Renderer, so anything heavier here would
  14. close a compile-time load cycle. Components are loaded lazily, at call time,
  15. for the same reason.
  16. =head1 Termination Conditions
  17. Dies if the component can't be named, found, loaded or rendered. There is no
  18. sensible half-rendered page.
  19. Dying is necessary but not sufficient: Xslate catches exceptions thrown out of a
  20. function call, hands them to its warn handler, and carries on with an empty
  21. string where the call was -- so a broken component would otherwise vanish from
  22. the page with nothing but a line on stderr to say so. The failure is therefore
  23. also recorded in $Trog::Component::error, which Trog::Renderer::Base::render()
  24. localises and checks, turning it into the 500 it deserves.
  25. =head1 FUNCTIONS
  26. =cut
  27. # A component whose template asks for itself would otherwise recurse until the
  28. # stack gives out, which says nothing about which component did it.
  29. our $MAX_DEPTH = 10;
  30. our $depth = 0;
  31. # The first component failure of the current render. See above -- dying alone
  32. # does not stop an Xslate render. Always read through a local() in the caller.
  33. our $error;
  34. =head2 component($name, $args) = STRING
  35. Render the named component, marked raw so that callers need no C<| mark_raw>.
  36. $name is one flat namespace level under Trog::Component -- word characters only.
  37. It becomes a path handed to require(), so this is what stops a template naming
  38. its way out of Trog/Component/.
  39. $args is an optional HASHREF, flattened into the component's render(). Nullary
  40. components simply ignore it.
  41. =cut
  42. sub component ( $name, $args = {} ) {
  43. my ( $out, $err );
  44. {
  45. local $@;
  46. $out = eval { _component( $name, $args ) };
  47. $err = $@;
  48. }
  49. return $out if defined $out;
  50. # Record as well as rethrow: the die on its own gets swallowed by Xslate.
  51. # First failure wins, as the ones after it tend to be consequences of it.
  52. # The throw uses our own error rather than $error, so a direct caller is
  53. # told what *it* did even when something earlier left a record behind.
  54. $err ||= "Component '$name' failed for no stated reason\n";
  55. $error //= $err;
  56. die $err;
  57. }
  58. sub _component ( $name, $args ) {
  59. my $module = _load($name);
  60. die "Component '$name' recursed more than $MAX_DEPTH deep\n" if $depth >= $MAX_DEPTH;
  61. local $depth = $depth + 1;
  62. $args = {} unless ref $args eq 'HASH';
  63. my $out;
  64. local $@;
  65. eval {
  66. $out = $module->can('render')->(%$args);
  67. 1;
  68. } or do {
  69. die "Component '$name' failed to render: $@";
  70. };
  71. # Trog::Renderer::render() answers failure with a PSGI triplet from _yeet(),
  72. # which is meaningless halfway through a template. Catching it here is what
  73. # saves every call site from the 'return $it if ref $it eq ARRAY' dance.
  74. die "Component '$name' returned a " . ref($out) . " rather than a string\n" if ref $out;
  75. die "Component '$name' rendered nothing\n" if !defined $out;
  76. return Text::Xslate::mark_raw($out);
  77. }
  78. # Resolved at call time rather than compile time, so that components are free to
  79. # use Trog::Renderer without cycling back through us. require() memoizes in
  80. # %INC; %loaded only saves us the repeat eval and string munging.
  81. sub _load ($name) {
  82. state %loaded;
  83. die "Component name must be given\n" unless defined $name && length $name;
  84. die "Invalid component name '$name'\n" unless $name =~ m/^[A-Za-z][A-Za-z0-9_]*$/;
  85. my $module = "Trog::Component::$name";
  86. return $module if $loaded{$module};
  87. local $@;
  88. eval {
  89. require "Trog/Component/$name.pm"; ## no critic (RequireBarewordIncludes) -- $name is validated above
  90. 1;
  91. } or do {
  92. die "Could not load component '$name': $@";
  93. };
  94. die "Component '$name' has no render()\n" unless $module->can('render');
  95. $loaded{$module} = 1;
  96. return $module;
  97. }
  98. 1;