 big_pipe.libraries.yml                             |   2 +-
 js/big_pipe.js                                     |  10 +-
 src/Render/BigPipe.php                             |  53 +++---
 src/Render/Placeholder/BigPipeStrategy.php         |  77 +++++---
 .../BigPipeResponseAttachmentsProcessorTest.php    | 151 ++++++++++++++++
 .../Render/Placeholder/BigPipeStrategyTest.php     | 201 +++++++++++++++++++++
 6 files changed, 437 insertions(+), 57 deletions(-)

diff --git a/big_pipe.libraries.yml b/big_pipe.libraries.yml
index 88ecff6..4a9331d 100644
--- a/big_pipe.libraries.yml
+++ b/big_pipe.libraries.yml
@@ -3,7 +3,7 @@ big_pipe:
   js:
     js/big_pipe.js: {}
   drupalSettings:
-    bigPipePlaceholders: []
+    bigPipePlaceholderIds: []
   dependencies:
     - core/jquery
     - core/drupal
diff --git a/js/big_pipe.js b/js/big_pipe.js
index 026b0e0..7890ab5 100644
--- a/js/big_pipe.js
+++ b/js/big_pipe.js
@@ -12,16 +12,16 @@
    *
    * @param {number} index
    *   Current index.
-   * @param {HTMLScriptElement} placeholder
+   * @param {HTMLScriptElement} placeholderReplacement
    *   Script tag created by bigPipe.
    */
-  function bigPipeProcessPlaceholder(index, placeholder) {
-    var placeholderName = this.getAttribute('data-big-pipe-placeholder');
+  function bigPipeProcessPlaceholderReplacement(index, placeholderReplacement) {
+    var placeholderId = this.getAttribute('data-big-pipe-replacement-for-placeholder-with-id');
     var content = this.textContent.trim();
     // Ignore any placeholders that are not in the known placeholder list.
     // This is used to avoid someone trying to XSS the site via the
     // placeholdering mechanism.;
-    if (typeof drupalSettings.bigPipePlaceholders[placeholderName] !== 'undefined') {
+    if (typeof drupalSettings.bigPipePlaceholderIds[placeholderId] !== 'undefined') {
       // If we try to parse the content too early textContent will be empty,
       // making JSON.parse fail. Remove once so that it can be processed again
       // later.
@@ -61,7 +61,7 @@
     }
 
     $(context).find('script[data-drupal-ajax-processor="big_pipe"]').once('big-pipe')
-      .each(bigPipeProcessPlaceholder);
+      .each(bigPipeProcessPlaceholderReplacement);
 
     // If we see a stop element always clear the timeout.
     if (context.querySelector('script[data-big-pipe-event="stop"]')) {
diff --git a/src/Render/BigPipe.php b/src/Render/BigPipe.php
index abe8133..0b4250c 100644
--- a/src/Render/BigPipe.php
+++ b/src/Render/BigPipe.php
@@ -195,18 +195,26 @@ class BigPipe implements BigPipeInterface {
    *   BigPipe placeholders.
    */
   protected function sendNoJsPlaceholders($html, $no_js_placeholders, AttachedAssetsInterface $cumulative_assets) {
-    $fragments = explode('<div data-big-pipe-selector-nojs="', $html);
-    print array_shift($fragments);
-    ob_end_flush();
-    flush();
-
-    foreach ($fragments as $fragment) {
-      $t = explode('"></div>', $fragment, 2);
-      $placeholder = $t[0];
-      if (!isset($no_js_placeholders[$placeholder])) {
+    // Split the HTML on every no-JS placeholder string.
+    $prepare_for_preg_split = function ($placeholder_string) {
+      return '(' . preg_quote($placeholder_string, '/') . ')';
+    };
+    $preg_placeholder_strings = array_map($prepare_for_preg_split, array_keys($no_js_placeholders));
+    $fragments = preg_split('/' . implode('|', $preg_placeholder_strings) . '/', $html, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
+    assert('count($fragments) % 2 === 1');
+
+    foreach ($fragments as $index => $fragment) {
+      // The fragments with an even index (0, 2, 4 …) contain HTML which needs
+      // to be printed and flushed right away. The rest of the logic in the loop
+      // handles the uneven indices (1, 3, 5 …) which contains placeholders.
+      if ($index % 2 === 0) {
+        print $fragment;
+        flush();
         continue;
       }
 
+      $placeholder = $fragment;
+      assert('isset($no_js_placeholders[$placeholder])');
       $token = Crypt::randomBytesBase64(55);
 
       // Render the placeholder, but include the cumulative settings assets, so
@@ -252,7 +260,6 @@ class BigPipe implements BigPipeInterface {
 
       // Send this embedded HTML response.
       print $html_response->getContent();
-      print $t[1];
       flush();
 
       // Another placeholder was rendered and sent, track the set of asset
@@ -270,11 +277,11 @@ class BigPipe implements BigPipeInterface {
    *
    * @param array $placeholders
    *   Associative array; the BigPipe placeholders. Keys are the BigPipe
-   *   selectors.
+   *   placeholder IDs.
    * @param array $placeholder_order
    *   Indexed array; the order in which the BigPipe placeholders must be sent.
-   *   Values are the BigPipe selectors. (These values correspond to keys in
-   *   $placeholders.)
+   *   Values are the BigPipe placeholder IDs. (These values correspond to keys
+   *   in $placeholders.)
    * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets
    *   The cumulative assets sent so far; to be updated while rendering BigPipe
    *   placeholders.
@@ -300,23 +307,23 @@ class BigPipe implements BigPipeInterface {
     $fake_request = $this->requestStack->getMasterRequest()->duplicate();
     $fake_request->headers->set('Accept', 'application/json');
 
-    foreach ($placeholder_order as $placeholder) {
-      if (!isset($placeholders[$placeholder])) {
+    foreach ($placeholder_order as $placeholder_id) {
+      if (!isset($placeholders[$placeholder_id])) {
         continue;
       }
 
       // Render the placeholder.
-      $placeholder_render_array = $placeholders[$placeholder];
-      $elements = $this->renderPlaceholder($placeholder, $placeholder_render_array);
+      $placeholder_render_array = $placeholders[$placeholder_id];
+      $elements = $this->renderPlaceholder($placeholder_id, $placeholder_render_array);
 
       // Create a new AjaxResponse.
       $ajax_response = new AjaxResponse();
       // JavaScript's querySelector automatically decodes HTML entities in
       // attributes, so we must decode the entities of the current BigPipe
-      // placeholder (which has HTML entities encoded since we use it to find
+      // placeholder ID (which has HTML entities encoded since we use it to find
       // the placeholders).
-      $big_pipe_js_selector = Html::decodeEntities($placeholder);
-      $ajax_response->addCommand(new ReplaceCommand(sprintf('[data-big-pipe-selector="%s"]', $big_pipe_js_selector), $elements['#markup']));
+      $big_pipe_js_placeholder_id = Html::decodeEntities($placeholder_id);
+      $ajax_response->addCommand(new ReplaceCommand(sprintf('[data-big-pipe-placeholder-id="%s"]', $big_pipe_js_placeholder_id), $elements['#markup']));
       $ajax_response->setAttachments($elements['#attached']);
 
       // Push a fake request with the asset libraries loaded so far and dispatch
@@ -338,7 +345,7 @@ class BigPipe implements BigPipeInterface {
       // Send this embedded AJAX response.
       $json = $ajax_response->getContent();
       $output = <<<EOF
-    <script type="application/json" data-big-pipe-placeholder="$placeholder" data-drupal-ajax-processor="big_pipe">
+    <script type="application/json" data-big-pipe-replacement-for-placeholder-with-id="$placeholder_id" data-drupal-ajax-processor="big_pipe">
     $json
     </script>
 EOF;
@@ -408,10 +415,10 @@ EOF;
    *
    * @return array
    *   Indexed array; the order in which the BigPipe placeholders must be sent.
-   *   Values are the BigPipe selectors.
+   *   Values are the BigPipe placeholder IDs.
    */
   protected function getPlaceholderOrder($html) {
-    $fragments = explode('<div data-big-pipe-selector="', $html);
+    $fragments = explode('<div data-big-pipe-placeholder-id="', $html);
     array_shift($fragments);
     $order = [];
 
diff --git a/src/Render/Placeholder/BigPipeStrategy.php b/src/Render/Placeholder/BigPipeStrategy.php
index c98ea3d..bf4ea76 100644
--- a/src/Render/Placeholder/BigPipeStrategy.php
+++ b/src/Render/Placeholder/BigPipeStrategy.php
@@ -132,13 +132,13 @@ class BigPipeStrategy implements PlaceholderStrategyInterface {
       // @see \Drupal\Core\Form\FormBuilder::renderFormTokenPlaceholder()
       // @see \Drupal\Core\Form\FormBuilder::renderPlaceholderFormAction()
       if ($placeholder[0] !== '<' || $placeholder !== Html::normalize($placeholder)) {
-        $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements);
+        $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements, TRUE);
       }
       else {
         // If the current request/session doesn't have JavaScript, fall back to
         // no-JS BigPipe.
         if ($this->requestStack->getCurrentRequest()->cookies->has(static::NOJS_COOKIE)) {
-          $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements);
+          $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements, FALSE);
         }
         else {
           $overridden_placeholders[$placeholder] = static::createBigPipeJsPlaceholder($placeholder, $placeholder_elements);
@@ -162,24 +162,10 @@ class BigPipeStrategy implements PlaceholderStrategyInterface {
    *   The resulting BigPipe JS placeholder render array.
    */
   protected static function createBigPipeJsPlaceholder($original_placeholder, array $placeholder_render_array) {
-    // Generate a BigPipe selector (to be used by BigPipe's JavaScript).
-    // @see \Drupal\Core\Render\PlaceholderGenerator::createPlaceholder()
-    if (isset($placeholder_render_array['#lazy_builder'])) {
-      $callback = $placeholder_render_array['#lazy_builder'][0];
-      $arguments = $placeholder_render_array['#lazy_builder'][1];
-      $token = hash('crc32b', serialize($placeholder_render_array));
-      $big_pipe_js_selector = UrlHelper::buildQuery(['callback' => $callback, 'args' => $arguments, 'token' => $token]);
-    }
-    // When the placeholder's render array is not using a #lazy_builder,
-    // anything could be in there: only #lazy_builder has a strict contract that
-    // allows us to create a more sane selector. Therefore, simply the original
-    // placeholder into a usable selector, at the cost of it being obtuse.
-    else {
-      $big_pipe_js_selector = Html::getId($original_placeholder);
-    }
+    $big_pipe_placeholder_id = static::generateBigPipePlaceholderId($original_placeholder, $placeholder_render_array);
 
     return [
-      '#markup' => '<div data-big-pipe-selector="' . Html::escape($big_pipe_js_selector) . '"></div>',
+      '#markup' => '<div data-big-pipe-placeholder-id="' . Html::escape($big_pipe_placeholder_id) . '"></div>',
       '#cache' => [
         'max-age' => 0,
         'contexts' => [
@@ -190,12 +176,12 @@ class BigPipeStrategy implements PlaceholderStrategyInterface {
         'library' => [
           'big_pipe/big_pipe',
         ],
-        // Inform BigPipe' JavaScript known BigPipe placeholders (a whitelist).
+        // Inform BigPipe' JavaScript known BigPipe placeholder IDs (a whitelist).
         'drupalSettings' => [
-          'bigPipePlaceholders' => [$big_pipe_js_selector => TRUE],
+          'bigPipePlaceholderIds' => [$big_pipe_placeholder_id => TRUE],
         ],
         'big_pipe_placeholders' => [
-          Html::escape($big_pipe_js_selector) => $placeholder_render_array,
+          Html::escape($big_pipe_placeholder_id) => $placeholder_render_array,
         ],
       ],
     ];
@@ -208,17 +194,23 @@ class BigPipeStrategy implements PlaceholderStrategyInterface {
    *   The original placeholder.
    * @param array $placeholder_render_array
    *   The render array for a placeholder.
+   * @param bool $placeholder_must_be_attribute_safe
+   *   Whether the placeholder must be safe for use in a HTML attribute (in case
+   *   it's a placeholder for a HTML attribute value or a subset of it).
    *
    * @return array
    *   The resulting BigPipe no-JS placeholder render array.
-   *
-   * @todo Figure out how to simplify this. Perhaps no new placeholder is in fact necessary?
-   * @todo Related, perhaps distinguish between "HTML" and "non-HTML (attr value)" use cases? Because right now, this *breaks* HTML and therefore breaks response filters: this indiscriminately uses a <div> as a placeholder, which is invalid inside a HTML attribute, and thus breaks DOM parsing.
    */
-  protected static function createBigPipeNoJsPlaceholder($original_placeholder, array $placeholder_render_array) {
-    $html_placeholder = Html::getId($original_placeholder);
+  protected static function createBigPipeNoJsPlaceholder($original_placeholder, array $placeholder_render_array, $placeholder_must_be_attribute_safe = FALSE) {
+    if (!$placeholder_must_be_attribute_safe) {
+      $big_pipe_placeholder = '<div data-big-pipe-nojs-placeholder-id="' . Html::escape(static::generateBigPipePlaceholderId($original_placeholder, $placeholder_render_array))  . '"></div>';
+    }
+    else {
+      $big_pipe_placeholder = 'big_pipe_nojs_placeholder_attribute_safe:' . Html::escape($original_placeholder);
+    }
+
     return [
-      '#markup' => '<div data-big-pipe-selector-nojs="' . $html_placeholder . '"></div>',
+      '#markup' => $big_pipe_placeholder,
       '#cache' => [
         'max-age' => 0,
         'contexts' => [
@@ -227,10 +219,39 @@ class BigPipeStrategy implements PlaceholderStrategyInterface {
       ],
       '#attached' => [
         'big_pipe_nojs_placeholders' => [
-          $html_placeholder => $placeholder_render_array,
+          $big_pipe_placeholder => $placeholder_render_array,
         ],
       ],
     ];
   }
 
+  /**
+   * Generates a BigPipe placeholder ID.
+   *
+   * @param string $original_placeholder
+   *   The original placeholder.
+   * @param array $placeholder_render_array
+   *   The render array for a placeholder.
+   *
+   * @return string
+   *   The generated BigPipe placeholder ID.
+   */
+  protected static function generateBigPipePlaceholderId($original_placeholder, array $placeholder_render_array) {
+    // Generate a BigPipe placeholder ID (to be used by BigPipe's JavaScript).
+    // @see \Drupal\Core\Render\PlaceholderGenerator::createPlaceholder()
+    if (isset($placeholder_render_array['#lazy_builder'])) {
+      $callback = $placeholder_render_array['#lazy_builder'][0];
+      $arguments = $placeholder_render_array['#lazy_builder'][1];
+      $token = hash('crc32b', serialize($placeholder_render_array));
+      return UrlHelper::buildQuery(['callback' => $callback, 'args' => $arguments, 'token' => $token]);
+    }
+    // When the placeholder's render array is not using a #lazy_builder,
+    // anything could be in there: only #lazy_builder has a strict contract that
+    // allows us to create a more sane selector. Therefore, simply the original
+    // placeholder into a usable placeholder ID, at the cost of it being obtuse.
+    else {
+      return Html::getId($original_placeholder);
+    }
+  }
+
 }
diff --git a/src/Tests/Render/BigPipeResponseAttachmentsProcessorTest.php b/src/Tests/Render/BigPipeResponseAttachmentsProcessorTest.php
new file mode 100644
index 0000000..726783b
--- /dev/null
+++ b/src/Tests/Render/BigPipeResponseAttachmentsProcessorTest.php
@@ -0,0 +1,151 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\big_pipeTests\Render\BigPipeResponseAttachmentsProcessorTest.
+ */
+
+namespace Drupal\big_pipeTests\Render;
+
+use Drupal\big_pipe\Render\BigPipeResponse;
+use Drupal\big_pipe\Render\BigPipeResponseAttachmentsProcessor;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Asset\AssetCollectionRendererInterface;
+use Drupal\Core\Asset\AssetResolverInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Render\AttachmentsInterface;
+use Drupal\Core\Render\AttachmentsResponseProcessorInterface;
+use Drupal\Core\Render\HtmlResponse;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * @coversDefaultClass \Drupal\big_pipe\Render\BigPipeResponseAttachmentsProcessor
+ * @group big_pipe
+ */
+class BigPipeResponseAttachmentsProcessorTest extends UnitTestCase {
+
+  /**
+   * @covers ::processAttachments
+   *
+   * @dataProvider nonHtmlResponseProvider
+   *
+   * @expectedException \InvalidArgumentException
+   */
+  public function testNonHtmlResponse($response_class) {
+    $big_pipe_response_attachments_processor = new BigPipeResponseAttachmentsProcessor(
+      $this->prophesize(AttachmentsResponseProcessorInterface::class)->reveal(),
+      $this->prophesize(AssetResolverInterface::class)->reveal(),
+      $this->prophesize(ConfigFactoryInterface::class)->reveal(),
+      $this->prophesize(AssetCollectionRendererInterface::class)->reveal(),
+      $this->prophesize(AssetCollectionRendererInterface::class)->reveal(),
+      $this->prophesize(RequestStack::class)->reveal(),
+      $this->prophesize(RendererInterface::class)->reveal(),
+      $this->prophesize(ModuleHandlerInterface::class)->reveal()
+    );
+
+    $non_html_response = new $response_class();
+    $big_pipe_response_attachments_processor->processAttachments($non_html_response);
+  }
+
+  function nonHtmlResponseProvider() {
+    return [
+      'AjaxResponse, which implements AttachmentsInterface' => [AjaxResponse::class],
+      'A dummy that implements AttachmentsInterface' => [get_class($this->prophesize(AttachmentsInterface::class)->reveal())],
+    ];
+  }
+
+  /**
+   * @covers ::processAttachments
+   *
+   * @dataProvider attachmentsProvider
+   */
+  public function testHtmlResponse(array $attachments) {
+    $big_pipe_response = new BigPipeResponse('original');
+    $big_pipe_response->setAttachments($attachments);
+
+    // This mock is the main expectation of this test: verify that the decorated
+    // service (that is this mock) never receives BigPipe placeholder
+    // attachments, because it doesn't know (nor should it) how to handle them.
+    $html_response_attachments_processor = $this->prophesize(AttachmentsResponseProcessorInterface::class);
+    $html_response_attachments_processor->processAttachments(Argument::that(function ($response) {
+      return $response instanceof HtmlResponse && empty(array_intersect(['big_pipe_placeholders', 'big_pipe_nojs_placeholders'], array_keys($response->getAttachments())));
+    }))
+      ->will(function ($args) {
+        $response = $args[0];
+        // Simulate its actual behavior.
+        $attachments = array_diff_key($response->getAttachments(), ['html_response_attachment_placeholders' => TRUE]);
+        $response->setContent('processed');
+        $response->setAttachments($attachments);
+        return $response;
+      })
+      ->shouldBeCalled();
+
+    $big_pipe_response_attachments_processor = new BigPipeResponseAttachmentsProcessor(
+      $html_response_attachments_processor->reveal(),
+      $this->prophesize(AssetResolverInterface::class)->reveal(),
+      $this->prophesize(ConfigFactoryInterface::class)->reveal(),
+      $this->prophesize(AssetCollectionRendererInterface::class)->reveal(),
+      $this->prophesize(AssetCollectionRendererInterface::class)->reveal(),
+      $this->prophesize(RequestStack::class)->reveal(),
+      $this->prophesize(RendererInterface::class)->reveal(),
+      $this->prophesize(ModuleHandlerInterface::class)->reveal()
+    );
+    $processed_big_pipe_response = $big_pipe_response_attachments_processor->processAttachments($big_pipe_response);
+
+    // The secondary expectation of this test: the original (passed in) response
+    // object remains unchanged, the processed (returned) response object has
+    // the expected values.
+    $this->assertSame($attachments, $big_pipe_response->getAttachments(), 'Attachments of original response object MUST NOT be changed.');
+    $this->assertEquals('original', $big_pipe_response->getContent(), 'Content of original response object MUST NOT be changed.');
+    $this->assertEquals(array_diff_key($attachments, ['html_response_attachment_placeholders' => TRUE]), $processed_big_pipe_response->getAttachments(), 'Attachments of returned (processed) response object MUST be changed.');
+    $this->assertEquals('processed', $processed_big_pipe_response->getContent(), 'Content of returned (processed) response object MUST be changed.');
+  }
+
+  public function attachmentsProvider() {
+    $typical_cases = [
+      'no attachments' => [
+        [],
+        [],
+      ],
+      'libraries' => [
+        ['library' => ['core/drupal']],
+        ['library' => ['core/drupal']],
+      ],
+      'libraries + drupalSettings' => [
+        ['library' => ['core/drupal'], 'drupalSettings' => ['foo' => 'bar']],
+        ['library' => ['core/drupal'], 'drupalSettings' => ['foo' => 'bar']],
+      ],
+    ];
+
+    $official_attachment_types = ['html_head', 'feed', 'html_head_link', 'http_header', 'library', 'placeholders', 'drupalSettings', 'html_response_attachment_placeholders'];
+    $official_attachments_with_random_values = [];
+    foreach ($official_attachment_types as $type) {
+      $official_attachments_with_random_values[$type] = $this->randomMachineName();
+    }
+    $random_attachments = ['random' . $this->randomMachineName() => $this->randomMachineName()];
+    $edge_cases = [
+      'all official attachment types, with random assigned values, even if technically not valid, to prove BigPipeResponseAttachmentsProcessor is a perfect decorator' => [$official_attachments_with_random_values],
+      'random attachment type (unofficial), with random assigned value, to prove BigPipeResponseAttachmentsProcessor is a perfect decorator' => [$random_attachments],
+    ];
+
+    $big_pipe_placeholder_attachments = ['big_pipe_placeholders' => $this->randomMachineName()];
+    $big_pipe_nojs_placeholder_attachments = ['big_pipe_nojs_placeholders' => $this->randomMachineName()];
+    $big_pipe_cases = [
+      'only big_pipe_placeholders' => [$big_pipe_placeholder_attachments],
+      'only big_pipe_nojs_placeholders' => [$big_pipe_nojs_placeholder_attachments],
+      'big_pipe_placeholders + big_pipe_nojs_placeholders' => [$big_pipe_placeholder_attachments + $big_pipe_nojs_placeholder_attachments],
+    ];
+
+    $combined_cases = [
+      'all official attachment types + big_pipe_placeholders + big_pipe_nojs_placeholders' => [$official_attachments_with_random_values + $big_pipe_placeholder_attachments + $big_pipe_nojs_placeholder_attachments],
+      'random attachment types + big_pipe_placeholders + big_pipe_nojs_placeholders' => [$random_attachments + $big_pipe_placeholder_attachments + $big_pipe_nojs_placeholder_attachments],
+    ];
+
+    return $typical_cases + $edge_cases + $big_pipe_cases + $combined_cases;
+  }
+
+}
diff --git a/tests/src/Unit/Render/Placeholder/BigPipeStrategyTest.php b/tests/src/Unit/Render/Placeholder/BigPipeStrategyTest.php
new file mode 100644
index 0000000..0ff77fd
--- /dev/null
+++ b/tests/src/Unit/Render/Placeholder/BigPipeStrategyTest.php
@@ -0,0 +1,201 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\big_pipe\Unit\Render\Placeholder\BigPipeStrategyTest.
+ */
+
+namespace Drupal\Tests\big_pipe\Unit\Render\Placeholder;
+
+use Drupal\big_pipe\Render\Placeholder\BigPipeStrategy;
+use Drupal\Core\Session\SessionConfigurationInterface;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * @coversDefaultClass \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy
+ * @group big_pipe
+ */
+class BigPipeStrategyTest extends UnitTestCase {
+
+  /**
+   * @covers ::processPlaceholders
+   *
+   * @dataProvider placeholdersProvider
+   */
+  public function testProcessPlaceholders(array $placeholders, $request_has_session, $request_has_big_pipe_nojs_cookie, array $expected_big_pipe_placeholders) {
+    $request = new Request();
+    if ($request_has_big_pipe_nojs_cookie) {
+      $request->cookies->set(BigPipeStrategy::NOJS_COOKIE, 1);
+    }
+    $request_stack = $this->prophesize(RequestStack::class);
+    $request_stack->getCurrentRequest()
+      ->willReturn($request)
+      ->shouldBeCalled();
+
+    $session_configuration = $this->prophesize(SessionConfigurationInterface::class);
+    $session_configuration->hasSession(Argument::type(Request::class))
+      ->willReturn($request_has_session)
+      ->shouldBeCalled();
+
+    $big_pipe_strategy = new BigPipeStrategy($session_configuration->reveal(), $request_stack->reveal());
+    $processed_placeholders = $big_pipe_strategy->processPlaceholders($placeholders);
+
+    if ($request_has_session) {
+      $this->assertSameSize($placeholders, $processed_placeholders, 'BigPipe is able to deliver all placeholders.');
+      foreach (array_keys($placeholders) as $placeholder) {
+        $this->assertSame($expected_big_pipe_placeholders[$placeholder], $processed_placeholders[$placeholder], "Verifying how BigPipeStrategy handles the placeholder '$placeholder'");
+      }
+    }
+    else {
+      $this->assertSame(0, count($processed_placeholders));
+    }
+  }
+
+  public function placeholdersProvider() {
+    // Define the two types of cacheability that we expect to see. These will be
+    // used in the expectations.
+    $cacheability_depends_on_session_only = [
+      'max-age' => 0,
+      'contexts' => ['session.exists'],
+    ];
+    $cacheability_depends_on_session_and_nojs_cookie = [
+      'max-age' => 0,
+      'contexts' => ['session.exists', 'cookies:big_pipe_nojs'],
+    ];
+
+    // Real-world example of HTML placeholder.
+    $messages_placeholder_markup = '<drupal-render-placeholder callback="Drupal\Core\Render\Element\StatusMessages::renderMessages" arguments="0" token="a8c34b5e"></drupal-render-placeholder>';
+    $placeholders[$messages_placeholder_markup] = [
+      '#lazy_builder' => [
+        'Drupal\Core\Render\Element\StatusMessages::renderMessages',
+        [NULL]
+      ],
+    ];
+    $messages_big_pipe_placeholder = [
+      '#markup' => '<div data-big-pipe-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args[0]&amp;token=a8c34b5e"></div>',
+      '#cache' => $cacheability_depends_on_session_and_nojs_cookie,
+      '#attached' => [
+        'library' => ['big_pipe/big_pipe'],
+        'drupalSettings' => [
+          'bigPipePlaceholderIds' => [
+            'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=a8c34b5e' => TRUE,
+          ],
+        ],
+        'big_pipe_placeholders' => [
+          'callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args[0]&amp;token=a8c34b5e' => $placeholders[$messages_placeholder_markup],
+        ],
+      ],
+    ];
+    $messages_big_pipe_nojs_placeholder = [
+      '#markup' => '<div data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args[0]&amp;token=a8c34b5e"></div>',
+      '#cache' => $cacheability_depends_on_session_and_nojs_cookie,
+      '#attached' => [
+        'big_pipe_nojs_placeholders' => [
+          '<div data-big-pipe-nojs-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&amp;args[0]&amp;token=a8c34b5e"></div>' => $placeholders[$messages_placeholder_markup],
+        ],
+      ],
+    ];
+
+    // Real-world example of HTML attribute value placeholder.
+    $placeholders['form_action_cc611e1d'] = [
+      '#lazy_builder' => ['form_builder:renderPlaceholderFormAction', []],
+    ];
+    $form_action_big_pipe_nojs_placeholder = [
+      '#markup' => 'big_pipe_nojs_placeholder_attribute_safe:form_action_cc611e1d',
+      '#cache' => $cacheability_depends_on_session_only,
+      '#attached' => [
+        'big_pipe_nojs_placeholders' => [
+          'big_pipe_nojs_placeholder_attribute_safe:form_action_cc611e1d' => $placeholders['form_action_cc611e1d'],
+        ],
+      ],
+    ];
+
+    // Real-world example of HTML attribute value subset placeholder.
+    $placeholders['22dcc695f4a22e809a62d8eb7addd6fccecfbf31'] = [
+      '#lazy_builder' => [
+        'route_processor_csrf:renderPlaceholderCsrfToken',
+        ['admin/config/user-interface/shortcut/manage/default/add-link-inline']
+      ],
+    ];
+    $csrf_token_big_pipe_nojs_placeholder = [
+      '#markup' => 'big_pipe_nojs_placeholder_attribute_safe:22dcc695f4a22e809a62d8eb7addd6fccecfbf31',
+      '#cache' => $cacheability_depends_on_session_only,
+      '#attached' => [
+        'big_pipe_nojs_placeholders' => [
+          'big_pipe_nojs_placeholder_attribute_safe:22dcc695f4a22e809a62d8eb7addd6fccecfbf31' => $placeholders['22dcc695f4a22e809a62d8eb7addd6fccecfbf31'],
+        ],
+      ],
+    ];
+
+    // Edge case: custom string to be considered as a placeholder that happens
+    // to not be valid HTML.
+    $placeholders['<hello'] = [
+      '#lazy_builder' => [
+        'hello_or_yarhar',
+        []
+      ],
+    ];
+    $hello_big_pipe_nojs_placeholder = [
+      '#markup' => 'big_pipe_nojs_placeholder_attribute_safe:&lt;hello',
+      '#cache' => $cacheability_depends_on_session_only,
+      '#attached' => [
+        'big_pipe_nojs_placeholders' => [
+          'big_pipe_nojs_placeholder_attribute_safe:&lt;hello' => $placeholders['<hello'],
+        ],
+      ],
+    ];
+
+    // Edge case: non-#lazy_builder placeholder.
+    $placeholders['<time>CURRENT TIME</time>'] = [
+      '#pre_render' => ['current_time'],
+    ];
+    $current_time_big_pipe_placeholder = [
+      '#markup' => '<div data-big-pipe-placeholder-id="timecurrent-timetime"></div>',
+      '#cache' => $cacheability_depends_on_session_and_nojs_cookie,
+      '#attached' => [
+        'library' => ['big_pipe/big_pipe'],
+        'drupalSettings' => [
+          'bigPipePlaceholderIds' => [
+            'timecurrent-timetime' => TRUE,
+          ],
+        ],
+        'big_pipe_placeholders' => [
+          'timecurrent-timetime' => $placeholders['<time>CURRENT TIME</time>'],
+        ],
+      ],
+    ];
+    $current_time_big_pipe_nojs_placeholder = [
+      '#markup' => '<div data-big-pipe-nojs-placeholder-id="timecurrent-timetime"></div>',
+      '#cache' => $cacheability_depends_on_session_and_nojs_cookie,
+      '#attached' => [
+        'big_pipe_nojs_placeholders' => [
+          '<div data-big-pipe-nojs-placeholder-id="timecurrent-timetime"></div>' => $placeholders['<time>CURRENT TIME</time>'],
+        ],
+      ],
+    ];
+
+
+    return [
+      'no session, no-JS cookie absent' => [$placeholders, FALSE, FALSE, []],
+      'no session, no-JS cookie present' => [$placeholders, FALSE, TRUE, []],
+      'session, no-JS cookie absent: (JS-powered) BigPipe placeholder used for HTML placeholders' => [$placeholders, TRUE, FALSE, [
+        $messages_placeholder_markup => $messages_big_pipe_placeholder,
+        'form_action_cc611e1d' => $form_action_big_pipe_nojs_placeholder,
+        '22dcc695f4a22e809a62d8eb7addd6fccecfbf31' => $csrf_token_big_pipe_nojs_placeholder,
+        '<hello' => $hello_big_pipe_nojs_placeholder,
+        '<time>CURRENT TIME</time>' => $current_time_big_pipe_placeholder,
+      ]],
+      'session, no-JS cookie present: no-JS BigPipe placeholder used for HTML placeholders' => [$placeholders, TRUE, TRUE, [
+        $messages_placeholder_markup => $messages_big_pipe_nojs_placeholder,
+        'form_action_cc611e1d' => $form_action_big_pipe_nojs_placeholder,
+        '22dcc695f4a22e809a62d8eb7addd6fccecfbf31' => $csrf_token_big_pipe_nojs_placeholder,
+        '<hello' => $hello_big_pipe_nojs_placeholder,
+        '<time>CURRENT TIME</time>' => $current_time_big_pipe_nojs_placeholder,
+      ]],
+    ];
+  }
+
+}
