theme.inc 67.8 KB
Newer Older
Dries's avatar
   
Dries committed
1
<?php
2

3
/**
Dries's avatar
   
Dries committed
4
 * @file
5
 * The theme system, which controls the output of Drupal.
Dries's avatar
   
Dries committed
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\Render\MarkupInterface;
15
use Drupal\Component\Utility\Unicode;
16
use Drupal\Core\Cache\CacheableDependencyInterface;
17
use Drupal\Core\Config\Config;
18
use Drupal\Core\Config\StorageException;
19
20
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\BubbleableMetadata;
21
use Drupal\Core\Render\RenderableInterface;
22
use Drupal\Core\Template\Attribute;
23
use Drupal\Core\Theme\ThemeSettings;
24
use Drupal\Component\Utility\NestedArray;
25
use Drupal\Core\Render\Element;
26
use Drupal\Core\Render\Markup;
27

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

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

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

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

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
 * 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';

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

72
/**
73
 * Gets the theme registry.
74
 *
75
 * @param bool $complete
76
 *   Optional boolean to indicate whether to return the complete theme registry
77
78
79
80
81
82
83
 *   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.
84
 *
85
 * @return
86
87
 *   The complete theme registry array, or an instance of the
 *   Drupal\Core\Utility\ThemeRegistry class.
88
 */
89
function theme_get_registry($complete = TRUE) {
90
  $theme_registry = \Drupal::service('theme.registry');
91
  if ($complete) {
92
    return $theme_registry->get();
93
94
  }
  else {
95
    return $theme_registry->getRuntime();
96
97
98
  }
}

99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/**
 * Returns an array of default theme features.
 *
 * @see \Drupal\Core\Extension\ThemeHandler::$defaultFeatures
 */
function _system_default_theme_features() {
  return array(
    'favicon',
    'logo',
    'node_user_picture',
    'comment_user_picture',
    'comment_user_verification',
  );
}

114
/**
115
116
117
118
 * 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.
119
 */
120
function drupal_theme_rebuild() {
121
  \Drupal::service('theme.registry')->reset();
122
123
}

124
/**
125
 * Allows themes and/or theme engines to discover overridden theme functions.
126
 *
127
 * @param array $cache
128
 *   The existing cache of theme hooks to test against.
129
 * @param array $prefixes
130
131
 *   An array of prefixes to test, in reverse order of importance.
 *
132
 * @return array
133
134
135
 *   The functions found, suitable for returning from hook_theme;
 */
function drupal_find_theme_functions($cache, $prefixes) {
136
  $implementations = [];
137
  $grouped_functions = \Drupal::service('theme.registry')->getPrefixGroupedUserFunctions($prefixes);
138
139
140

  foreach ($cache as $hook => $info) {
    foreach ($prefixes as $prefix) {
141
142
143
144
145
146
      // 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
147
      // the same base hook. To keep things simple, deep hierarchy of
148
149
150
151
      // 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.
152
      $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__');
153
154
155
156
      // 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]);
157
158
        if ($matches) {
          foreach ($matches as $match) {
159
            $new_hook = substr($match, strlen($prefix) + 1);
160
            $arg_name = isset($info['variables']) ? 'variables' : 'render element';
161
            $implementations[$new_hook] = array(
162
              'function' => $match,
163
              $arg_name => $info[$arg_name],
164
              'base hook' => $hook,
165
166
167
168
            );
          }
        }
      }
169
170
171
      // 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.
172
      if (function_exists($prefix . '_' . $hook)) {
173
        $implementations[$hook] = array(
174
          'function' => $prefix . '_' . $hook,
175
176
177
178
179
        );
      }
    }
  }

180
  return $implementations;
181
182
183
}

/**
184
 * Allows themes and/or theme engines to easily discover overridden templates.
185
186
187
188
189
190
191
192
193
 *
 * @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) {
194
  $implementations = array();
195

196
197
198
199
  // 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();
200
  foreach (\Drupal::service('theme_handler')->listInfo() as $theme_info) {
201
    if (!empty($theme_info->base_theme)) {
202
      $theme_paths[$theme_info->base_theme][$theme_info->getName()] = $theme_info->getPath();
203
204
205
206
207
208
209
    }
  }
  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]);
      }
210
211
    }
  }
212
  $theme = \Drupal::theme()->getActiveTheme()->getName();
213
  $subtheme_paths = isset($theme_paths[$theme]) ? $theme_paths[$theme] : array();
214

215
  // Escape the periods in the extension.
216
  $regex = '/' . str_replace('.', '\.', $extension) . '$/';
217
  // Get a listing of all template files in the path to search.
218
  $files = file_scan_directory($path, $regex, array('key' => 'filename'));
219
220
221
222

  // 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.
223
  foreach ($files as $template => $file) {
224
    // Ignore sub-theme templates for the current theme.
225
    if (strpos($file->uri, str_replace($subtheme_paths, '', $file->uri)) !== 0) {
226
227
      continue;
    }
228
229
    // Remove the extension from the filename.
    $template = str_replace($extension, '', $template);
230
231
232
233
    // Transform - in filenames to _ to match function naming scheme
    // for the purposes of searching.
    $hook = strtr($template, '-', '_');
    if (isset($cache[$hook])) {
234
      $implementations[$hook] = array(
235
        'template' => $template,
236
        'path' => dirname($file->uri),
237
238
      );
    }
239
240
241
242

    // Match templates based on the 'template' filename.
    foreach ($cache as $hook => $info) {
      if (isset($info['template'])) {
243
        $template_candidates = array($info['template'], str_replace($info['theme path'] . '/templates/', '', $info['template']));
244
245
246
247
248
249
250
251
        if (in_array($template, $template_candidates)) {
          $implementations[$hook] = array(
            'template' => $template,
            'path' => dirname($file->uri),
          );
        }
      }
    }
252
253
  }

254
  // Find templates that implement possible "suggestion" variants of registered
255
  // theme hooks and add those as new registered theme hooks. See
256
257
  // drupal_find_theme_functions() for more information about suggestions and
  // the use of 'pattern' and 'base hook'.
258
259
  $patterns = array_keys($files);
  foreach ($cache as $hook => $info) {
260
    $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__');
261
    if (!isset($info['base hook']) && !empty($pattern)) {
262
263
      // Transform _ in pattern to - to match file naming scheme
      // for the purposes of searching.
264
      $pattern = strtr($pattern, '_', '-');
265

266
      $matches = preg_grep('/^' . $pattern . '/', $patterns);
267
268
      if ($matches) {
        foreach ($matches as $match) {
269
          $file = $match;
270
271
          // Remove the extension from the filename.
          $file = str_replace($extension, '', $file);
272
273
          // Put the underscores back in for the hook name and register this
          // pattern.
274
          $arg_name = isset($info['variables']) ? 'variables' : 'render element';
275
          $implementations[strtr($file, '-', '_')] = array(
276
            'template' => $file,
277
            'path' => dirname($files[$match]->uri),
278
            $arg_name => $info[$arg_name],
279
            'base hook' => $hook,
280
281
282
283
284
          );
        }
      }
    }
  }
285
  return $implementations;
286
287
}

Dries's avatar
   
Dries committed
288
/**
289
 * Retrieves a setting for the current theme or for a given theme.
Dries's avatar
   
Dries committed
290
 *
291
292
293
294
295
296
 * 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.
Dries's avatar
   
Dries committed
297
298
 *
 * @param $setting_name
299
 *   The name of the setting to be retrieved.
300
 * @param $theme
301
302
 *   The name of a given theme; defaults to the current theme.
 *
Dries's avatar
   
Dries committed
303
304
305
 * @return
 *   The value of the requested setting, NULL if the setting does not exist.
 */
306
function theme_get_setting($setting_name, $theme = NULL) {
307
  /** @var \Drupal\Core\Theme\ThemeSettings[] $cache */
308
  $cache = &drupal_static(__FUNCTION__, array());
Dries's avatar
   
Dries committed
309

310
  // If no key is given, use the current theme if we can determine it.
311
  if (!isset($theme)) {
312
    $theme = \Drupal::theme()->getActiveTheme()->getName();
313
  }
Dries's avatar
   
Dries committed
314

315
  if (empty($cache[$theme])) {
316
317
    // Create a theme settings object.
    $cache[$theme] = new ThemeSettings($theme);
318
319
    // Get the global settings from configuration.
    $cache[$theme]->setData(\Drupal::config('system.theme.global')->get());
320

321
322
    // Get the values for the theme-specific settings from the .info.yml files
    // of the theme and all its base themes.
323
324
    $themes = \Drupal::service('theme_handler')->listInfo();
    if (isset($themes[$theme])) {
325
326
      $theme_object = $themes[$theme];

327
328
329
330
331
332
333
334
      // Retrieve configured theme-specific settings, if any.
      try {
        if ($theme_settings = \Drupal::config($theme . '.settings')->get()) {
          $cache[$theme]->merge($theme_settings);
        }
      }
      catch (StorageException $e) {
      }
335

336
337
338
      // 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'])) {
339
        foreach (_system_default_theme_features() as $feature) {
340
          if (!in_array($feature, $theme_object->info['features'])) {
341
            $cache[$theme]->set('features.' . $feature, NULL);
342
343
344
345
          }
        }
      }

346
      // Generate the path to the logo image.
347
      if ($cache[$theme]->get('logo.use_default')) {
348
        $cache[$theme]->set('logo.url', file_url_transform_relative(file_create_url($theme_object->getPath() . '/logo.svg')));
349
350
      }
      elseif ($logo_path = $cache[$theme]->get('logo.path')) {
351
        $cache[$theme]->set('logo.url', file_url_transform_relative(file_create_url($logo_path)));
352
      }
353
354

      // Generate the path to the favicon.
355
356
357
      if ($cache[$theme]->get('features.favicon')) {
        $favicon_path = $cache[$theme]->get('favicon.path');
        if ($cache[$theme]->get('favicon.use_default')) {
358
          if (file_exists($favicon = $theme_object->getPath() . '/favicon.ico')) {
359
            $cache[$theme]->set('favicon.url', file_url_transform_relative(file_create_url($favicon)));
360
361
          }
          else {
362
            $cache[$theme]->set('favicon.url', file_url_transform_relative(file_create_url('core/misc/favicon.ico')));
363
364
          }
        }
365
        elseif ($favicon_path) {
366
          $cache[$theme]->set('favicon.url', file_url_transform_relative(file_create_url($favicon_path)));
367
368
        }
        else {
369
          $cache[$theme]->set('features.favicon', FALSE);
370
        }
371
      }
372
    }
Dries's avatar
   
Dries committed
373
374
  }

375
376
377
  return $cache[$theme]->get($setting_name);
}

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
/**
 * 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) {
405
406
407
408
409
410
411
412
413
414
415
  // If it's a renderable, then it'll be up to the generated render array it
  // returns to contain the necessary cacheability & attachment metadata. If
  // it doesn't implement CacheableDependencyInterface or AttachmentsInterface
  // then there is nothing to do here.
  if (!($arg instanceof RenderableInterface) && ($arg instanceof CacheableDependencyInterface || $arg instanceof AttachmentsInterface)) {
    $arg_bubbleable = [];
    BubbleableMetadata::createFromObject($arg)
      ->applyTo($arg_bubbleable);
    \Drupal::service('renderer')->render($arg_bubbleable);
  }

416
  if ($arg instanceof MarkupInterface) {
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
    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
432
    // http://php.net/manual/language.oop5.magic.php#object.tostring so
433
434
435
436
437
    // we also support a toString method.
    elseif (method_exists($arg, 'toString')) {
      $return = $arg->toString();
    }
    else {
438
      throw new \Exception('Object of type ' . get_class($arg) . ' cannot be printed.');
439
440
441
442
443
    }
  }

  // We have a string or an object converted to a string: Escape it!
  if (isset($return)) {
444
    return $return instanceof MarkupInterface ? $return : Html::escape($return);
445
446
447
448
449
450
451
452
453
454
455
456
457
  }

  // 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);
}

458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
/**
 * 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);
    }
476
    elseif ($key == 'logo_path') {
477
478
      $config->set('logo.path', $value);
    }
479
    elseif ($key == 'default_favicon') {
480
481
      $config->set('favicon.use_default', $value);
    }
482
    elseif ($key == 'favicon_path') {
483
484
      $config->set('favicon.path', $value);
    }
485
    elseif ($key == 'favicon_mimetype') {
486
487
      $config->set('favicon.mimetype', $value);
    }
488
    elseif (substr($key, 0, 7) == 'toggle_') {
489
      $config->set('features.' . Unicode::substr($key, 7), $value);
490
    }
491
    elseif (!in_array($key, array('theme', 'logo_upload'))) {
492
493
494
495
      $config->set($key, $value);
    }
  }
  return $config;
Dries's avatar
   
Dries committed
496
497
}

498
/**
499
500
501
 * Prepares variables for time templates.
 *
 * Default template: time.html.twig.
502
503
 *
 * @param array $variables
504
 *   An associative array possibly containing:
505
506
507
 *   - attributes['timestamp']:
 *   - timestamp:
 *   - text:
508
 */
509
function template_preprocess_time(&$variables) {
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
  // 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'];
    }
  }
}

529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
/**
 * 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'];
  }

576
  // Suppress error messages.
577
578
  $variables['errors'] = NULL;

579
580
581
582
  if (!empty($element['#description'])) {
    $variables['description'] = $element['#description'];
  }

583
  $variables['required'] = FALSE;
584
585
  // For required datetime fields 'form-required' & 'js-form-required' classes
  // are appended to the label attributes.
586
  if (!empty($element['#required'])) {
587
    $variables['required'] = TRUE;
588
589
590
591
  }
  $variables['content'] = $element['#children'];
}

Dries's avatar
   
Dries committed
592
/**
593
 * Prepares variables for links templates.
594
 *
595
596
 * Default template: links.html.twig.
 *
597
598
 * Unfortunately links templates duplicate the "active" class handling of l()
 * and LinkGenerator::generate() because it needs to be able to set the "active"
599
 * class not on the links themselves (<a> tags), but on the list items (<li>
600
601
602
603
604
605
 * 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:
 * jQuery('li:has("a.is-active")')
 *
606
 * @param array $variables
607
 *   An associative array containing:
608
609
 *   - links: An array of links to be themed. Each link should be itself an
 *     array, with the following elements:
610
 *     - title: The link text.
611
612
 *     - url: (optional) The \Drupal\Core\Url object to link to. If omitted, no
 *       anchor tag is printed out.
613
614
 *     - attributes: (optional) Attributes for the anchor, or for the <span>
 *       tag used in its place if no 'href' is supplied. If element 'class' is
615
 *       included, it must be an array of one or more class names.
616
617
 *     If the 'href' element is supplied, the entire link array is passed to
 *     l() as its $options parameter.
618
619
 *   - attributes: A keyed array of attributes for the <ul> containing the list
 *     of links.
620
 *   - set_active_class: (optional) Whether each link should compare the
621
 *     route_name + route_parameters or href (path), language and query options
622
623
624
625
 *     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.
626
627
628
629
630
 *     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
631
 *     JavaScript is added in system_page_attachments().
632
633
634
 *   - 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:
635
636
 *     - text: The heading text.
 *     - level: The heading level (e.g. 'h2', 'h3').
637
 *     - attributes: (optional) An array of the CSS attributes for the heading.
638
 *     When using a string it will be used as the text of the heading and the
639
640
 *     level will default to 'h2'. Headings should be used on navigation menus
 *     and any list of links that consistently appears on multiple pages. To
641
 *     make the heading invisible use the 'visually-hidden' CSS class. Do not
642
643
 *     use 'display:none', which removes it from screen readers and assistive
 *     technology. Headings allow screen reader and keyboard only users to
644
645
646
 *     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.
647
 *
648
 * @see \Drupal\Core\Utility\LinkGenerator
649
 * @see \Drupal\Core\Utility\LinkGenerator::generate()
650
 * @see system_page_attachments()
Dries's avatar
   
Dries committed
651
 */
652
function template_preprocess_links(&$variables) {
653
  $links = $variables['links'];
654
  $heading = &$variables['heading'];
655

656
657
  if (!empty($links)) {
    // Prepend the heading to the list, if any.
658
    if (!empty($heading)) {
659
      // Convert a string heading into an array, using a <h2> tag by default.
660
      if (is_string($heading)) {
661
        $heading = array('text' => $heading);
662
      }
663
664
665
666
667
      // Merge in default array properties into $heading.
      $heading += array(
        'level' => 'h2',
        'attributes' => array(),
      );
668
669
      // Convert the attributes array into an Attribute object.
      $heading['attributes'] = new Attribute($heading['attributes']);
670
671
    }

672
    $variables['links'] = array();
673
    foreach ($links as $key => $link) {
674
      $item = array();
675
      $link += array(
676
        'ajax' => NULL,
677
        'url' => NULL,
678
679
      );

680
      $li_attributes = array();
681
      $keys = ['title', 'url'];
682
683
      $link_element = array(
        '#type' => 'link',
684
        '#title' => $link['title'],
685
        '#options' => array_diff_key($link, array_combine($keys, $keys)),
686
        '#url' => $link['url'],
687
        '#ajax' => $link['ajax'],
688
689
      );

690
691
      // Handle links and ensure that the active class is added on the LIs, but
      // only if the 'set_active_class' option is not empty.
692
      if (isset($link['url'])) {
693
        if (!empty($variables['set_active_class'])) {
694

695
696
          // Also enable set_active_class for the contained link.
          $link_element['#options']['set_active_class'] = TRUE;
697

698
          if (!empty($link['language'])) {
699
            $li_attributes['hreflang'] = $link['language']->getId();
700
701
702
703
704
705
706
707
708
709
          }

          // 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);
          }

710
711
712
713
714
715
716
717
718
          /** @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;
719
          }
720
        }
721

722
        $item['link'] = $link_element;
723
      }
724

725
      // Handle title-only text items.
726
      $item['text'] = $link['title'];
727
728
      if (isset($link['attributes'])) {
        $item['text_attributes'] = new Attribute($link['attributes']);
729
      }
730

731
732
      // Handle list item attributes.
      $item['attributes'] = new Attribute($li_attributes);
733

734
      // Add the item to the list of links.
735
      $variables['links'][$key] = $item;
736
    }
737
  }
Dries's avatar
   
Dries committed
738
}
Dries's avatar
   
Dries committed
739

Dries's avatar
   
Dries committed
740
/**
741
 * Prepares variables for image templates.
Dries's avatar
   
Dries committed
742
 *
743
744
745
 * Default template: image.html.twig.
 *
 * @param array $variables
746
 *   An associative array containing:
747
 *   - uri: Either the path of the image file (relative to base_path()) or a
748
 *     full URL.
749
750
 *   - width: The width of the image (if known).
 *   - height: The height of the image (if known).
751
752
753
754
755
 *   - 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
756
 *     accessibility requirements, so it is strongly encouraged for code
757
758
 *     building variables for image.html.twig templates to pass a meaningful
 *     value for this variable.
759
760
761
 *     - 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
762
763
764
 *   - 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.
765
 *   - srcset: Array of multiple URIs and sizes/multipliers.
766
767
 *   - 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
Dries's avatar
   
Dries committed
768
 */
769
function template_preprocess_image(&$variables) {
770
  if (!empty($variables['uri'])) {
771
    $variables['attributes']['src'] = file_url_transform_relative(file_create_url($variables['uri']));
772
773
774
775
776
777
778
  }
  // 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.
779
      $source = file_url_transform_relative(file_create_url($src['uri']));
780
781
782
783
784
785
786
787
788
789
      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);
  }
790

791
  foreach (array('width', 'height', 'alt', 'title', 'sizes') as $key) {
792
    if (isset($variables[$key])) {
793
794
795
796
797
      // If the property has already been defined in the attributes,
      // do not override, including NULL.
      if (array_key_exists($key, $variables['attributes'])) {
        continue;
      }
798
      $variables['attributes'][$key] = $variables[$key];
799
    }
Dries's avatar
   
Dries committed
800
  }
Dries's avatar
   
Dries committed
801
}
Dries's avatar
   
Dries committed
802

Dries's avatar
   
Dries committed
803
/**
804
 * Prepares variables for table templates.
805
 *
806
807
808
 * Default template: table.html.twig.
 *
 * @param array $variables
809
810
811
812
 *   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:
813
814
 *     - data: The localized title of the table column, as a string or render
 *       array.
815
 *     - field: The database field represented in the table column (required
816
 *       if user is to be able to sort on this column).
817
818
819
820
821
822
823
824
825
826
827
 *     - 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.
828
829
830
831
 *     - 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:
832
 *     - data: An array of cells.
833
 *     - Any HTML attributes, such as "class", to apply to the table row.
834
 *     - no_striping: A Boolean indicating that the row should receive no
835
 *       'even / odd' styling. Defaults to FALSE.
836
837
 *     Each cell can be either a string or an associative array with the
 *     following keys:
838
 *     - data: The string or render array to display in the table cell.
839
 *     - header: Indicates this cell is a header.
840
841
 *     - Any HTML attributes, such as "colspan", to apply to the table cell.
 *     Here's an example for $rows:
842
 *     @code
843
844
 *     $rows = array(
 *       // Simple row
845
 *       array(
846
 *         'Cell 1', 'Cell 2', 'Cell 3'
847
 *       ),
848
849
850
 *       // 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')
851
 *       ),
852
 *     );
853
 *     @endcode
854
855
 *   - footer: An array of table rows which will be printed within a <tfoot>
 *     tag, in the same format as the rows element (see above).
856
857
858
859
860
 *   - 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
861
862
863
864
 *       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
865
866
 *       associative array of HTML attributes.
 *     Here's an example for $colgroup:
867
 *     @code
868
 *     $colgroup = array(
869
 *       // <colgroup> with one <col> element.
870
 *       array(
871
 *         array(
872
 *           'class' => array('funky'), // Attribute for the <col> element.
873
874
 *         ),
 *       ),
875
 *       // <colgroup> with attributes and inner <col> elements.
876
877
878
 *       array(
 *         'data' => array(
 *           array(
879
 *             'class' => array('funky'), // Attribute for the <col> element.
880
881
 *           ),
 *         ),
882
 *         'class' => array('jazzy'), // Attribute for the <colgroup> element.
883
884
 *       ),
 *     );
885
 *     @endcode
886
887
888
889
 *     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.
890
891
 *   - empty: The message to display in an extra row if table does not have any
 *     rows.
Dries's avatar
   
Dries committed
892
 */
893
function template_preprocess_table(&$variables) {
894
  // Format the table columns:
895
896
  if (!empty($variables['colgroups'])) {
    foreach ($variables['colgroups'] as &$colgroup) {
897
898
      // Check if we're dealing with a simple or complex column
      if (isset($colgroup['data'])) {
899
900
901
        $cols = $colgroup['data'];
        unset($colgroup['data']);
        $colgroup_attributes = $colgroup;
902
903
904
      }
      else {
        $cols = $colgroup;
905
        $colgroup_attributes = array();
906
      }
907
908
909
910
911
912
913
914
      $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);
915
916
917
918
919
        }
      }
    }
  }

920
921
922
  // Build an associative array of responsive classes keyed by column.
  $responsive_classes = array();

923
  // Format the table header:
924
  $ts = array();
925
  $header_columns = 0;
926
927
928
  if (!empty($variables['header'])) {
    $ts = tablesort_init($variables['header']);

929
930
931
    // Use a separate index with responsive classes as headers
    // may be associative.
    $responsive_index = -1;
932
    foreach ($variables['header'] as $col_key => $cell) {
933
934
935
      // Increase the responsive index.
      $responsive_index++;

936
      if (!is_array($cell)) {
937
        $header_columns++;
938
        $cell_content = $cell;
939
        $cell_attributes = new Attribute();
940
941
942
        $is_header = TRUE;
      }
      else {
943
944
945
946
947
948
        if (isset($cell['colspan'])) {
          $header_columns += $cell['colspan'];
        }
        else {
          $header_columns++;
        }
949
950
951
952
        $cell_content = '';
        if (isset($cell['data'])) {
          $cell_content = $cell['data'];
          unset($cell['data']);
953
        }
954
955
956
957
958
959
960
961
962
963
        // 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