Component.pm 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  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. =head2 render_template($template, $data) = STRING
  79. Render a component template and hand back the body, for the many components
  80. whose whole job is exactly that.
  81. Dies naming the template if the render fails, rather than answering with the
  82. PSGI triplet Trog::Renderer::render() produces on failure.
  83. =cut
  84. sub render_template ( $template, $data = {} ) {
  85. # Required rather than used, for the same reason components are loaded
  86. # lazily: Trog::Renderer::Base uses this module, so a compile-time use here
  87. # would close the cycle. By the time anyone calls this it is already loaded.
  88. require Trog::Renderer;
  89. my $out = Trog::Renderer->render(
  90. contenttype => 'text/html',
  91. component => 1,
  92. template => $template,
  93. data => $data,
  94. );
  95. die "Could not render component template '$template'\n" if ref $out;
  96. return $out;
  97. }
  98. # Resolved at call time rather than compile time, so that components are free to
  99. # use Trog::Renderer without cycling back through us. require() memoizes in
  100. # %INC; %loaded only saves us the repeat eval and string munging.
  101. sub _load ($name) {
  102. state %loaded;
  103. die "Component name must be given\n" unless defined $name && length $name;
  104. die "Invalid component name '$name'\n" unless $name =~ m/^[A-Za-z][A-Za-z0-9_]*$/;
  105. my $module = "Trog::Component::$name";
  106. return $module if $loaded{$module};
  107. local $@;
  108. eval {
  109. require "Trog/Component/$name.pm"; ## no critic (RequireBarewordIncludes) -- $name is validated above
  110. 1;
  111. } or do {
  112. die "Could not load component '$name': $@";
  113. };
  114. die "Component '$name' has no render()\n" unless $module->can('render');
  115. $loaded{$module} = 1;
  116. return $module;
  117. }
  118. 1;