/** * Processes the content for output. * * If content is a render array, it may contain attached assets to be * processed. * * @return string|\Drupal\Component\Utility\SafeStringInterface * HTML rendered content. */ protected function getRenderedContent() { $this->attachedAssets = new AttachedAssets(); if (is_array($this->content)) { $html = \Drupal::service('renderer')->renderRoot($this->content); $this->attachedAssets = AttachedAssets::createFromRenderArray($this->content); return $html; } else { return $this->content; } }
/** * Tests AjaxResponse::prepare() AJAX commands ordering. */ public function testOrder() { $expected_commands = array(); // Expected commands, in a very specific order. $asset_resolver = \Drupal::service('asset.resolver'); $css_collection_renderer = \Drupal::service('asset.css.collection_renderer'); $js_collection_renderer = \Drupal::service('asset.js.collection_renderer'); $renderer = \Drupal::service('renderer'); $expected_commands[0] = new SettingsCommand(array('ajax' => 'test'), TRUE); $build['#attached']['library'][] = 'ajax_test/order-css-command'; $assets = AttachedAssets::createFromRenderArray($build); $css_render_array = $css_collection_renderer->render($asset_resolver->getCssAssets($assets, FALSE)); $expected_commands[1] = new AddCssCommand($renderer->render($css_render_array)); $build['#attached']['library'][] = 'ajax_test/order-header-js-command'; $build['#attached']['library'][] = 'ajax_test/order-footer-js-command'; $assets = AttachedAssets::createFromRenderArray($build); list($js_assets_header, $js_assets_footer) = $asset_resolver->getJsAssets($assets, FALSE); $js_header_render_array = $js_collection_renderer->render($js_assets_header); $js_footer_render_array = $js_collection_renderer->render($js_assets_footer); $expected_commands[2] = new PrependCommand('head', $renderer->render($js_header_render_array)); $expected_commands[3] = new AppendCommand('body', $renderer->render($js_footer_render_array)); $expected_commands[4] = new HtmlCommand('body', 'Hello, world!'); // Load any page with at least one CSS file, at least one JavaScript file // and at least one #ajax-powered element. The latter is an assumption of // drupalPostAjaxForm(), the two former are assumptions of // AjaxResponse::ajaxRender(). // @todo refactor AJAX Framework + tests to make less assumptions. $this->drupalGet('ajax_forms_test_lazy_load_form'); // Verify AJAX command order — this should always be the order: // 1. JavaScript settings // 2. CSS files // 3. JavaScript files in the header // 4. JavaScript files in the footer // 5. Any other AJAX commands, in whatever order they were added. $commands = $this->drupalPostAjaxForm(NULL, array(), NULL, 'ajax-test/order', array(), array(), NULL, array()); $this->assertCommand(array_slice($commands, 0, 1), $expected_commands[0]->render(), 'Settings command is first.'); $this->assertCommand(array_slice($commands, 1, 1), $expected_commands[1]->render(), 'CSS command is second (and CSS files are ordered correctly).'); $this->assertCommand(array_slice($commands, 2, 1), $expected_commands[2]->render(), 'Header JS command is third.'); $this->assertCommand(array_slice($commands, 3, 1), $expected_commands[3]->render(), 'Footer JS command is fourth.'); $this->assertCommand(array_slice($commands, 4, 1), $expected_commands[4]->render(), 'HTML command is fifth.'); }
/** * Tests JavaScript files that have querystrings attached get added right. */ function testAddJsFileWithQueryString() { $build['#attached']['library'][] = 'common_test/querystring'; $assets = AttachedAssets::createFromRenderArray($build); $css = $this->assetResolver->getCssAssets($assets, FALSE); $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; $this->assertTrue(array_key_exists('core/modules/system/tests/modules/common_test/querystring.css?arg1=value1&arg2=value2', $css), 'CSS file with query string is correctly added.'); $this->assertTrue(array_key_exists('core/modules/system/tests/modules/common_test/querystring.js?arg1=value1&arg2=value2', $js), 'JavaScript file with query string is correctly added.'); $css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css); $rendered_css = $this->renderer->renderPlain($css_render_array); $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); $rendered_js = $this->renderer->renderPlain($js_render_array); $query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0'; $this->assertNotIdentical(strpos($rendered_css, '<link rel="stylesheet" href="' . str_replace('&', '&', file_url_transform_relative(file_create_url('core/modules/system/tests/modules/common_test/querystring.css?arg1=value1&arg2=value2'))) . '&' . $query_string . '" media="all" />'), FALSE, 'CSS file with query string gets version query string correctly appended..'); $this->assertNotIdentical(strpos($rendered_js, '<script src="' . str_replace('&', '&', file_url_transform_relative(file_create_url('core/modules/system/tests/modules/common_test/querystring.js?arg1=value1&arg2=value2'))) . '&' . $query_string . '"></script>'), FALSE, 'JavaScript file with query string gets version query string correctly appended.'); }
/** * {@inheritdoc} */ public function processAttachments(AttachmentsInterface $response) { // @todo Convert to assertion once https://www.drupal.org/node/2408013 lands if (!$response instanceof HtmlResponse) { throw new \InvalidArgumentException('\\Drupal\\Core\\Render\\HtmlResponse instance expected.'); } // First, render the actual placeholders; this may cause additional // attachments to be added to the response, which the attachment // placeholders rendered by renderHtmlResponseAttachmentPlaceholders() will // need to include. // // @todo Exceptions should not be used for code flow control. However, the // Form API does not integrate with the HTTP Kernel based architecture of // Drupal 8. In order to resolve this issue properly it is necessary to // completely separate form submission from rendering. // @see https://www.drupal.org/node/2367555 try { $response = $this->renderPlaceholders($response); } catch (EnforcedResponseException $e) { return $e->getResponse(); } // Get a reference to the attachments. $attached = $response->getAttachments(); // Send a message back if the render array has unsupported #attached types. $unsupported_types = array_diff(array_keys($attached), ['html_head', 'feed', 'html_head_link', 'http_header', 'library', 'html_response_attachment_placeholders', 'placeholders', 'drupalSettings']); if (!empty($unsupported_types)) { throw new \LogicException(sprintf('You are not allowed to use %s in #attached.', implode(', ', $unsupported_types))); } // If we don't have any placeholders, there is no need to proceed. if (!empty($attached['html_response_attachment_placeholders'])) { // Get the placeholders from attached and then remove them. $attachment_placeholders = $attached['html_response_attachment_placeholders']; unset($attached['html_response_attachment_placeholders']); $assets = AttachedAssets::createFromRenderArray(['#attached' => $attached]); // Take Ajax page state into account, to allow for something like // Turbolinks to be implemented without altering core. // @see https://github.com/rails/turbolinks/ $ajax_page_state = $this->requestStack->getCurrentRequest()->get('ajax_page_state'); $assets->setAlreadyLoadedLibraries(isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []); $variables = $this->processAssetLibraries($assets, $attachment_placeholders); // $variables now contains the markup to load the asset libraries. Update // $attached with the final list of libraries and JavaScript settings, so // that $response can be updated with those. Then the response object will // list the final, processed attachments. $attached['library'] = $assets->getLibraries(); $attached['drupalSettings'] = $assets->getSettings(); // Since we can only replace content in the HTML head section if there's a // placeholder for it, we can safely avoid processing the render array if // it's not present. if (!empty($attachment_placeholders['head'])) { // 'feed' is a special case of 'html_head_link'. We process them into // 'html_head_link' entries and merge them. if (!empty($attached['feed'])) { $attached = BubbleableMetadata::mergeAttachments($attached, $this->processFeed($attached['feed'])); unset($attached['feed']); } // 'html_head_link' is a special case of 'html_head' which can be present // as a head element, but also as a Link: HTTP header depending on // settings in the render array. Processing it can add to both the // 'html_head' and 'http_header' keys of '#attached', so we must address // it before 'html_head'. if (!empty($attached['html_head_link'])) { // Merge the processed 'html_head_link' into $attached so that its // 'html_head' and 'http_header' values are present for further // processing. $attached = BubbleableMetadata::mergeAttachments($attached, $this->processHtmlHeadLink($attached['html_head_link'])); unset($attached['html_head_link']); } // Now we can process 'html_head', which contains both 'feed' and // 'html_head_link'. if (!empty($attached['html_head'])) { $variables['head'] = $this->processHtmlHead($attached['html_head']); } } // Now replace the attachment placeholders. $this->renderHtmlResponseAttachmentPlaceholders($response, $attachment_placeholders, $variables); } // Set the HTTP headers and status code on the response if any bubbled. if (!empty($attached['http_header'])) { $this->setHeaders($response, $attached['http_header']); } // AttachmentsResponseProcessorInterface mandates that the response it // processes contains the final attachment values. $response->setAttachments($attached); return $response; }
/** * {@inheritdoc} */ public function sendContent($content, array $attachments) { // First, gather the BigPipe placeholders that must be replaced. $placeholders = isset($attachments['big_pipe_placeholders']) ? $attachments['big_pipe_placeholders'] : []; $nojs_placeholders = isset($attachments['big_pipe_nojs_placeholders']) ? $attachments['big_pipe_nojs_placeholders'] : []; // BigPipe sends responses using "Transfer-Encoding: chunked". To avoid // sending already-sent assets, it is necessary to track cumulative assets // from all previously rendered/sent chunks. // @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.41 $cumulative_assets = AttachedAssets::createFromRenderArray(['#attached' => $attachments]); $cumulative_assets->setAlreadyLoadedLibraries($attachments['library']); // The content in the placeholders may depend on the session, and by the // time the response is sent (see index.php), the session is already closed. // Reopen it for the duration that we are rendering placeholders. $this->session->start(); list($pre_body, $post_body) = explode('</body>', $content, 2); $this->sendPreBody($pre_body, $nojs_placeholders, $cumulative_assets); $this->sendPlaceholders($placeholders, $this->getPlaceholderOrder($pre_body), $cumulative_assets); $this->sendPostBody($post_body); // Close the session again. $this->session->save(); return $this; }
/** * Gathers the markup for each type of asset. * * @param array $render_array * The render array for the entity. * * @return array * An array of rendered assets. See self::getRenderedEntity() for the keys. * * @todo template_preprocess_html() should be split up and made reusable. */ protected function gatherAssetMarkup(array $render_array) { $assets = AttachedAssets::createFromRenderArray($render_array); // Render the asset collections. $css_assets = $this->assetResolver->getCssAssets($assets, FALSE); $variables['styles'] = $this->cssCollectionRenderer->render($css_assets); list($js_assets_header, $js_assets_footer) = $this->assetResolver->getJsAssets($assets, FALSE); $variables['scripts'] = $this->jsCollectionRenderer->render($js_assets_header); $variables['scripts_bottom'] = $this->jsCollectionRenderer->render($js_assets_footer); // @todo Handle all non-asset attachments. //$variables['head'] = drupal_get_html_head(FALSE); return $variables; }
/** * Processes asset libraries into render arrays. * * @param array $attached * The attachments to process. * @param array $placeholders * The placeholders that exist in the response. * * @return array * An array keyed by asset type, with keys: * - styles * - scripts * - scripts_bottom */ protected function processAssetLibraries(array $attached, array $placeholders) { $all_attached = ['#attached' => $attached]; $assets = AttachedAssets::createFromRenderArray($all_attached); // Take Ajax page state into account, to allow for something like Turbolinks // to be implemented without altering core. // @see https://github.com/rails/turbolinks/ // @todo https://www.drupal.org/node/2497115 - Below line is broken due to ->request. $ajax_page_state = $this->requestStack->getCurrentRequest()->request->get('ajax_page_state'); $assets->setAlreadyLoadedLibraries(isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []); $variables = []; // Print styles - if present. if (isset($placeholders['styles'])) { // Optimize CSS if necessary, but only during normal site operation. $optimize_css = !defined('MAINTENANCE_MODE') && $this->config->get('css.preprocess'); $variables['styles'] = $this->cssCollectionRenderer->render($this->assetResolver->getCssAssets($assets, $optimize_css)); } // Print scripts - if any are present. if (isset($placeholders['scripts']) || isset($placeholders['scripts_bottom'])) { // Optimize JS if necessary, but only during normal site operation. $optimize_js = !defined('MAINTENANCE_MODE') && !\Drupal::state()->get('system.maintenance_mode') && $this->config->get('js.preprocess'); list($js_assets_header, $js_assets_footer) = $this->assetResolver->getJsAssets($assets, $optimize_js); $variables['scripts'] = $this->jsCollectionRenderer->render($js_assets_header); $variables['scripts_bottom'] = $this->jsCollectionRenderer->render($js_assets_footer); } return $variables; }
/** * Generate the HTML for our entity. * * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The entity we're rendering. * @param bool $use_default_css * TRUE if we should inject our default CSS otherwise FALSE. * @param bool $optimize_css * TRUE if we should compress the CSS otherwise FALSE. * * @return string * The generated HTML. * * @throws \Exception */ protected function getHtml(ContentEntityInterface $entity, $use_default_css, $optimize_css) { $render_controller = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId()); $render = ['#theme' => 'entity_print__' . $entity->getEntityTypeId() . '__' . $entity->id(), '#entity' => $entity, '#entity_array' => $render_controller->view($entity, 'pdf'), '#attached' => []]; // Inject some generic CSS across all templates. if ($use_default_css) { $render['#attached']['library'][] = 'entity_print/default'; } // Allow other modules to add their own CSS. $this->moduleHandler->alter('entity_print_css', $render, $entity); // Inject CSS from the theme info files and then render the CSS. $render = $this->addCss($render, $entity); $css_assets = $this->assetResolver->getCssAssets(AttachedAssets::createFromRenderArray($render), $optimize_css); $rendered_css = $this->cssRenderer->render($css_assets); $render['#entity_print_css'] = $this->renderer->render($rendered_css); return $this->renderer->render($render); }