From 2250c09addf51b6c042ccc46fb9fc92ac5dfdff9 Mon Sep 17 00:00:00 2001
From: webchick <webchick@24967.no-reply.drupal.org>
Date: Sun, 4 May 2014 00:07:44 -0700
Subject: [PATCH] Issue #1939008 by sun, joelpittet, gnuget, Gokul N K, sphism,
 drupalninja99, c4rl, Cottser, mdrummond, long wave, steveoliver, andypost,
 Fabianx | jenlampton: Convert theme_table() to Twig.

---
 core/includes/theme.inc                       | 164 ++++++++----------
 .../Tests/NodeTypeRenameConfigImportTest.php  |  32 +++-
 .../Drupal/system/Tests/Theme/TableTest.php   |  52 ++++--
 core/modules/system/templates/table.html.twig |  81 +++++++++
 4 files changed, 220 insertions(+), 109 deletions(-)
 create mode 100644 core/modules/system/templates/table.html.twig

diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index b62ad74d6ce5..654803cf3b3e 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -1411,9 +1411,11 @@ function drupal_pre_render_table(array $element) {
 }
 
 /**
- * Returns HTML for a table.
+ * Prepares variables for table templates.
  *
- * @param $variables
+ * Default template: table.html.twig.
+ *
+ * @param array $variables
  *   An associative array containing:
  *   - header: An array containing the table headers. Each element of the array
  *     can be either a localized string or an associative array with the
@@ -1495,83 +1497,66 @@ function drupal_pre_render_table(array $element) {
  *   - empty: The message to display in an extra row if table does not have any
  *     rows.
  */
-function theme_table($variables) {
-  $header = $variables['header'];
-  $rows = $variables['rows'];
-  $attributes = $variables['attributes'];
-  $caption = $variables['caption'];
-  $colgroups = $variables['colgroups'];
-  $sticky = $variables['sticky'];
-  $responsive = $variables['responsive'];
-  $empty = $variables['empty'];
-
-  $output = '<table' . new Attribute($attributes) . ">\n";
-
-  if (isset($caption)) {
-    $output .= '<caption>' . $caption . "</caption>\n";
-  }
+function template_preprocess_table(&$variables) {
+  $is_sticky = !empty($variables['sticky']);
+  $is_responsive = !empty($variables['responsive']);
 
   // Format the table columns:
-  if (count($colgroups)) {
-    foreach ($colgroups as $colgroup) {
-      $attributes = array();
-
+  if (!empty($variables['colgroups'])) {
+    foreach ($variables['colgroups'] as &$colgroup) {
       // Check if we're dealing with a simple or complex column
       if (isset($colgroup['data'])) {
-        foreach ($colgroup as $key => $value) {
-          if ($key == 'data') {
-            $cols = $value;
-          }
-          else {
-            $attributes[$key] = $value;
-          }
-        }
+        $cols = $colgroup['data'];
+        unset($colgroup['data']);
+        $colgroup_attributes = $colgroup;
       }
       else {
         $cols = $colgroup;
+        $colgroup_attributes = array();
       }
-
-      // Build colgroup
-      if (is_array($cols) && count($cols)) {
-        $output .= ' <colgroup' . new Attribute($attributes) . '>';
-        foreach ($cols as $col) {
-          $output .= ' <col' . new Attribute($col) . ' />';
+      $colgroup = array();
+      $colgroup['attributes'] = new Attribute($colgroup_attributes);
+      $colgroup['cols'] = array();
+
+      // Build columns.
+      if (is_array($cols) && !empty($cols)) {
+        foreach ($cols as $col_key => $col) {
+          $colgroup['cols'][$col_key]['attributes'] = new Attribute($col);
         }
-        $output .= " </colgroup>\n";
-      }
-      else {
-        $output .= ' <colgroup' . new Attribute($attributes) . " />\n";
       }
     }
   }
 
   // Add the 'empty' row message if available.
-  if (!count($rows) && $empty) {
+  if (empty($variables['rows']) && isset($variables['empty'])) {
     $header_count = 0;
-    foreach ($header as $header_cell) {
-      if (is_array($header_cell)) {
-        $header_count += isset($header_cell['colspan']) ? $header_cell['colspan'] : 1;
+    foreach ($variables['header'] as $header_cell) {
+      if (is_array($header_cell) && isset($header_cell['colspan'])) {
+        $header_count += $header_cell['colspan'];
       }
       else {
         $header_count++;
       }
     }
-    $rows[] = array(array('data' => $empty, 'colspan' => $header_count, 'class' => array('empty', 'message')));
+    $variables['rows'][] = array(array(
+      'data' => $variables['empty'],
+      'colspan' => $header_count,
+      'class' => array('empty', 'message'),
+    ));
   }
 
-  $responsive = array();
+  // Build an associative array of responsive classes keyed by column.
+  $responsive_classes = array();
+
   // Format the table header:
-  if (count($header)) {
-    $ts = tablesort_init($header);
-    // HTML requires that the thead tag has tr tags in it followed by tbody
-    // tags. Using ternary operator to check and see if we have any rows.
-    $output .= (count($rows) ? ' <thead><tr>' : ' <tr>');
-    $i = 0;
-    foreach ($header as $cell) {
-      $i++;
+  $ts = array();
+  if (!empty($variables['header'])) {
+    $ts = tablesort_init($variables['header']);
+
+    foreach ($variables['header'] as $col_key => $cell) {
       if (!is_array($cell)) {
         $cell_content = $cell;
-        $cell_attributes = '';
+        $cell_attributes = new Attribute();
         $is_header = TRUE;
       }
       else {
@@ -1590,10 +1575,10 @@ function theme_table($variables) {
         // must be transferred to the content cells.
         if (!empty($cell['class']) && is_array($cell['class'])) {
           if (in_array(RESPONSIVE_PRIORITY_MEDIUM, $cell['class'])) {
-            $responsive[$i] = RESPONSIVE_PRIORITY_MEDIUM;
+            $responsive_classes[$col_key] = RESPONSIVE_PRIORITY_MEDIUM;
           }
           elseif (in_array(RESPONSIVE_PRIORITY_LOW, $cell['class'])) {
-            $responsive[$i] = RESPONSIVE_PRIORITY_LOW;
+            $responsive_classes[$col_key] = RESPONSIVE_PRIORITY_LOW;
           }
         }
 
@@ -1601,54 +1586,50 @@ function theme_table($variables) {
           $cell_content = drupal_render($cell_content);
         }
 
-        tablesort_header($cell_content, $cell, $header, $ts);
+        tablesort_header($cell_content, $cell, $variables['header'], $ts);
 
         // tablesort_header() removes the 'sort' and 'field' keys.
         $cell_attributes = new Attribute($cell);
       }
-      $cell_tag = $is_header ? 'th' : 'td';
-      $output .= '<' . $cell_tag . $cell_attributes . '>' . $cell_content . '</' . $cell_tag . '>';
+      $variables['header'][$col_key] = array();
+      $variables['header'][$col_key]['tag'] = $is_header ? 'th' : 'td';
+      $variables['header'][$col_key]['attributes'] = $cell_attributes;
+      $variables['header'][$col_key]['content'] = $cell_content;
     }
-    // Using ternary operator to close the tags based on whether or not there are rows
-    $output .= (count($rows) ? " </tr></thead>\n" : "</tr>\n");
-  }
-  else {
-    $ts = array();
   }
 
-  // Format the table rows:
-  if (count($rows)) {
-    $output .= "<tbody>\n";
+  if (!empty($variables['rows'])) {
     $flip = array('even' => 'odd', 'odd' => 'even');
     $class = 'even';
-    foreach ($rows as $row) {
+    foreach ($variables['rows'] as $row_key => $row) {
       // Check if we're dealing with a simple or complex row
       if (isset($row['data'])) {
         $cells = $row['data'];
         $no_striping = isset($row['no_striping']) ? $row['no_striping'] : FALSE;
 
         // Set the attributes array and exclude 'data' and 'no_striping'.
-        $attributes = $row;
-        unset($attributes['data']);
-        unset($attributes['no_striping']);
+        $row_attributes = $row;
+        unset($row_attributes['data']);
+        unset($row_attributes['no_striping']);
       }
       else {
         $cells = $row;
-        $attributes = array();
+        $row_attributes = array();
         $no_striping = FALSE;
       }
-      if (count($cells)) {
-        // Add odd/even class
-        if (!$no_striping) {
-          $class = $flip[$class];
-          $attributes['class'][] = $class;
-        }
 
-        // Build row
-        $output .= ' <tr' . new Attribute($attributes) . '>';
-        $i = 0;
-        foreach ($cells as $cell) {
-          $i++;
+      // Add odd/even class.
+      if (!$no_striping) {
+        $class = $flip[$class];
+        $row_attributes['class'][] = $class;
+      }
+
+      // Build row.
+      $variables['rows'][$row_key] = array();
+      $variables['rows'][$row_key]['attributes'] = new Attribute($row_attributes);
+      $variables['rows'][$row_key]['cells'] = array();
+      if (!empty($cells)) {
+        foreach ($cells as $col_key => $cell) {
           if (!is_array($cell)) {
             $cell_content = $cell;
             $cell_attributes = array();
@@ -1671,26 +1652,22 @@ function theme_table($variables) {
             }
           }
           // Add active class if needed for sortable tables.
-          if (isset($header[$i]['data']) && $header[$i]['data'] == $ts['name'] && !empty($header[$i]['field'])) {
+          if (isset($variables['header'][$col_key]['data']) && $variables['header'][$col_key]['data'] == $ts['name'] && !empty($variables['header'][$col_key]['field'])) {
             $cell_attributes['class'][] = 'active';
           }
           // Copy RESPONSIVE_PRIORITY_LOW/RESPONSIVE_PRIORITY_MEDIUM
           // class from header to cell as needed.
-          if (isset($responsive[$i])) {
-            $cell_attributes['class'][] = $responsive[$i];
+          if (isset($responsive_classes[$col_key])) {
+            $cell_attributes['class'][] = $responsive_classes[$col_key];
           }
 
-          $cell_tag = $is_header ? 'th' : 'td';
-          $output .= '<' . $cell_tag . new Attribute($cell_attributes) . '>' . $cell_content . '</' . $cell_tag . '>';
+          $variables['rows'][$row_key]['cells'][$col_key]['tag'] = $is_header ? 'th' : 'td';
+          $variables['rows'][$row_key]['cells'][$col_key]['attributes'] = new Attribute($cell_attributes);
+          $variables['rows'][$row_key]['cells'][$col_key]['content'] = $cell_content;
         }
-        $output .= " </tr>\n";
       }
     }
-    $output .= "</tbody>\n";
   }
-
-  $output .= "</table>\n";
-  return $output;
 }
 
 /**
@@ -2577,6 +2554,7 @@ function drupal_common_theme() {
     ),
     'table' => array(
       'variables' => array('header' => NULL, 'rows' => NULL, 'attributes' => array(), 'caption' => NULL, 'colgroups' => array(), 'sticky' => FALSE, 'responsive' => TRUE, 'empty' => ''),
+      'template' => 'table',
     ),
     'tablesort_indicator' => array(
       'variables' => array('style' => NULL),
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeTypeRenameConfigImportTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeTypeRenameConfigImportTest.php
index bd2ba66d09c7..5a189abb464c 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeTypeRenameConfigImportTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeTypeRenameConfigImportTest.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\String;
 use Drupal\Component\Utility\Unicode;
+use Drupal\Component\Utility\Xss;
 use Drupal\Core\Config\Entity\ConfigEntityStorage;
 use Drupal\simpletest\WebTestBase;
 
@@ -106,8 +107,12 @@ public function testConfigurationRename() {
       $entity_type = \Drupal::entityManager()->getDefinition($config_entity_type);
       $old_id = ConfigEntityStorage::getIDFromConfigName($names['old_name'], $entity_type->getConfigPrefix());
       $new_id = ConfigEntityStorage::getIDFromConfigName($names['new_name'], $entity_type->getConfigPrefix());
-      $this->assertText('-' . $entity_type->getKey('id') . ': ' . $old_id);
-      $this->assertText('+' . $entity_type->getKey('id') . ': ' . $new_id);
+
+      $id_key = $entity_type->getKey('id');
+      $text = "$id_key: $old_id";
+      $this->assertTextPattern('/\-\s+' . preg_quote($text, '/') . '/', "'-$text' found.");
+      $text = "$id_key: $new_id";
+      $this->assertTextPattern('/\+\s+' . preg_quote($text, '/') . '/', "'+$text' found.");
     }
 
     // Run the import.
@@ -119,4 +124,27 @@ public function testConfigurationRename() {
     $this->assertIdentical($staged_type, $content_type->type);
   }
 
+  /**
+   * Asserts that a Perl regex pattern is found in the text content.
+   *
+   * @param string $pattern
+   *   Perl regex to look for including the regex delimiters.
+   * @param string $message
+   *   (optional) A message to display with the assertion.
+   *
+   * @return bool
+   *   TRUE on pass, FALSE on failure.
+   */
+  protected function assertTextPattern($pattern, $message = NULL) {
+    // @see WebTestBase::assertTextHelper()
+    if ($this->plainTextContent === FALSE) {
+      $this->plainTextContent = Xss::filter($this->drupalGetContent(), array());
+    }
+    // @see WebTestBase::assertPattern()
+    if (!$message) {
+      $message = String::format('Pattern "@pattern" found', array('@pattern' => $pattern));
+    }
+    return $this->assert((bool) preg_match($pattern, $this->plainTextContent), $message);
+  }
+
 }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/TableTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/TableTest.php
index 26a0fd8292b2..6b9c5ef95cc6 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Theme/TableTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Theme/TableTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Tests\Theme;
 
+use Drupal\Component\Utility\String;
 use Drupal\simpletest\DrupalUnitTestBase;
 
 /**
@@ -41,10 +42,10 @@ function testThemeTableStickyHeaders() {
       '#rows' => $rows,
       '#sticky' => TRUE,
     );
-    $this->content = drupal_render($table);
+    $this->render($table);
     $js = _drupal_add_js();
-    $this->assertTrue(isset($js['core/misc/tableheader.js']), 'tableheader.js was included when $sticky = TRUE.');
-    $this->assertRaw('sticky-enabled',  'Table has a class of sticky-enabled when $sticky = TRUE.');
+    $this->assertTrue(isset($js['core/misc/tableheader.js']), 'tableheader.js found.');
+    $this->assertRaw('sticky-enabled');
     drupal_static_reset('_drupal_add_js');
   }
 
@@ -66,10 +67,10 @@ function testThemeTableNoStickyHeaders() {
       '#colgroups' => $colgroups,
       '#sticky' => FALSE,
     );
-    $this->content = drupal_render($table);
+    $this->render($table);
     $js = _drupal_add_js();
-    $this->assertFalse(isset($js['core/misc/tableheader.js']), 'tableheader.js was not included because $sticky = FALSE.');
-    $this->assertNoRaw('sticky-enabled',  'Table does not have a class of sticky-enabled because $sticky = FALSE.');
+    $this->assertFalse(isset($js['core/misc/tableheader.js']), 'tableheader.js not found.');
+    $this->assertNoRaw('sticky-enabled');
     drupal_static_reset('_drupal_add_js');
   }
 
@@ -79,9 +80,9 @@ function testThemeTableNoStickyHeaders() {
    */
   function testThemeTableWithEmptyMessage() {
     $header = array(
-      t('Header 1'),
+      'Header 1',
       array(
-        'data' => t('Header 2'),
+        'data' => 'Header 2',
         'colspan' => 2,
       ),
     );
@@ -89,11 +90,12 @@ function testThemeTableWithEmptyMessage() {
       '#type' => 'table',
       '#header' => $header,
       '#rows' => array(),
-      '#empty' => t('No strings available.'),
+      '#empty' => 'Empty row.',
     );
-    $this->content = drupal_render($table);
-    $this->assertRaw('<tr class="odd"><td colspan="3" class="empty message">No strings available.</td>', 'Correct colspan was set on empty message.');
-    $this->assertRaw('<thead><tr><th>Header 1</th>', 'Table header was printed.');
+    $this->render($table);
+    $this->removeWhiteSpace();
+    $this->assertRaw('<thead><tr><th>Header 1</th><th colspan="2">Header 2</th></tr>', 'Table header found.');
+    $this->assertRaw('<tr class="odd"><td colspan="3" class="empty message">Empty row.</td>', 'Colspan on #empty row found.');
   }
 
   /**
@@ -110,7 +112,7 @@ function testThemeTableWithNoStriping() {
       '#type' => 'table',
       '#rows' => $rows,
     );
-    $this->content = drupal_render($table);
+    $this->render($table);
     $this->assertNoRaw('class="odd"', 'Odd/even classes were not added because $no_striping = TRUE.');
     $this->assertNoRaw('no_striping', 'No invalid no_striping HTML attribute was printed.');
   }
@@ -130,10 +132,32 @@ function testThemeTableHeaderCellOption() {
       '#type' => 'table',
       '#rows' => $rows,
     );
-    $this->content = drupal_render($table);
+    $this->render($table);
+    $this->removeWhiteSpace();
     $this->assertRaw('<th>1</th><td>1</td><td>1</td>', 'The th and td tags was printed correctly.');
   }
 
+  /**
+   * Renders a given render array.
+   *
+   * @param array $elements
+   *   The render array elements to render.
+   *
+   * @return string
+   *   The rendered HTML.
+   */
+  protected function render(array $elements) {
+    $this->content = drupal_render($elements);
+    $this->verbose('<pre>' . String::checkPlain($this->content));
+  }
+
+  /**
+   * Removes all white-space between HTML tags from $this->content.
+   */
+  protected function removeWhiteSpace() {
+    $this->content = preg_replace('@>\s+<@', '><', $this->content);
+  }
+
   /**
    * Asserts that a raw string appears in $this->content.
    *
diff --git a/core/modules/system/templates/table.html.twig b/core/modules/system/templates/table.html.twig
new file mode 100644
index 000000000000..ce69286108b8
--- /dev/null
+++ b/core/modules/system/templates/table.html.twig
@@ -0,0 +1,81 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display a table.
+ *
+ * Available variables:
+ * - attributes: HTML attributes to apply to the <table> tag.
+ * - caption: A localized string for the <caption> tag.
+ * - colgroups: Column groups. Each group contains the following properties:
+ *   - attributes: HTML attributes to apply to the <col> tag.
+ *     Note: Drupal currently supports only one table header row, see
+ *     http://drupal.org/node/893530 and
+ *     http://api.drupal.org/api/drupal/includes!theme.inc/function/theme_table/7#comment-5109.
+ * - header: Table header cells. Each cell contains the following properties:
+ *   - tag: The HTML tag name to use; either TH or TD.
+ *   - attributes: HTML attributes to apply to the tag.
+ *   - content: A localized string for the title of the column.
+ *   - field: Field name (required for column sorting).
+ *   - sort: Default sort order for this column ("asc" or "desc").
+ * - sticky: A flag indicating whether to use a "sticky" table header.
+ * - rows: Table rows. Each row contains the following properties:
+ *   - attributes: HTML attributes to apply to the <tr> tag.
+ *   - data: Table cells.
+ *   - no_striping: A flag indicating that the row should receive no
+ *     'even / odd' styling. Defaults to FALSE.
+ *   - cells: Table cells of the row. Each cell contains the following keys:
+ *     - tag: The HTML tag name to use; either TH or TD.
+ *     - attributes: Any HTML attributes, such as "colspan", to apply to the
+ *       table cell.
+ *     - content: The string to display in the table cell.
+ * - empty: The message to display in an extra row if table does not have
+ *   any rows.
+ *
+ * @see template_preprocess_table()
+ *
+ * @ingroup themeable
+ */
+#}
+<table{{ attributes }}>
+  {% if caption %}
+    <caption>{{ caption }}</caption>
+  {% endif %}
+
+  {% for colgroup in colgroups %}
+    {% if colgroup.cols %}
+      <colgroup{{ colgroup.attributes }}>
+        {% for col in colgroup.cols %}
+          <col{{ col.attributes }} />
+        {% endfor %}
+      </colgroup>
+    {% else %}
+      <colgroup{{ colgroup.attributes }} />
+    {% endif %}
+  {% endfor %}
+
+  {% if header %}
+    <thead>
+      <tr>
+        {% for cell in header %}
+          <{{ cell.tag }}{{ cell.attributes }}>
+            {{- cell.content -}}
+          </{{ cell.tag }}>
+        {% endfor %}
+      </tr>
+    </thead>
+  {% endif %}
+
+  {% if rows %}
+    <tbody>
+      {% for row in rows %}
+        <tr{{ row.attributes }}>
+          {% for cell in row.cells %}
+            <{{ cell.tag }}{{ cell.attributes }}>
+              {{- cell.content -}}
+            </{{ cell.tag }}>
+          {% endfor %}
+        </tr>
+      {% endfor %}
+    </tbody>
+  {% endif %}
+</table>
-- 
GitLab