Component.pm 5.7 KB

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