Component.pm 5.3 KB

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