You are here

AmpHtmlRenderer.php in Accelerated Mobile Pages (AMP) 8.3

Namespace

Drupal\amp\Render

File

src/Render/AmpHtmlRenderer.php
View source
<?php

namespace Drupal\amp\Render;

use Drupal\Core\Render\MainContent\HtmlRenderer;
use Drupal\Core\Controller\TitleResolverInterface;
use Drupal\Component\Plugin\PluginManagerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Render\RenderCacheInterface;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\HtmlResponse;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\amp\Service\AMPService;
use Drupal\Component\Utility\Xss;

/**
 * Default main content renderer for AMPHTML requests.
 *
 * @see template_preprocess_html()
 * @see \Drupal\Core\Render\MainContent\HtmlRenderer
 */
class AmpHtmlRenderer extends HtmlRenderer {

  /**
   * AMP Service.
   *
   * @var \Drupal\amp\Service\AMPService
   */
  protected $ampService;

  /**
   * Constructs a new HtmlRenderer.
   *
   * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
   *   The title resolver.
   * @param \Drupal\Component\Plugin\PluginManagerInterface $display_variant_manager
   *   The display variant manager.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
   *   The event dispatcher.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The renderer service.
   * @param \Drupal\Core\Render\RenderCacheInterface $render_cache
   *   The render cache service.
   * @param array $renderer_config
   *   The renderer configuration array.
   * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
   *   The theme manager.
   * @param \Drupal\amp\Service\AMPService $amp_service
   *   The AMP service.
   */
  public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache, array $renderer_config, ThemeManagerInterface $theme_manager = NULL, AMPService $amp_service) {
    parent::__construct($title_resolver, $display_variant_manager, $event_dispatcher, $module_handler, $renderer, $render_cache, $renderer_config, $theme_manager);
    $this->ampService = $amp_service;
  }

  /**
   * {@inheritdoc}
   *
   * Copy of Drupal\Core\Render\MainContent\HtmlRenderer:renderResponse()
   * with two important differences:
   *
   * - the page is run through renderRoot() instead of render() to force
   *   placeholders to be replaced on the server, because Big Pipe and other
   *   placeholder replacement javascript won't be available on the client.
   *
   * - the final page markup may be also be run through the AMP converter,
   *   depending on configuration in the AMP module.
   *
   * @TODO Need to watch for changes to parent method and mirror them here.
   */
  public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) {
    list($page, $title) = $this
      ->prepare($main_content, $request, $route_match);
    if (!isset($page['#type']) || $page['#type'] !== 'page') {
      throw new \LogicException('Must be #type page');
    }
    $page['#title'] = $title;

    // Now render the rendered page.html.twig template inside the html.html.twig
    // template, and use the bubbled #attached metadata from $page to ensure we
    // load all attached assets.
    $html = [
      '#type' => 'html',
      'page' => $page,
    ];
    $html['page']['#cache']['contexts'] += [
      'url.query_args:amp',
    ];
    if ($this->ampService
      ->isDevPage()) {
      $html['page']['#cache']['contexts'] += [
        'url.query_args:development',
      ];
    }

    // The special page regions will appear directly in html.html.twig, not in
    // page.html.twig, hence add them here just before rendering
    // html.html.twig.
    $this
      ->buildPageTopAndBottom($html);

    // Render and replace placeholders using RendererInterface::renderRoot()
    // instead of RendererInterface::render().
    // @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor.
    $render_context = new RenderContext();
    $this->renderer
      ->executeInRenderContext($render_context, function () use (&$html) {

      // @todo Simplify this when https://www.drupal.org/node/2495001 lands.
      $this->renderer
        ->renderRoot($html);
    });
    $content = $this->renderCache
      ->getCacheableRenderArray($html);

    // See if the page markup should be run through the AMP converter.
    if (!empty($this->ampService
      ->ampConfig('process_full_html'))) {

      // Replacing the entire page won't work because the page head still
      // contains placeholders for libraries and css. So replace only the
      // contents of <body>.
      $amp = $this->ampService
        ->createAMPConverter();
      $markup = $content['#markup']
        ->__toString();

      // Retrieve the internal contents of <body></body> and run it through AMP.
      $pattern = "/<body[^>]*>(.*?)<\\/body>/is";
      preg_match($pattern, $markup, $matches);
      $body = $matches[1];
      $amp
        ->loadHtml($body);
      $replaced_body = $amp
        ->convertToAmpHtml();

      // Find the replaced body tag attributes.
      $attr_pattern = "/<body[^>](.*?)>/is";
      preg_match($attr_pattern, $markup, $matches);
      $attributes = $matches[1];

      // Reconstruct the page with the updated body.
      $replaced_body = '<body ' . $attributes . '>' . $replaced_body . '<body>';
      $content['#markup'] = preg_replace($pattern, $replaced_body, $markup);
      $content['#allowed_tags'] = array_merge(Xss::getAdminTagList(), [
        'amp-img',
      ]);

      // Add additional required javascript libraries, if found. No worry about
      // duplication of previously-added libraries since Drupal's libraries
      // system will properly handle that.
      if (!empty($amp
        ->getComponentJs())) {
        $libraries = $this->ampService
          ->addComponentLibraries($amp
          ->getComponentJs());
        $content['#allowed_tags'] = array_merge($this->ampService
          ->getComponentTags($amp
          ->getComponentJs()), $content['#allowed_tags']);
      }

      // If development messages are displayed, display the changes made to the
      // markup as a diff.
      if (!empty($amp
        ->getInputOutputHtmlDiff())) {
        $title = '<h2>' . t('AMP converter changes') . '</h2>';
        $pre = '<div>' . t('The AMP converter made the following changes to this page. If you not want this behavior, turn off the option to <strong>Run the page body through the AMP converter</strong> in the AMP settings.') . '</div>';
        $this->ampService
          ->devMessage($title . $pre . '<pre>' . $amp
          ->getInputOutputHtmlDiff() . '</pre>');
      }
      $content['#attached']['library'] = array_merge($content['#attached']['library'], $libraries);
    }

    // Also associate the "rendered" cache tag. This allows us to invalidate the
    // entire render cache, regardless of the cache bin.
    $content['#cache']['tags'][] = 'rendered';
    $response = new HtmlResponse($content, 200, [
      'Content-Type' => 'text/html; charset=UTF-8',
    ]);
    return $response;
  }

}

Classes

Namesort descending Description
AmpHtmlRenderer Default main content renderer for AMPHTML requests.