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

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

11
use Drupal\Component\Serialization\Json;
12
use Drupal\Component\Utility\Html;
13
use Drupal\Component\Utility\SafeMarkup;
14
use Drupal\Component\Utility\String;
15
use Drupal\Component\Utility\Unicode;
16
use Drupal\Component\Utility\UrlHelper;
17
use Drupal\Component\Utility\Xss;
18
use Drupal\Core\Config\Config;
19
use Drupal\Core\Config\StorageException;
20
use Drupal\Core\Extension\Extension;
21
use Drupal\Core\Extension\ExtensionNameLengthException;
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 Symfony\Component\HttpFoundation\Request;
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
74
/**
 * Determines if a theme is available to use.
 *
75
 * @param string|\Drupal\Core\Extension\Extension $theme
76
77
 *   Either the name of a theme or a full theme object.
 *
78
 * @return bool
79
 *   Boolean TRUE if the theme is installed or is the site administration theme;
80
 *   FALSE otherwise.
81
 *
82
83
84
85
 * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
 *   Use \Drupal::service('access_check.theme')->checkAccess().
 *
 * @see \Drupal\Core\Theme\ThemeAccessCheck::checkAccess().
86
87
 */
function drupal_theme_access($theme) {
88
  if ($theme instanceof Extension) {
89
    $theme = $theme->getName();
90
  }
91
  return \Drupal::service('access_check.theme')->checkAccess($theme);
92
93
}

94
/**
95
 * Gets the theme registry.
96
 *
97
 * @param bool $complete
98
 *   Optional boolean to indicate whether to return the complete theme registry
99
100
101
102
103
104
105
 *   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.
106
 *
107
 * @return
108
109
 *   The complete theme registry array, or an instance of the
 *   Drupal\Core\Utility\ThemeRegistry class.
110
 */
111
function theme_get_registry($complete = TRUE) {
112
  $theme_registry = \Drupal::service('theme.registry');
113
  if ($complete) {
114
    return $theme_registry->get();
115
116
  }
  else {
117
    return $theme_registry->getRuntime();
118
119
120
121
  }
}

/**
122
123
124
125
 * 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.
126
 */
127
function drupal_theme_rebuild() {
128
  \Drupal::service('theme.registry')->reset();
129
130
}

Dries Buytaert's avatar
   
Dries Buytaert committed
131
/**
132
 * Returns a list of all currently available themes.
Dries Buytaert's avatar
   
Dries Buytaert committed
133
 *
134
135
 * Retrieved from the database, if available and the site is not in maintenance
 * mode; otherwise compiled freshly from the filesystem.
136
 *
Dries Buytaert's avatar
   
Dries Buytaert committed
137
 * @param $refresh
138
 *   Whether to reload the list of themes from the database. Defaults to FALSE.
139
 *
140
141
 * @return array
 *   An associative array of the currently available themes.
142
 *
143
144
145
146
 * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
 *   Use \Drupal::service('theme_handler')->listInfo().
 *
 * @see \Drupal\Core\Extension\ThemeHandler::listInfo().
147
 */
148
function list_themes($refresh = FALSE) {
149
150
  /** @var \Drupal\Core\Extension\ThemeHandler $theme_handler */
  $theme_handler = \Drupal::service('theme_handler');
Dries Buytaert's avatar
   
Dries Buytaert committed
151
152

  if ($refresh) {
153
    $theme_handler->reset();
154
    system_list_reset();
Dries Buytaert's avatar
   
Dries Buytaert committed
155
156
  }

157
  return $theme_handler->listInfo();
Dries Buytaert's avatar
   
Dries Buytaert committed
158
159
}

160
/**
161
 * Allows themes and/or theme engines to discover overridden theme functions.
162
163
164
165
166
167
 *
 * @param $cache
 *   The existing cache of theme hooks to test against.
 * @param $prefixes
 *   An array of prefixes to test, in reverse order of importance.
 *
168
 * @return $implementations
169
170
171
 *   The functions found, suitable for returning from hook_theme;
 */
function drupal_find_theme_functions($cache, $prefixes) {
172
  $implementations = array();
173
174
175
176
  $functions = get_defined_functions();

  foreach ($cache as $hook => $info) {
    foreach ($prefixes as $prefix) {
177
178
179
180
181
182
      // 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
183
      // the same base hook. To keep things simple, deep hierarchy of
184
185
186
187
      // 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.
188
      $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__');
189
      if (!isset($info['base hook']) && !empty($pattern)) {
190
        $matches = preg_grep('/^' . $prefix . '_' . $pattern . '/', $functions['user']);
191
192
        if ($matches) {
          foreach ($matches as $match) {
193
            $new_hook = substr($match, strlen($prefix) + 1);
194
            $arg_name = isset($info['variables']) ? 'variables' : 'render element';
195
            $implementations[$new_hook] = array(
196
              'function' => $match,
197
              $arg_name => $info[$arg_name],
198
              'base hook' => $hook,
199
200
201
202
            );
          }
        }
      }
203
204
205
      // 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.
206
      if (function_exists($prefix . '_' . $hook)) {
207
        $implementations[$hook] = array(
208
          'function' => $prefix . '_' . $hook,
209
210
211
212
213
        );
      }
    }
  }

214
  return $implementations;
215
216
217
}

/**
218
 * Allows themes and/or theme engines to easily discover overridden templates.
219
220
221
222
223
224
225
226
227
 *
 * @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) {
228
  $implementations = array();
229

230
231
232
233
  // 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();
234
  foreach (list_themes() as $theme_info) {
235
    if (!empty($theme_info->base_theme)) {
236
      $theme_paths[$theme_info->base_theme][$theme_info->getName()] = $theme_info->getPath();
237
238
239
240
241
242
243
    }
  }
  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]);
      }
244
245
    }
  }
246
  $theme = \Drupal::theme()->getActiveTheme()->getName();
247
  $subtheme_paths = isset($theme_paths[$theme]) ? $theme_paths[$theme] : array();
248

249
  // Escape the periods in the extension.
250
  $regex = '/' . str_replace('.', '\.', $extension) . '$/';
251
  // Get a listing of all template files in the path to search.
252
  $files = file_scan_directory($path, $regex, array('key' => 'filename'));
253
254
255
256

  // 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.
257
  foreach ($files as $template => $file) {
258
    // Ignore sub-theme templates for the current theme.
259
    if (strpos($file->uri, str_replace($subtheme_paths, '', $file->uri)) !== 0) {
260
261
      continue;
    }
262
263
    // Remove the extension from the filename.
    $template = str_replace($extension, '', $template);
264
265
266
267
    // Transform - in filenames to _ to match function naming scheme
    // for the purposes of searching.
    $hook = strtr($template, '-', '_');
    if (isset($cache[$hook])) {
268
      $implementations[$hook] = array(
269
        'template' => $template,
270
        'path' => dirname($file->uri),
271
272
      );
    }
273
274
275
276

    // Match templates based on the 'template' filename.
    foreach ($cache as $hook => $info) {
      if (isset($info['template'])) {
277
        $template_candidates = array($info['template'], str_replace($info['theme path'] . '/templates/', '', $info['template']));
278
279
280
281
282
283
284
285
        if (in_array($template, $template_candidates)) {
          $implementations[$hook] = array(
            'template' => $template,
            'path' => dirname($file->uri),
          );
        }
      }
    }
286
287
  }

288
  // Find templates that implement possible "suggestion" variants of registered
289
  // theme hooks and add those as new registered theme hooks. See
290
291
  // drupal_find_theme_functions() for more information about suggestions and
  // the use of 'pattern' and 'base hook'.
292
293
  $patterns = array_keys($files);
  foreach ($cache as $hook => $info) {
294
    $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__');
295
    if (!isset($info['base hook']) && !empty($pattern)) {
296
297
      // Transform _ in pattern to - to match file naming scheme
      // for the purposes of searching.
298
      $pattern = strtr($pattern, '_', '-');
299

300
      $matches = preg_grep('/^' . $pattern . '/', $patterns);
301
302
      if ($matches) {
        foreach ($matches as $match) {
303
          $file = $match;
304
305
          // Remove the extension from the filename.
          $file = str_replace($extension, '', $file);
306
307
          // Put the underscores back in for the hook name and register this
          // pattern.
308
          $arg_name = isset($info['variables']) ? 'variables' : 'render element';
309
          $implementations[strtr($file, '-', '_')] = array(
310
            'template' => $file,
311
            'path' => dirname($files[$match]->uri),
312
            $arg_name => $info[$arg_name],
313
            'base hook' => $hook,
314
315
316
317
318
          );
        }
      }
    }
  }
319
  return $implementations;
320
321
}

Dries Buytaert's avatar
   
Dries Buytaert committed
322
/**
323
 * Retrieves a setting for the current theme or for a given theme.
Dries Buytaert's avatar
   
Dries Buytaert committed
324
 *
325
326
327
328
329
330
 * 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 Buytaert's avatar
   
Dries Buytaert committed
331
332
 *
 * @param $setting_name
333
 *   The name of the setting to be retrieved.
334
 * @param $theme
335
336
 *   The name of a given theme; defaults to the current theme.
 *
Dries Buytaert's avatar
   
Dries Buytaert committed
337
338
339
 * @return
 *   The value of the requested setting, NULL if the setting does not exist.
 */
340
function theme_get_setting($setting_name, $theme = NULL) {
341
  /** @var \Drupal\Core\Theme\ThemeSettings[] $cache */
342
  $cache = &drupal_static(__FUNCTION__, array());
Dries Buytaert's avatar
   
Dries Buytaert committed
343

344
  // If no key is given, use the current theme if we can determine it.
345
  if (!isset($theme)) {
346
    $theme = \Drupal::theme()->getActiveTheme()->getName();
347
  }
Dries Buytaert's avatar
   
Dries Buytaert committed
348

349
  if (empty($cache[$theme])) {
350
351
    // Create a theme settings object.
    $cache[$theme] = new ThemeSettings($theme);
352
353
    // Get the global settings from configuration.
    $cache[$theme]->setData(\Drupal::config('system.theme.global')->get());
354

355
356
    $themes = \Drupal::service('theme_handler')->listInfo();
    if (isset($themes[$theme])) {
357
358
      $theme_object = $themes[$theme];

359
360
361
362
363
364
365
366
      // Retrieve configured theme-specific settings, if any.
      try {
        if ($theme_settings = \Drupal::config($theme . '.settings')->get()) {
          $cache[$theme]->merge($theme_settings);
        }
      }
      catch (StorageException $e) {
      }
367

368
369
370
      // 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'])) {
371
        foreach (_system_default_theme_features() as $feature) {
372
          if (!in_array($feature, $theme_object->info['features'])) {
373
            $cache[$theme]->set('features.' . $feature, NULL);
374
375
376
377
          }
        }
      }

378
      // Generate the path to the logo image.
379
380
381
      if ($cache[$theme]->get('features.logo')) {
        $logo_path = $cache[$theme]->get('logo.path');
        if ($cache[$theme]->get('logo.use_default')) {
382
          $cache[$theme]->set('logo.url', file_create_url($theme_object->getPath() . '/logo.png'));
383
        }
384
385
        elseif ($logo_path) {
          $cache[$theme]->set('logo.url', file_create_url($logo_path));
386
387
        }
      }
388
389

      // Generate the path to the favicon.
390
391
392
      if ($cache[$theme]->get('features.favicon')) {
        $favicon_path = $cache[$theme]->get('favicon.path');
        if ($cache[$theme]->get('favicon.use_default')) {
393
          if (file_exists($favicon = $theme_object->getPath() . '/favicon.ico')) {
394
            $cache[$theme]->set('favicon.url', file_create_url($favicon));
395
396
          }
          else {
397
            $cache[$theme]->set('favicon.url', file_create_url('core/misc/favicon.ico'));
398
399
          }
        }
400
401
        elseif ($favicon_path) {
          $cache[$theme]->set('favicon.url', file_create_url($favicon_path));
402
403
        }
        else {
404
          $cache[$theme]->set('features.favicon', FALSE);
405
        }
406
      }
407
    }
Dries Buytaert's avatar
   
Dries Buytaert committed
408
409
  }

410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
  return $cache[$theme]->get($setting_name);
}

/**
 * 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_') {
444
      $config->set('features.' . Unicode::substr($key, 7), $value);
445
446
447
448
449
450
    }
    else if (!in_array($key, array('theme', 'logo_upload'))) {
      $config->set($key, $value);
    }
  }
  return $config;
Dries Buytaert's avatar
   
Dries Buytaert committed
451
452
}

453
/**
454
 * @addtogroup themeable
455
456
 * @{
 */
457

458
/**
459
460
461
 * Prepares variables for time templates.
 *
 * Default template: time.html.twig.
462
463
 *
 * @param array $variables
464
 *   An associative array possibly containing:
465
466
467
 *   - attributes['timestamp']:
 *   - timestamp:
 *   - text:
468
 */
469
function template_preprocess_time(&$variables) {
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
  // 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']);
      $variables['html'] = FALSE;
    }
    // Otherwise, use the literal datetime attribute.
    elseif (isset($variables['attributes']['datetime'])) {
      $variables['text'] = $variables['attributes']['datetime'];
      $variables['html'] = FALSE;
    }
  }
}

491
492
493
494
495
496
497
498
499
500
501
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
/**
 * 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'];
  }

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

542
  $variables['required'] = FALSE;
543
544
545
  // For required datetime fields a 'form-required' class is appended to the
  // label attributes.
  if (!empty($element['#required'])) {
546
    $variables['required'] = TRUE;
547
548
549
550
  }
  $variables['content'] = $element['#children'];
}

Dries Buytaert's avatar
   
Dries Buytaert committed
551
/**
552
 * Prepares variables for status message templates.
Dries Buytaert's avatar
   
Dries Buytaert committed
553
 *
554
 * Default template: status-messages.html.twig.
555
 *
556
 * @param array $variables
557
 *   An associative array containing:
558
559
 *   - display: (optional) May have a value of 'status' or 'error' when only
 *     displaying messages of that specific type.
Dries Buytaert's avatar
   
Dries Buytaert committed
560
 */
561
562
563
function template_preprocess_status_messages(&$variables) {
  $variables['message_list'] = drupal_get_messages($variables['display']);
  $variables['status_headings'] = array(
564
565
566
567
    'status' => t('Status message'),
    'error' => t('Error message'),
    'warning' => t('Warning message'),
  );
Dries Buytaert's avatar
   
Dries Buytaert committed
568
569
}

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

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

655
    $variables['links'] = array();
656
    foreach ($links as $key => $link) {
657
      $item = array();
658
      $link += array(
659
        'ajax' => NULL,
660
        'url' => NULL,
661
662
      );

663
      $li_attributes = array();
664
      $keys = ['title', 'url'];
665
666
      $link_element = array(
        '#type' => 'link',
667
        '#title' => $link['title'],
668
        '#options' => array_diff_key($link, array_combine($keys, $keys)),
669
        '#url' => $link['url'],
670
        '#ajax' => $link['ajax'],
671
672
      );

673
674
      // Handle links and ensure that the active class is added on the LIs, but
      // only if the 'set_active_class' option is not empty.
675
      if (isset($link['url'])) {
676
        if (!empty($variables['set_active_class'])) {
677

678
679
          // Also enable set_active_class for the contained link.
          $link_element['#options']['set_active_class'] = TRUE;
680

681
          if (!empty($link['language'])) {
682
            $li_attributes['hreflang'] = $link['language']->getId();
683
684
685
686
687
688
689
690
691
692
          }

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

693
694
695
696
697
698
699
700
701
          /** @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;
702
          }
703
        }
704

705
        $item['link'] = $link_element;
706
      }
707

708
      // Handle title-only text items.
709
710
      $text = (!empty($link['html']) ? $link['title'] : String::checkPlain($link['title']));
      $item['text'] = $text;
711
712
      if (isset($link['attributes'])) {
        $item['text_attributes'] = new Attribute($link['attributes']);
713
      }
714

715
716
      // Handle list item attributes.
      $item['attributes'] = new Attribute($li_attributes);
717

718
      // Add the item to the list of links.
719
      $variables['links'][$key] = $item;
720
    }
721
  }
Dries Buytaert's avatar
   
Dries Buytaert committed
722
}
Dries Buytaert's avatar
   
Dries Buytaert committed
723

Dries Buytaert's avatar
   
Dries Buytaert committed
724
/**
725
 * Prepares variables for image templates.
Dries Buytaert's avatar
   
Dries Buytaert committed
726
 *
727
728
729
 * Default template: image.html.twig.
 *
 * @param array $variables
730
 *   An associative array containing:
731
 *   - uri: Either the path of the image file (relative to base_path()) or a
732
 *     full URL.
733
734
 *   - width: The width of the image (if known).
 *   - height: The height of the image (if known).
735
736
737
738
739
 *   - 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
740
 *     accessibility requirements, so it is strongly encouraged for code
741
 *     calling _theme('image') to pass a meaningful value for this variable.
742
743
744
 *     - 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
745
746
747
 *   - 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.
748
 *   - srcset: Array of multiple URIs and sizes/multipliers.
749
750
 *   - 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 Buytaert's avatar
   
Dries Buytaert committed
751
 */
752
function template_preprocess_image(&$variables) {
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
  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);
  }
773

774
  foreach (array('width', 'height', 'alt', 'title', 'sizes') as $key) {
775
    if (isset($variables[$key])) {
776
777
778
779
780
      // If the property has already been defined in the attributes,
      // do not override, including NULL.
      if (array_key_exists($key, $variables['attributes'])) {
        continue;
      }
781
      $variables['attributes'][$key] = $variables[$key];
782
    }
Dries Buytaert's avatar
   
Dries Buytaert committed
783
  }
Dries Buytaert's avatar
   
Dries Buytaert committed
784
}
Dries Buytaert's avatar
   
Dries Buytaert committed
785

Dries Buytaert's avatar
   
Dries Buytaert committed
786
/**
787
 * Prepares variables for table templates.
788
 *
789
790
791
 * Default template: table.html.twig.
 *
 * @param array $variables
792
793
794
795
 *   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:
796
797
 *     - data: The localized title of the table column.
 *     - field: The database field represented in the table column (required
798
 *       if user is to be able to sort on this column).
799
800
801
802
803
804
805
806
807
808
809
 *     - 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.
810
811
812
813
 *     - 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:
814
 *     - data: An array of cells.
815
 *     - Any HTML attributes, such as "class", to apply to the table row.
816
 *     - no_striping: A Boolean indicating that the row should receive no
817
 *       'even / odd' styling. Defaults to FALSE.
818
819
 *     Each cell can be either a string or an associative array with the
 *     following keys:
820
821
 *     - data: The string to display in the table cell.
 *     - header: Indicates this cell is a header.
822
823
 *     - Any HTML attributes, such as "colspan", to apply to the table cell.
 *     Here's an example for $rows:
824
 *     @code
825
826
 *     $rows = array(
 *       // Simple row
827
 *       array(
828
 *         'Cell 1', 'Cell 2', 'Cell 3'
829
 *       ),
830
831
832
 *       // 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')
833
 *       ),
834
 *     );
835
 *     @endcode
836
837
 *   - footer: An array of table rows which will be printed within a <tfoot>
 *     tag, in the same format as the rows element (see above).
838
839
840
841
842
843
844
845
846
847
848
 *   - 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:
849
 *     @code
850
851
852
 *     $colgroup = array(
 *       // COLGROUP with one COL element.
 *       array(
853
 *         array(
854
 *           'class' => array('funky'), // Attribute for the COL element.
855
856
 *         ),
 *       ),
857
858
859
860
861
862
863
864
865
866
 *       // 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.
 *       ),
 *     );
867
 *     @endcode
868
869
870
871
 *     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.
872
873
 *   - empty: The message to display in an extra row if table does not have any
 *     rows.
Dries Buytaert's avatar
   
Dries Buytaert committed
874
 */
875
876
877
function template_preprocess_table(&$variables) {
  $is_sticky = !empty($variables['sticky']);
  $is_responsive = !empty($variables['responsive']);
878

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

905
906
907
  // Build an associative array of responsive classes keyed by column.
  $responsive_classes = array();

908
  // Format the table header:
909
  $ts = array();
910
  $header_columns = 0;
911
912
913
  if (!empty($variables['header'])) {
    $ts = tablesort_init($variables['header']);

914
915
916
    // Use a separate index with responsive classes as headers
    // may be associative.
    $responsive_index = -1;
917
    foreach ($variables['header'] as $col_key => $cell) {
918
919
920
      // Increase the responsive index.
      $responsive_index++;

921
      if (!is_array($cell)) {
922
        $header_columns++;
923
        $cell_content = $cell;
924
        $cell_attributes = new Attribute();
925
926
927
        $is_header = TRUE;
      }
      else {
928
929
930
931
932
933
        if (isset($cell['colspan'])) {
          $header_columns += $cell['colspan'];
        }
        else {
          $header_columns++;
        }
934
935
936
937
        $cell_content = '';
        if (isset($cell['data'])) {
          $cell_content = $cell['data'];
          unset($cell['data']);
938
        }
939
940