theme.inc 66.6 KB
Newer Older
1
<?php
2

3
/**
4
 * @file
5
 * The theme system, which controls the output of Drupal.
6 7 8 9
 *
 * The theme system allows for nearly all output of the Drupal system to be
 * customized by user themes.
 */
Dries's avatar
Dries committed
10

11
use Drupal\Component\Serialization\Json;
12
use Drupal\Component\Utility\Crypt;
13
use Drupal\Component\Utility\Html;
14
use Drupal\Component\Utility\SafeMarkup;
15
use Drupal\Component\Utility\SafeStringInterface;
16
use Drupal\Component\Utility\Unicode;
17
use Drupal\Component\Utility\Xss;
18
use Drupal\Core\Config\Config;
19
use Drupal\Core\Config\StorageException;
20
use Drupal\Core\Render\RenderableInterface;
21
use Drupal\Core\Template\Attribute;
22
use Drupal\Core\Theme\ThemeSettings;
23
use Drupal\Component\Utility\NestedArray;
24
use Drupal\Core\Render\Element;
25
use Drupal\Core\Render\SafeString;
26

27
/**
28
 * @defgroup content_flags Content markers
29
 * @{
30 31 32 33
 * Markers used by mark.html.twig and node_mark() to designate content.
 *
 * @see mark.html.twig
 * @see node_mark()
34
 */
35 36 37 38

/**
 * Mark content as read.
 */
39
const MARK_READ = 0;
40 41 42 43

/**
 * Mark content as being new.
 */
44
const MARK_NEW = 1;
45 46 47 48

/**
 * Mark content as being updated.
 */
49
const MARK_UPDATED = 2;
50

51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
/**
 * A responsive table class; hide table cell on narrow devices.
 *
 * Indicates that a column has medium priority and thus can be hidden on narrow
 * width devices and shown on medium+ width devices (i.e. tablets and desktops).
 */
const RESPONSIVE_PRIORITY_MEDIUM = 'priority-medium';

/**
 * A responsive table class; only show table cell on wide devices.
 *
 * Indicates that a column has low priority and thus can be hidden on narrow
 * and medium viewports and shown on wide devices (i.e. desktops).
 */
const RESPONSIVE_PRIORITY_LOW = 'priority-low';

67
/**
68
 * @} End of "defgroup content_flags".
69 70
 */

71
/**
72
 * Gets the theme registry.
73
 *
74
 * @param bool $complete
75
 *   Optional boolean to indicate whether to return the complete theme registry
76 77 78 79 80 81 82
 *   array or an instance of the Drupal\Core\Utility\ThemeRegistry class.
 *   If TRUE, the complete theme registry array will be returned. This is useful
 *   if you want to foreach over the whole registry, use array_* functions or
 *   inspect it in a debugger. If FALSE, an instance of the
 *   Drupal\Core\Utility\ThemeRegistry class will be returned, this provides an
 *   ArrayObject which allows it to be accessed with array syntax and isset(),
 *   and should be more lightweight than the full registry. Defaults to TRUE.
83
 *
84
 * @return
85 86
 *   The complete theme registry array, or an instance of the
 *   Drupal\Core\Utility\ThemeRegistry class.
87
 */
88
function theme_get_registry($complete = TRUE) {
89
  $theme_registry = \Drupal::service('theme.registry');
90
  if ($complete) {
91
    return $theme_registry->get();
92 93
  }
  else {
94
    return $theme_registry->getRuntime();
95 96 97 98
  }
}

/**
99 100 101 102
 * Forces the system to rebuild the theme registry.
 *
 * This function should be called when modules are added to the system, or when
 * a dynamic system needs to add more theme hooks.
103
 */
104
function drupal_theme_rebuild() {
105
  \Drupal::service('theme.registry')->reset();
106 107
}

108
/**
109
 * Allows themes and/or theme engines to discover overridden theme functions.
110
 *
111
 * @param array $cache
112
 *   The existing cache of theme hooks to test against.
113
 * @param array $prefixes
114 115
 *   An array of prefixes to test, in reverse order of importance.
 *
116
 * @return array
117 118 119
 *   The functions found, suitable for returning from hook_theme;
 */
function drupal_find_theme_functions($cache, $prefixes) {
120
  $implementations = [];
121
  $grouped_functions = \Drupal::service('theme.registry')->getPrefixGroupedUserFunctions();
122 123 124

  foreach ($cache as $hook => $info) {
    foreach ($prefixes as $prefix) {
125 126 127 128 129 130
      // Find theme functions that implement possible "suggestion" variants of
      // registered theme hooks and add those as new registered theme hooks.
      // The 'pattern' key defines a common prefix that all suggestions must
      // start with. The default is the name of the hook followed by '__'. An
      // 'base hook' key is added to each entry made for a found suggestion,
      // so that common functionality can be implemented for all suggestions of
131
      // the same base hook. To keep things simple, deep hierarchy of
132 133 134 135
      // suggestions is not supported: each suggestion's 'base hook' key
      // refers to a base hook, not to another suggestion, and all suggestions
      // are found using the base hook's pattern, not a pattern from an
      // intermediary suggestion.
136
      $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__');
137 138 139 140
      // Grep only the functions which are within the prefix group.
      list($first_prefix,) = explode('_', $prefix, 2);
      if (!isset($info['base hook']) && !empty($pattern) && isset($grouped_functions[$first_prefix])) {
        $matches = preg_grep('/^' . $prefix . '_' . $pattern . '/', $grouped_functions[$first_prefix]);
141 142
        if ($matches) {
          foreach ($matches as $match) {
143
            $new_hook = substr($match, strlen($prefix) + 1);
144
            $arg_name = isset($info['variables']) ? 'variables' : 'render element';
145
            $implementations[$new_hook] = array(
146
              'function' => $match,
147
              $arg_name => $info[$arg_name],
148
              'base hook' => $hook,
149 150 151 152
            );
          }
        }
      }
153 154 155
      // Find theme functions that implement registered theme hooks and include
      // that in what is returned so that the registry knows that the theme has
      // this implementation.
156
      if (function_exists($prefix . '_' . $hook)) {
157
        $implementations[$hook] = array(
158
          'function' => $prefix . '_' . $hook,
159 160 161 162 163
        );
      }
    }
  }

164
  return $implementations;
165 166 167
}

/**
168
 * Allows themes and/or theme engines to easily discover overridden templates.
169 170 171 172 173 174 175 176 177
 *
 * @param $cache
 *   The existing cache of theme hooks to test against.
 * @param $extension
 *   The extension that these templates will have.
 * @param $path
 *   The path to search.
 */
function drupal_find_theme_templates($cache, $extension, $path) {
178
  $implementations = array();
179

180 181 182 183
  // Collect paths to all sub-themes grouped by base themes. These will be
  // used for filtering. This allows base themes to have sub-themes in its
  // folder hierarchy without affecting the base themes template discovery.
  $theme_paths = array();
184
  foreach (\Drupal::service('theme_handler')->listInfo() as $theme_info) {
185
    if (!empty($theme_info->base_theme)) {
186
      $theme_paths[$theme_info->base_theme][$theme_info->getName()] = $theme_info->getPath();
187 188 189 190 191 192 193
    }
  }
  foreach ($theme_paths as $basetheme => $subthemes) {
    foreach ($subthemes as $subtheme => $subtheme_path) {
      if (isset($theme_paths[$subtheme])) {
        $theme_paths[$basetheme] = array_merge($theme_paths[$basetheme], $theme_paths[$subtheme]);
      }
194 195
    }
  }
196
  $theme = \Drupal::theme()->getActiveTheme()->getName();
197
  $subtheme_paths = isset($theme_paths[$theme]) ? $theme_paths[$theme] : array();
198

199
  // Escape the periods in the extension.
200
  $regex = '/' . str_replace('.', '\.', $extension) . '$/';
201
  // Get a listing of all template files in the path to search.
202
  $files = file_scan_directory($path, $regex, array('key' => 'filename'));
203 204 205 206

  // Find templates that implement registered theme hooks and include that in
  // what is returned so that the registry knows that the theme has this
  // implementation.
207
  foreach ($files as $template => $file) {
208
    // Ignore sub-theme templates for the current theme.
209
    if (strpos($file->uri, str_replace($subtheme_paths, '', $file->uri)) !== 0) {
210 211
      continue;
    }
212 213
    // Remove the extension from the filename.
    $template = str_replace($extension, '', $template);
214 215 216 217
    // Transform - in filenames to _ to match function naming scheme
    // for the purposes of searching.
    $hook = strtr($template, '-', '_');
    if (isset($cache[$hook])) {
218
      $implementations[$hook] = array(
219
        'template' => $template,
220
        'path' => dirname($file->uri),
221 222
      );
    }
223 224 225 226

    // Match templates based on the 'template' filename.
    foreach ($cache as $hook => $info) {
      if (isset($info['template'])) {
227
        $template_candidates = array($info['template'], str_replace($info['theme path'] . '/templates/', '', $info['template']));
228 229 230 231 232 233 234 235
        if (in_array($template, $template_candidates)) {
          $implementations[$hook] = array(
            'template' => $template,
            'path' => dirname($file->uri),
          );
        }
      }
    }
236 237
  }

238
  // Find templates that implement possible "suggestion" variants of registered
239
  // theme hooks and add those as new registered theme hooks. See
240 241
  // drupal_find_theme_functions() for more information about suggestions and
  // the use of 'pattern' and 'base hook'.
242 243
  $patterns = array_keys($files);
  foreach ($cache as $hook => $info) {
244
    $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__');
245
    if (!isset($info['base hook']) && !empty($pattern)) {
246 247
      // Transform _ in pattern to - to match file naming scheme
      // for the purposes of searching.
248
      $pattern = strtr($pattern, '_', '-');
249

250
      $matches = preg_grep('/^' . $pattern . '/', $patterns);
251 252
      if ($matches) {
        foreach ($matches as $match) {
253
          $file = $match;
254 255
          // Remove the extension from the filename.
          $file = str_replace($extension, '', $file);
256 257
          // Put the underscores back in for the hook name and register this
          // pattern.
258
          $arg_name = isset($info['variables']) ? 'variables' : 'render element';
259
          $implementations[strtr($file, '-', '_')] = array(
260
            'template' => $file,
261
            'path' => dirname($files[$match]->uri),
262
            $arg_name => $info[$arg_name],
263
            'base hook' => $hook,
264 265 266 267 268
          );
        }
      }
    }
  }
269
  return $implementations;
270 271
}

272
/**
273
 * Retrieves a setting for the current theme or for a given theme.
274
 *
275 276 277 278 279 280
 * The final setting is obtained from the last value found in the following
 * sources:
 * - the saved values from the global theme settings form
 * - the saved values from the theme's settings form
 * To only retrieve the default global theme setting, an empty string should be
 * given for $theme.
281 282
 *
 * @param $setting_name
283
 *   The name of the setting to be retrieved.
284
 * @param $theme
285 286
 *   The name of a given theme; defaults to the current theme.
 *
287 288 289
 * @return
 *   The value of the requested setting, NULL if the setting does not exist.
 */
290
function theme_get_setting($setting_name, $theme = NULL) {
291
  /** @var \Drupal\Core\Theme\ThemeSettings[] $cache */
292
  $cache = &drupal_static(__FUNCTION__, array());
293

294
  // If no key is given, use the current theme if we can determine it.
295
  if (!isset($theme)) {
296
    $theme = \Drupal::theme()->getActiveTheme()->getName();
297
  }
298

299
  if (empty($cache[$theme])) {
300 301
    // Create a theme settings object.
    $cache[$theme] = new ThemeSettings($theme);
302 303
    // Get the global settings from configuration.
    $cache[$theme]->setData(\Drupal::config('system.theme.global')->get());
304

305 306
    // Get the values for the theme-specific settings from the .info.yml files
    // of the theme and all its base themes.
307 308
    $themes = \Drupal::service('theme_handler')->listInfo();
    if (isset($themes[$theme])) {
309 310
      $theme_object = $themes[$theme];

311 312 313 314 315 316 317 318
      // Retrieve configured theme-specific settings, if any.
      try {
        if ($theme_settings = \Drupal::config($theme . '.settings')->get()) {
          $cache[$theme]->merge($theme_settings);
        }
      }
      catch (StorageException $e) {
      }
319

320 321 322
      // If the theme does not support a particular feature, override the global
      // setting and set the value to NULL.
      if (!empty($theme_object->info['features'])) {
323
        foreach (_system_default_theme_features() as $feature) {
324
          if (!in_array($feature, $theme_object->info['features'])) {
325
            $cache[$theme]->set('features.' . $feature, NULL);
326 327 328 329
          }
        }
      }

330
      // Generate the path to the logo image.
331 332 333 334 335
      if ($cache[$theme]->get('logo.use_default')) {
        $cache[$theme]->set('logo.url', file_create_url($theme_object->getPath() . '/logo.svg'));
      }
      elseif ($logo_path = $cache[$theme]->get('logo.path')) {
        $cache[$theme]->set('logo.url', file_create_url($logo_path));
336
      }
337 338

      // Generate the path to the favicon.
339 340 341
      if ($cache[$theme]->get('features.favicon')) {
        $favicon_path = $cache[$theme]->get('favicon.path');
        if ($cache[$theme]->get('favicon.use_default')) {
342
          if (file_exists($favicon = $theme_object->getPath() . '/favicon.ico')) {
343
            $cache[$theme]->set('favicon.url', file_create_url($favicon));
344 345
          }
          else {
346
            $cache[$theme]->set('favicon.url', file_create_url('core/misc/favicon.ico'));
347 348
          }
        }
349 350
        elseif ($favicon_path) {
          $cache[$theme]->set('favicon.url', file_create_url($favicon_path));
351 352
        }
        else {
353
          $cache[$theme]->set('features.favicon', FALSE);
354
        }
355
      }
356
    }
357 358
  }

359 360 361
  return $cache[$theme]->get($setting_name);
}

362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
/**
 * Escapes and renders variables for theme functions.
 *
 * This method is used in theme functions to ensure that the result is safe for
 * output inside HTML fragments. This mimics the behavior of the auto-escape
 * functionality in Twig.
 *
 * Note: This function should be kept in sync with
 * \Drupal\Core\Template\TwigExtension::escapeFilter().
 *
 * @param mixed $arg
 *   The string, object, or render array to escape if needed.
 *
 * @return string
 *   The rendered string, safe for use in HTML. The string is not safe when used
 *   as any part of an HTML attribute name or value.
 *
 * @throws \Exception
 *   Thrown when an object is passed in which cannot be printed.
 *
 * @see \Drupal\Core\Template\TwigExtension::escapeFilter()
 *
 * @todo Discuss deprecating this in https://www.drupal.org/node/2575081.
 * @todo Refactor this to keep it in sync with Twig filtering in
 *   https://www.drupal.org/node/2575065
 */
function theme_render_and_autoescape($arg) {
  if ($arg instanceOf SafeStringInterface) {
    return (string) $arg;
  }
  $return = NULL;

  if (is_scalar($arg)) {
    $return = (string) $arg;
  }
  elseif (is_object($arg)) {
    if ($arg instanceof RenderableInterface) {
      $arg = $arg->toRenderable();
    }
    elseif (method_exists($arg, '__toString')) {
      $return = (string) $arg;
    }
    // You can't throw exceptions in the magic PHP __toString methods, see
    // http://php.net/manual/en/language.oop5.magic.php#object.tostring so
    // we also support a toString method.
    elseif (method_exists($arg, 'toString')) {
      $return = $arg->toString();
    }
    else {
      throw new \Exception(t('Object of type "@class" cannot be printed.', array('@class' => get_class($arg))));
    }
  }

  // We have a string or an object converted to a string: Escape it!
  if (isset($return)) {
    return SafeMarkup::isSafe($return, 'html') ? $return : Html::escape($return);
  }

  // This is a normal render array, which is safe by definition, with special
  // simple cases already handled.

  // Early return if this element was pre-rendered (no need to re-render).
  if (isset($arg['#printed']) && $arg['#printed'] == TRUE && isset($arg['#markup']) && strlen($arg['#markup']) > 0) {
    return (string) $arg['#markup'];
  }
  $arg['#printed'] = FALSE;
  return (string) \Drupal::service('renderer')->render($arg);
}

431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461
/**
 * Converts theme settings to configuration.
 *
 * @see system_theme_settings_submit()
 *
 * @param array $theme_settings
 *   An array of theme settings from system setting form or a Drupal 7 variable.
 * @param Config $config
 *   The configuration object to update.
 *
 * @return
 *   The Config object with updated data.
 */
function theme_settings_convert_to_config(array $theme_settings, Config $config) {
  foreach ($theme_settings as $key => $value) {
    if ($key == 'default_logo') {
      $config->set('logo.use_default', $value);
    }
    else if ($key == 'logo_path') {
      $config->set('logo.path', $value);
    }
    else if ($key == 'default_favicon') {
      $config->set('favicon.use_default', $value);
    }
    else if ($key == 'favicon_path') {
      $config->set('favicon.path', $value);
    }
    else if ($key == 'favicon_mimetype') {
      $config->set('favicon.mimetype', $value);
    }
    else if (substr($key, 0, 7) == 'toggle_') {
462
      $config->set('features.' . Unicode::substr($key, 7), $value);
463 464 465 466 467 468
    }
    else if (!in_array($key, array('theme', 'logo_upload'))) {
      $config->set($key, $value);
    }
  }
  return $config;
469 470
}

471
/**
472 473 474
 * Prepares variables for time templates.
 *
 * Default template: time.html.twig.
475 476
 *
 * @param array $variables
477
 *   An associative array possibly containing:
478 479 480
 *   - attributes['timestamp']:
 *   - timestamp:
 *   - text:
481
 */
482
function template_preprocess_time(&$variables) {
483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
  // Format the 'datetime' attribute based on the timestamp.
  // @see http://www.w3.org/TR/html5-author/the-time-element.html#attr-time-datetime
  if (!isset($variables['attributes']['datetime']) && isset($variables['timestamp'])) {
    $variables['attributes']['datetime'] = format_date($variables['timestamp'], 'html_datetime', '', 'UTC');
  }

  // If no text was provided, try to auto-generate it.
  if (!isset($variables['text'])) {
    // Format and use a human-readable version of the timestamp, if any.
    if (isset($variables['timestamp'])) {
      $variables['text'] = format_date($variables['timestamp']);
    }
    // Otherwise, use the literal datetime attribute.
    elseif (isset($variables['attributes']['datetime'])) {
      $variables['text'] = $variables['attributes']['datetime'];
    }
  }
}

502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548
/**
 * Prepares variables for datetime form element templates.
 *
 * The datetime form element serves as a wrapper around the date element type,
 * which creates a date and a time component for a date.
 *
 * Default template: datetime-form.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - element: An associative array containing the properties of the element.
 *     Properties used: #title, #value, #options, #description, #required,
 *     #attributes.
 *
 * @see form_process_datetime()
 */
function template_preprocess_datetime_form(&$variables) {
  $element = $variables['element'];

  $variables['attributes'] = array();
  if (isset($element['#id'])) {
    $variables['attributes']['id'] = $element['#id'];
  }
  if (!empty($element['#attributes']['class'])) {
    $variables['attributes']['class'] = (array) $element['#attributes']['class'];
  }

  $variables['content'] = $element;
}

/**
 * Prepares variables for datetime form wrapper templates.
 *
 * Default template: datetime-wrapper.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - element: An associative array containing the properties of the element.
 *     Properties used: #title, #children, #required, #attributes.
 */
function template_preprocess_datetime_wrapper(&$variables) {
  $element = $variables['element'];

  if (!empty($element['#title'])) {
    $variables['title'] = $element['#title'];
  }

549 550 551 552 553 554
  // Display any error messages.
  $variables['errors'] = NULL;
  if (!empty($element['#errors']) && empty($element['#error_no_message'])) {
    $variables['errors'] = $element['#errors'];
  }

555 556 557 558
  if (!empty($element['#description'])) {
    $variables['description'] = $element['#description'];
  }

559
  $variables['required'] = FALSE;
560 561
  // For required datetime fields 'form-required' & 'js-form-required' classes
  // are appended to the label attributes.
562
  if (!empty($element['#required'])) {
563
    $variables['required'] = TRUE;
564 565 566 567
  }
  $variables['content'] = $element['#children'];
}

568
/**
569
 * Prepares variables for links templates.
570
 *
571 572 573
 * Default template: links.html.twig.
 *
 * @param array $variables
574
 *   An associative array containing:
575
 *   - links: An associative array of links to be themed. The key for each link
576
 *     is used as its CSS class. Each link should be itself an array, with the
577 578
 *     following elements:
 *     - title: The link text.
579 580
 *     - url: (optional) The url object to link to. If omitted, no a tag is
 *       printed out.
581 582
 *     - attributes: (optional) Attributes for the anchor, or for the <span>
 *       tag used in its place if no 'href' is supplied. If element 'class' is
583
 *       included, it must be an array of one or more class names.
584 585
 *     If the 'href' element is supplied, the entire link array is passed to
 *     l() as its $options parameter.
586 587
 *   - attributes: A keyed array of attributes for the UL containing the
 *     list of links.
588
 *   - set_active_class: (optional) Whether each link should compare the
589
 *     route_name + route_parameters or href (path), language and query options
590 591 592 593
 *     to the current URL, to determine whether the link is "active". If so, an
 *     "active" class will be applied to the list item containing the link, as
 *     well as the link itself. It is important to use this sparingly since it
 *     is usually unnecessary and requires extra processing.
594 595 596 597 598
 *     For anonymous users, the "active" class will be calculated on the server,
 *     because most sites serve each anonymous user the same cached page anyway.
 *     For authenticated users, the "active" class will be calculated on the
 *     client (through JavaScript), only data- attributes are added to list
 *     items and contained links, to prevent breaking the render cache. The
599
 *     JavaScript is added in system_page_attachments().
600 601 602
 *   - heading: (optional) A heading to precede the links. May be an
 *     associative array or a string. If it's an array, it can have the
 *     following elements:
603 604
 *     - text: The heading text.
 *     - level: The heading level (e.g. 'h2', 'h3').
605
 *     - attributes: (optional) An array of the CSS attributes for the heading.
606
 *     When using a string it will be used as the text of the heading and the
607 608
 *     level will default to 'h2'. Headings should be used on navigation menus
 *     and any list of links that consistently appears on multiple pages. To
609
 *     make the heading invisible use the 'visually-hidden' CSS class. Do not
610 611
 *     use 'display:none', which removes it from screen readers and assistive
 *     technology. Headings allow screen reader and keyboard only users to
612 613 614
 *     navigate to or skip the links. See
 *     http://juicystudio.com/article/screen-readers-display-none.php and
 *     http://www.w3.org/TR/WCAG-TECHS/H42.html for more information.
615
 *
616 617
 * Unfortunately links templates duplicate the "active" class handling of l()
 * and LinkGenerator::generate() because it needs to be able to set the "active"
618 619 620 621 622
 * class not on the links themselves ("a" tags), but on the list items ("li"
 * tags) that contain the links. This is necessary for CSS to be able to style
 * list items differently when the link is active, since CSS does not yet allow
 * one to style list items only if it contains a certain element with a certain
 * class. I.e. we cannot yet convert this jQuery selector to a CSS selector:
623
 *   jQuery('li:has("a.is-active")')
624
 *
625
 * @see \Drupal\Core\Utility\LinkGenerator
626
 * @see \Drupal\Core\Utility\LinkGenerator::generate()
627
 * @see system_page_attachments()
628
 */
629
function template_preprocess_links(&$variables) {
630
  $links = $variables['links'];
631
  $heading = &$variables['heading'];
632

633 634
  if (!empty($links)) {
    // Prepend the heading to the list, if any.
635
    if (!empty($heading)) {
636
      // Convert a string heading into an array, using a H2 tag by default.
637
      if (is_string($heading)) {
638
        $heading = array('text' => $heading);
639
      }
640 641 642 643 644
      // Merge in default array properties into $heading.
      $heading += array(
        'level' => 'h2',
        'attributes' => array(),
      );
645 646
      // Convert the attributes array into an Attribute object.
      $heading['attributes'] = new Attribute($heading['attributes']);
647 648
    }

649
    $variables['links'] = array();
650
    foreach ($links as $key => $link) {
651
      $item = array();
652
      $link += array(
653
        'ajax' => NULL,
654
        'url' => NULL,
655 656
      );

657
      $li_attributes = array();
658
      $keys = ['title', 'url'];
659 660
      $link_element = array(
        '#type' => 'link',
661
        '#title' => $link['title'],
662
        '#options' => array_diff_key($link, array_combine($keys, $keys)),
663
        '#url' => $link['url'],
664
        '#ajax' => $link['ajax'],
665 666
      );

667 668
      // Handle links and ensure that the active class is added on the LIs, but
      // only if the 'set_active_class' option is not empty.
669
      if (isset($link['url'])) {
670
        if (!empty($variables['set_active_class'])) {
671

672 673
          // Also enable set_active_class for the contained link.
          $link_element['#options']['set_active_class'] = TRUE;
674

675
          if (!empty($link['language'])) {
676
            $li_attributes['hreflang'] = $link['language']->getId();
677 678 679 680 681 682 683 684 685 686
          }

          // Add a "data-drupal-link-query" attribute to let the
          // drupal.active-link library know the query in a standardized manner.
          if (!empty($link['query'])) {
            $query = $link['query'];
            ksort($query);
            $li_attributes['data-drupal-link-query'] = Json::encode($query);
          }

687 688 689 690 691 692 693 694 695
          /** @var \Drupal\Core\Url $url */
          $url = $link['url'];
          if ($url->isRouted()) {
            // Add a "data-drupal-link-system-path" attribute to let the
            // drupal.active-link library know the path in a standardized manner.
            $system_path = $url->getInternalPath();
            // @todo System path is deprecated - use the route name and parameters.
            // Special case for the front page.
            $li_attributes['data-drupal-link-system-path'] = $system_path == '' ? '<front>' : $system_path;
696
          }
697
        }
698

699
        $item['link'] = $link_element;
700
      }
701

702
      // Handle title-only text items.
703
      $item['text'] = $link['title'];
704 705
      if (isset($link['attributes'])) {
        $item['text_attributes'] = new Attribute($link['attributes']);
706
      }
707

708 709
      // Handle list item attributes.
      $item['attributes'] = new Attribute($li_attributes);
710

711
      // Add the item to the list of links.
712
      $variables['links'][$key] = $item;
713
    }
714
  }
715
}
Dries's avatar
Dries committed
716

717
/**
718
 * Prepares variables for image templates.
719
 *
720 721 722
 * Default template: image.html.twig.
 *
 * @param array $variables
723
 *   An associative array containing:
724
 *   - uri: Either the path of the image file (relative to base_path()) or a
725
 *     full URL.
726 727
 *   - width: The width of the image (if known).
 *   - height: The height of the image (if known).
728 729 730 731 732
 *   - alt: The alternative text for text-based browsers. HTML 4 and XHTML 1.0
 *     always require an alt attribute. The HTML 5 draft allows the alt
 *     attribute to be omitted in some cases. Therefore, this variable defaults
 *     to an empty string, but can be set to NULL for the attribute to be
 *     omitted. Usually, neither omission nor an empty string satisfies
733
 *     accessibility requirements, so it is strongly encouraged for code
734
 *     calling _theme('image') to pass a meaningful value for this variable.
735 736 737
 *     - http://www.w3.org/TR/REC-html40/struct/objects.html#h-13.8
 *     - http://www.w3.org/TR/xhtml1/dtds.html
 *     - http://dev.w3.org/html5/spec/Overview.html#alt
738 739 740
 *   - title: The title text is displayed when the image is hovered in some
 *     popular browsers.
 *   - attributes: Associative array of attributes to be placed in the img tag.
741
 *   - srcset: Array of multiple URIs and sizes/multipliers.
742 743
 *   - sizes: The sizes attribute for viewport-based selection of images.
 *     - http://www.whatwg.org/specs/web-apps/current-work/multipage/embedded-content.html#introduction-3:viewport-based-selection-2
744
 */
745
function template_preprocess_image(&$variables) {
746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765
  if (!empty($variables['uri'])) {
    $variables['attributes']['src'] = file_create_url($variables['uri']);
  }
  // Generate a srcset attribute conforming to the spec at
  // http://www.w3.org/html/wg/drafts/html/master/embedded-content.html#attr-img-srcset
  if (!empty($variables['srcset'])) {
    $srcset = array();
    foreach ($variables['srcset'] as $src) {
      // URI is mandatory.
      $source = file_create_url($src['uri']);
      if (isset($src['width']) && !empty($src['width'])) {
        $source .= ' ' . $src['width'];
      }
      elseif (isset($src['multiplier']) && !empty($src['multiplier'])) {
        $source .= ' ' . $src['multiplier'];
      }
      $srcset[] = $source;
    }
    $variables['attributes']['srcset'] = implode(', ', $srcset);
  }
766

767
  foreach (array('width', 'height', 'alt', 'title', 'sizes') as $key) {
768
    if (isset($variables[$key])) {
769 770 771 772 773
      // If the property has already been defined in the attributes,
      // do not override, including NULL.
      if (array_key_exists($key, $variables['attributes'])) {
        continue;
      }
774
      $variables['attributes'][$key] = $variables[$key];
775
    }
776
  }
777
}
Dries's avatar
Dries committed
778

779
/**
780
 * Prepares variables for table templates.
781
 *
782 783 784
 * Default template: table.html.twig.
 *
 * @param array $variables
785 786 787 788
 *   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
 *     following keys:
789 790
 *     - data: The localized title of the table column, as a string or render
 *       array.
791
 *     - field: The database field represented in the table column (required
792
 *       if user is to be able to sort on this column).
793 794 795 796 797 798 799 800 801 802 803
 *     - sort: A default sort order for this column ("asc" or "desc"). Only
 *       one column should be given a default sort order because table sorting
 *       only applies to one column at a time.
 *     - class: An array of values for the 'class' attribute. In particular,
 *       the least important columns that can be hidden on narrow and medium
 *       width screens should have a 'priority-low' class, referenced with the
 *       RESPONSIVE_PRIORITY_LOW constant. Columns that should be shown on
 *       medium+ wide screens should be marked up with a class of
 *       'priority-medium', referenced by with the RESPONSIVE_PRIORITY_MEDIUM
 *       constant. Themes may hide columns with one of these two classes on
 *       narrow viewports to save horizontal space.
804 805 806 807
 *     - Any HTML attributes, such as "colspan", to apply to the column header
 *       cell.
 *   - rows: An array of table rows. Every row is an array of cells, or an
 *     associative array with the following keys:
808
 *     - data: An array of cells.
809
 *     - Any HTML attributes, such as "class", to apply to the table row.
810
 *     - no_striping: A Boolean indicating that the row should receive no
811
 *       'even / odd' styling. Defaults to FALSE.
812 813
 *     Each cell can be either a string or an associative array with the
 *     following keys:
814
 *     - data: The string or render array to display in the table cell.
815
 *     - header: Indicates this cell is a header.
816 817
 *     - Any HTML attributes, such as "colspan", to apply to the table cell.
 *     Here's an example for $rows:
818
 *     @code
819 820
 *     $rows = array(
 *       // Simple row
821
 *       array(
822
 *         'Cell 1', 'Cell 2', 'Cell 3'
823
 *       ),
824 825 826
 *       // Row with attributes on the row and some of its cells.
 *       array(
 *         'data' => array('Cell 1', array('data' => 'Cell 2', 'colspan' => 2)), 'class' => array('funky')
827
 *       ),
828
 *     );
829
 *     @endcode
830 831
 *   - footer: An array of table rows which will be printed within a <tfoot>
 *     tag, in the same format as the rows element (see above).
832 833 834 835 836 837 838 839 840 841 842
 *   - attributes: An array of HTML attributes to apply to the table tag.
 *   - caption: A localized string to use for the <caption> tag.
 *   - colgroups: An array of column groups. Each element of the array can be
 *     either:
 *     - An array of columns, each of which is an associative array of HTML
 *       attributes applied to the COL element.
 *     - An array of attributes applied to the COLGROUP element, which must
 *       include a "data" attribute. To add attributes to COL elements, set the
 *       "data" attribute with an array of columns, each of which is an
 *       associative array of HTML attributes.
 *     Here's an example for $colgroup:
843
 *     @code
844 845 846
 *     $colgroup = array(
 *       // COLGROUP with one COL element.
 *       array(
847
 *         array(
848
 *           'class' => array('funky'), // Attribute for the COL element.
849 850
 *         ),
 *       ),
851 852 853 854 855 856 857 858 859 860
 *       // Colgroup with attributes and inner COL elements.
 *       array(
 *         'data' => array(
 *           array(
 *             'class' => array('funky'), // Attribute for the COL element.
 *           ),
 *         ),
 *         'class' => array('jazzy'), // Attribute for the COLGROUP element.
 *       ),
 *     );
861
 *     @endcode
862 863 864 865
 *     These optional tags are used to group and set properties on columns
 *     within a table. For example, one may easily group three columns and
 *     apply same background style to all.
 *   - sticky: Use a "sticky" table header.
866 867
 *   - empty: The message to display in an extra row if table does not have any
 *     rows.
868
 */
869 870 871
function template_preprocess_table(&$variables) {
  $is_sticky = !empty($variables['sticky']);
  $is_responsive = !empty($variables['responsive']);
872

873
  // Format the table columns:
874 875
  if (!empty($variables['colgroups'])) {
    foreach ($variables['colgroups'] as &$colgroup) {
876 877
      // Check if we're dealing with a simple or complex column
      if (isset($colgroup['data'])) {
878 879 880
        $cols = $colgroup['data'];
        unset($colgroup['data']);
        $colgroup_attributes = $colgroup;
881 882 883
      }
      else {
        $cols = $colgroup;
884
        $colgroup_attributes = array();
885
      }
886 887 888 889 890 891 892 893
      $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);
894 895 896 897 898
        }
      }
    }
  }

899 900 901
  // Build an associative array of responsive classes keyed by column.
  $responsive_classes = array();

902
  // Format the table header:
903
  $ts = array();
904
  $header_columns = 0;
905 906 907
  if (!empty($variables['header'])) {
    $ts = tablesort_init($variables['header']);

908 909 910
    // Use a separate index with responsive classes as headers
    // may be associative.
    $responsive_index = -1;
911
    foreach ($variables['header'] as $col_key => $cell) {
912 913 914
      // Increase the responsive index.
      $responsive_index++;

915
      if (!is_array($cell)) {
916
        $header_columns++;
917
        $cell_content = $cell;
918
        $cell_attributes = new Attribute();
919 920 921
        $is_header = TRUE;
      }
      else {
922 923 924 925 926 927
        if (isset($cell['colspan'])) {
          $header_columns += $cell['colspan'];
        }
        else {
          $header_columns++;
        }
928 929 930 931
        $cell_content = '';
        if (isset($cell['data'])) {
          $cell_content = $cell['data'];
          unset($cell['data']);
932
        }
933 934 935 936 937 938 939 940 941 942
        // Flag the cell as a header or not and remove the flag.
        $is_header = isset($cell['header']) ? $cell['header'] : TRUE;
        unset($cell['header']);

        // Track responsive classes for each column as needed. Only the header
        // cells for a column are marked up with the responsive classes by a
        // module developer or themer. The responsive classes on the header cells
        // must be transferred to the content cells.
        if (!empty($cell['class']) && is_array($cell['class'])) {
          if (in_array(RESPONSIVE_PRIORITY_MEDIUM, $cell['class'])) {
943
            $responsive_classes[$responsive_index] = RESPONSIVE_PRIORITY_MEDIUM;
944 945
          }
          elseif (in_array(RESPONSIVE_PRIORITY_LOW, $cell['class'])) {
946
            $responsive_classes[$responsive_index] = RESPONSIVE_PRIORITY_LOW;
947
          }
948
        }
949

950
        tablesort_header($cell_content, $cell, $variables['header'], $ts);
951 952 953

        // tablesort_header() removes the 'sort' and 'field' keys.
        $cell_attributes = new Attribute($cell);
954
      }
955 956 957 958
      $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;
959
    }
960
  }
961
  $variables['header_columns'] = $header_columns;
962

963 964 965 966 967
  // Rows and footer have the same structure.
  $sections = array('rows' , 'footer');
  foreach ($sections as $section) {
    if (!empty($variables[$section])) {
      foreach ($variables[$section] as $row_key => $row) {
968
        $cells = $row;
969
        $row_attributes = array();
970

971 972 973
        // Check if we're dealing with a simple or complex row
        if (isset($row['data'])) {
          $cells = $row['data'];
974
          $variables['no_striping'] = isset($row['no_striping']) ? $row['no_striping'] : FALSE;
975

976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996
          // Set the attributes array and exclude 'data' and 'no_striping'.
          $row_attributes = $row;
          unset($row_attributes['data']);
          unset($row_attributes['no_striping']);
        }

        // Build row.
        $variables[$section][$row_key] = array();
        $variables[$section][$row_key]['attributes'] = new Attribute($row_attributes);
        $variables[$section][$row_key]['cells'] = array();
        if (!empty($cells)) {
          // Reset the responsive index.
          $responsive_index = -1;
          foreach ($cells as $col_key => $cell) {
            // Increase the responsive index.
            $responsive_index++;

            if (!is_array($cell)) {
              $cell_content = $cell;
              $cell_attributes = array();
              $is_header = FALSE;
997
            }
998 999 1000 1001 1002 1003
            else {
              $cell_content = '';
              if (isset($cell['data'])) {
                $cell_content = $cell['data'];
                unset($cell['data']);
              }
1004

1005 1006 1007
              // Flag the cell as a header or not and remove the flag.
              $is_header = !empty($cell['header']);
              unset($cell['header']);
1008

1009
              $cell_attributes = $cell;
1010
            }
1011
            // Active table sort information.
1012
            if (isset($variables['header'][$col_key]['data']) && $variables['header'][$col_key]['data'] == $ts['name'] && !empty($variables['header'][$col_key]['field'])) {
1013
              $variables[$section][$row_key]['cells'][$col_key]['active_table_sort'] = TRUE;
1014 1015 1016 1017 1018 1019 1020 1021 1022
            }
            // Copy RESPONSIVE_PRIORITY_LOW/RESPONSIVE_PRIORITY_MEDIUM
            // class from header to cell as needed.
            if (isset($responsive_classes[$responsive_index])) {
              $cell_attributes['class'][] = $responsive_classes[$responsive_index];
            }
            $variables[$section][$row_key]['cells'][$col_key]['tag'] = $is_header ? 'th' : 'td';
            $variables[$section][$row_key]['cells'][$col_key]['attributes'] = new Attribute($cell_attributes);
            $variables[$section][$row_key]['cells'][$col_key]['content'] = $cell_content;
1023
          }
1024
        }
1025 1026 1027
      }
    }
  }
1028 1029 1030
  if (empty($variables['no_striping'])) {
    $variables['attributes']['data-striping'] = 1;
  }
1031 1032
}

1033
/**
1034 1035 1036
 * Prepares variables for item list templates.
 *
 * Default template: item-list.html.twig.
1037 1038
 *
 * @param array $variables
1039 1040 1041 1042 1043 1044 1045 1046 1047 1048
 *   An associative array containing:
 *   - items: An array of items to be displayed in the list. Each item can be
 *     either a string or a render array. If #type, #theme, or #markup
 *     properties are not specified for child render arrays, they will be
 *     inherited from the parent list, allowing callers to specify larger
 *     nested lists without having to explicitly specify and repeat the
 *     render properties for all nested child lists.
 *   - title: A title to be prepended to the list.
 *   - list_type: The type of list to return (e.g. "ul", "ol").
 *
1049
 * @see https://www.drupal.org/node/1842756
1050 1051 1052
 */
function template_preprocess_item_list(&$variables) {
  foreach ($variables['items'] as &$item) {
1053
    $attributes = array();
1054 1055
    // If the item value is an array, then it is a render array.
    if (is_array($item)) {
1056 1057 1058 1059
      // List items support attributes via the '#wrapper_attributes' property.
      if (isset($item['#wrapper_attributes'])) {
        $attributes = $item['#wrapper_attributes'];
      }
1060 1061 1062 1063
      // Determine whether there are any child elements in the item that are not
      // fully-specified render arrays. If there are any, then the child
      // elements present nested lists and we automatically inherit the render
      // array properties of the current list to them.
1064
      foreach (Element::children($item) as $key) {
1065 1066 1067 1068
        $child = &$item[$key];
        // If this child element does not specify how it can be rendered, then
        // we need to inherit the render properties of the current list.
        if (!isset($child['#type']) && !isset($child['#theme']) && !isset($child['#markup'])) {
1069 1070 1071
          // Since item-list.html.twig supports both strings and render arrays
          // as items, the items of the nested list may have been specified as
          // the child elements of the nested list, instead of #items. For
1072 1073
          // convenience, we automatically move them into #items.
          if (!isset($child['#items'])) {
1074 1075 1076
            // This is the same condition as in
            // \Drupal\Core\Render\Element::children(), which cannot be used
            // here, since it triggers an error on string values.
1077 1078 1079 1080 1081 1082 1083 1084 1085
            foreach ($child as $child_key => $child_value) {
              if ($child_key[0] !== '#') {
                $child['#items'][$child_key] = $child_value;
                unset($child[$child_key]);
              }
            }
          }
          // Lastly, inherit the original theme variables of the current list.
          $child['#theme'] = $variables['theme_hook_original'];
1086
          $child['#list_type'] = $variables['list_type'];
1087 1088 1089 1090
        }
      }
    }

1091 1092 1093 1094 1095
    // Set the item's value and attributes for the template.
    $item = array(
      'value' => $item,
      'attributes' => new Attribute($attributes),
    );
1096
  }
Dries's avatar
Dries committed
1097 1098
}

1099
/**
1100
 * Returns HTML for an indentation div; used for drag and drop tables.
1101
 *
1102 1103 1104
 * @param $variables
 *   An associative array containing:
 *   - size: Optional. The number of indentations to create.
1105 1106
 *
 * @ingroup themeable
1107
 */
1108
function theme_indentation($variables) {
1109
  $output = '';
1110
  for ($n = 0; $n < $variables['size']; $n++) {
1111
    $output .= '<div class="js-indentation indentation">&nbsp;</div>';
1112 1113 1114 1115
  }
  return $output;
}

1116
/**
1117
 * Prepares variables for container templates.
1118
 *
1119
 * Default template: container.html.twig.
1120
 *
1121
 * @param array $variables
1122 1123 1124 1125
 *   An associative array containing:
 *   - element: An associative array containing the properties of the element.
 *     Properties used: #id, #attributes, #children.
 */
1126
function template_preprocess_container(&$variables) {
1127
  $variables['has_parent'] = FALSE;
1128
  $element = $variables['element'];
1129 1130
  // Ensure #attributes is set.
  $element += array('#attributes' => array());
1131 1132 1133 1134 1135 1136 1137

  // Special handling for form elements.
  if (isset($element['#array_parents'])) {
    // Assign an html ID.
    if (!isset($element['#attributes']['id'])) {
      $element['#attributes']['id'] = $element['#id'];
    }
1138
    $variables['has_parent'] = TRUE;
1139 1140
  }

1141 1142
  $variables['children'] = $element['#children'];
  $variables['attributes'] = $element['#attributes'];
1143 1144
}

1145
/**
1146
 * Prepares variables for maintenance task list templates.
1147
 *
1148
 * Default template: maintenance-task-list.html.twig.
1149 1150
 *
 * @param array $variables
1151 1152 1153 1154 1155 1156
 *   An associative array containing:
 *   - items: An associative array of maintenance tasks.
 *     It's the caller's responsibility to ensure this array's items contain no
 *     dangerous HTML such as SCRIPT tags.
 *   - active: The key for the currently active maintenance task.
 */
1157
function template_preprocess_maintenance_task_list(&$variables) {
1158 1159 1160 1161 1162
  $items = $variables['items'];
  $active = $variables['active'];

  $done = isset($items[$active]) || $active == NULL;
  foreach ($items as $k => $item) {
1163 1164
    $variables['tasks'][$k]['item'] = $item;
    $variables['tasks'][$k]['attributes'] = new Attribute();
1165
    if ($active == $k) {
1166
      $variables['tasks'][$k]['attributes']->addClass('is-active');
1167
      $variables['tasks'][$k]['status'] = t('active');
1168 1169 1170
      $done = FALSE;
    }
    else {
1171 1172 1173 1174
      if ($done) {
        $variables['tasks'][$k]['attributes']->addClass('done');
        $variables['tasks'][$k]['status'] = t('done');
      }
1175 1176 1177 1178
    }
  }
}

1179
/**
1180
 * Adds a default set of helper variables for preprocessors and templates.
1181
 *
1182 1183
 * This function is called for theme hooks implemented as templates only, not
 * for theme hooks implemented as functions. This preprocess function is the
1184
 * first in the sequence of preprocessing functions that are called when
1185
 * preparing variables for a template.
1186
 *
1187 1188
 * See the @link themeable Default theme implementations topic @endlink for
 * details.
1189
 */
1190
function template_preprocess(&$variables, $hook, $info) {
1191 1192 1193 1194 1195 1196 1197 1198
  // Merge in variables that don't depend on hook and don't change during a
  // single page request.
  // Use the advanced drupal_static() pattern, since this is called very often.
  static $drupal_static_fast;
  if (!isset($drupal_static_fast)) {
    $drupal_static_fast['default_variables'] = &drupal_static(__FUNCTION__);
  }
  $default_variables = &$drupal_static_fast['default_variables'];
1199
  if (!isset($default_variables)) {
1200 1201
    $default_variables = _template_preprocess_default_variables();
  }
1202 1203 1204 1205 1206 1207 1208 1209 1210 1211
  $variables += $default_variables;

  // When theming a render element, merge its #attributes into
  // $variables['attributes'].
  if (isset($info['render element'])) {
    $key = $info['render element'];
    if (isset($variables[$key]['#attributes'])) {
      $variables['attributes'] = NestedArray::mergeDeep($variables['attributes'], $variables[$key]['#attributes']);
    }
  }
1212 1213 1214
}

/**
1215
 * Returns hook-independent variables to template_preprocess().
1216 1217 1218 1219
 */
function _template_preprocess_default_variables() {
  // Variables that don't depend on a database connection.
  $variables = array(
1220 1221 1222
    'attributes' => array(),
    'title_attributes' => array(),
    'content_attributes' => array(),
1223 1224
    'title_prefix' => array(),
    'title_suffix' => array(),
1225 1226 1227
    'db_is_active' => !defined('MAINTENANCE_MODE'),
    'is_admin' => FALSE,
    'logged_in' => FALSE,
1228 1229
  );

1230
  // Give modules a chance to alter the default template variables.
1231
  \Drupal::moduleHandler()->alter('template_preprocess_default_variables', $variables);
1232

1233 1234 1235
  // Tell all templates where they are located.
  $variables['directory'] = \Drupal::theme()->getActiveTheme()->getPath();

1236
  return $variables;
1237 1238
}

1239
/**