Projet

Général

Profil

Paste
Télécharger (21,6 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / themes / bootstrap / includes / cdn.inc @ 1f623f01

1
<?php
2
/**
3
 * @file
4
 * cdn.inc
5
 *
6
 * Provides necessary CDN integration.
7
 */
8

    
9
define('BOOTSTRAP_CDN_PROVIDER_PATH', 'public://bootstrap/cdn_providers');
10

    
11
/**
12
 * Retrieves a list of available CDN providers for the Bootstrap framework.
13
 *
14
 * @param string $provider
15
 *   A specific provider data to return.
16
 * @param bool $reset
17
 *   Toggle determining whether or not to reset the database cache.
18
 *
19
 * @return array|FALSE
20
 *   An associative array of CDN providers, keyed by their machine name if
21
 *   $provider is not set. If $provider is set and exists, its individual
22
 *   data array will be returned. If $provider is set and the data does not
23
 *   exist then FALSE will be returned.
24
 *
25
 * @todo Move to bootstrap_core in 7.x-4.x
26
 */
27
function bootstrap_cdn_provider($provider = NULL, $reset = FALSE) {
28
  $original_provider = $provider;
29

    
30
  // Use the advanced drupal_static() pattern, since this is called very often.
31
  static $drupal_static_fast;
32
  if (!isset($drupal_static_fast)) {
33
    $drupal_static_fast['providers'] = &drupal_static(__FUNCTION__);
34
  }
35
  $providers = &$drupal_static_fast['providers'];
36
  if ($reset || !isset($providers)) {
37
    $provider_path = BOOTSTRAP_CDN_PROVIDER_PATH;
38
    file_prepare_directory($provider_path, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
39

    
40
    $cid = 'theme_registry:bootstrap:cdn_providers';
41
    if (($cached = cache_get($cid)) && !empty($cached->data)) {
42
      $providers = $cached->data;
43
    }
44
    if ($reset || !isset($providers)) {
45
      $providers = array(
46
        'custom' => array(
47
          'title' => t('Custom'),
48
        ),
49
        'jsdelivr' => array(
50
          'api' => 'http://api.jsdelivr.com/v1/bootstrap/libraries',
51
          'title' => t('jsDelivr'),
52
          'description' =>  t('<p style="background:#EB4C36"><a href="!jsdelivr" target="_blank"><img src="http://www.jsdelivr.com/img/logo-34.png" alt="jsDelivr Logo" /></a></p><p><a href="!jsdelivr" target="_blank">jsDelivr</a> is a free multi-CDN infrastructure that uses <a href="!maxcdn" target="_blank">MaxCDN</a>, <a href="!cloudflare" target="_blank">Cloudflare</a> and many others to combine their powers for the good of the open source community... <a href="!jsdelivr_about" target="_blank">read more</a></p>', array(
53
            '!jsdelivr' => 'http://www.jsdelivr.com',
54
            '!jsdelivr_about' => 'http://www.jsdelivr.com/about',
55
            '!maxcdn' => 'http://www.maxcdn.com',
56
            '!cloudflare' => 'http://www.cloudflare.com',
57
          )),
58
        ),
59
      );
60

    
61
      // @todo Use drupal_alter() once CDN is in bootstrap_core companion module.
62
      // drupal_alter('bootstrap_cdn_provider', $providers);
63

    
64
      // Defaults properties each provider must have.
65
      $defaults = array(
66
        'api' => NULL,
67
        'css' => array(),
68
        'description' => NULL,
69
        'error' => FALSE,
70
        'js' => array(),
71
        'imported' => FALSE,
72
        'min' => array(
73
          'css' => array(),
74
          'js' => array(),
75
        ),
76
        'title' => NULL,
77
      );
78

    
79
      // Process the providers.
80
      foreach ($providers as $name => &$data) {
81
        $data += $defaults;
82
        $data['name'] = $name;
83
        if (empty($name)) {
84
          continue;
85
        }
86

    
87
        // Use manually imported API data, if it exists.
88
        $request = NULL;
89
        if (!empty($data['api']) && file_exists("$provider_path/$name.json") && ($imported_data = file_get_contents("$provider_path/$name.json"))) {
90
          $data['imported'] = TRUE;
91
          $request = new stdClass();
92
          $request->code = '200';
93
          $request->data = $imported_data;
94
        }
95
        // Otherwise, attempt to request API data if the provider has specified
96
        // an "api" URL to use.
97
        elseif (!empty($data['api'])) {
98
          $request = drupal_http_request($data['api']);
99
        }
100

    
101
        // Alter the specific provider.
102
        $function = 'bootstrap_bootstrap_cdn_provider_' . $name . '_alter';
103
        if (function_exists($function)) {
104
          $function($data, $request);
105
        }
106
        // @todo Use drupal_alter() once CDN is in bootstrap_core companion module.
107
        // drupal_alter('bootstrap_cdn_provider_' . $name, $data, $request);
108
      }
109
      cache_set($cid, $providers);
110
    }
111
  }
112
  if (isset($original_provider)) {
113
    if (!isset($providers[$original_provider])) {
114
      return FALSE;
115
    }
116
    return $providers[$original_provider];
117
  }
118
  return $providers;
119
}
120

    
121
/**
122
 * Implements hook_bootstrap_cdn_provider_PROVIDER_alter().
123
 *
124
 * @param array $provider
125
 *   The provider data array, passed by reference.
126
 * @param $request
127
 *   The raw request object, if the provider specified an "api" URL to retrieve
128
 *   data prior to this alter hook being called. It is up to whatever
129
 *   implements these hooks to parse the requested data.
130
 *
131
 * @todo Finish documentation when hook is real.
132
 */
133
function bootstrap_bootstrap_cdn_provider_jsdelivr_alter(&$provider, $request = NULL) {
134
  $json = array();
135
  $provider['versions'] = array();
136
  $provider['themes'] = array();
137

    
138
  if ($request->code === '200' && !empty($request->data)) {
139
    $json = drupal_json_decode($request->data);
140
  }
141

    
142
  // Expected library names from jsDelivr API v1. Must use "twitter-bootstrap"
143
  // instead of "bootstrap" (which is just a folder alias).
144
  // @see https://www.drupal.org/node/2504343
145
  // @see https://github.com/jsdelivr/api/issues/94
146
  $bootstrap = 'twitter-bootstrap';
147
  $bootswatch = 'bootswatch';
148

    
149
  // Extract the raw asset files from the JSON data for each framework.
150
  $libraries = array();
151
  if ($json) {
152
    foreach ($json as $data) {
153
      if ($data['name'] === $bootstrap || $data['name'] === $bootswatch) {
154
        foreach ($data['assets'] as $asset) {
155
          if (preg_match('/^' . BOOTSTRAP_VERSION_MAJOR . '\.\d\.\d$/', $asset['version'])) {
156
            $libraries[$data['name']][$asset['version']] = $asset['files'];
157
          }
158
        }
159
      }
160
    }
161
  }
162

    
163
  // If the main bootstrap library could not be found, then provide defaults.
164
  if (!isset($libraries[$bootstrap])) {
165
    $provider['error'] = TRUE;
166
    $provider['versions'][BOOTSTRAP_VERSION] = BOOTSTRAP_VERSION;
167
    $provider['themes'][BOOTSTRAP_VERSION] = array(
168
      'bootstrap' => array(
169
        'title' => t('Bootstrap'),
170
        'css' => array('//cdn.jsdelivr.net/bootstrap/' . BOOTSTRAP_VERSION . '/css/bootstrap.css'),
171
        'js' => array('//cdn.jsdelivr.net/bootstrap/' . BOOTSTRAP_VERSION . '/js/bootstrap.js'),
172
        'min' => array(
173
          'css' => array('//cdn.jsdelivr.net/bootstrap/' . BOOTSTRAP_VERSION . '/css/bootstrap.min.css'),
174
          'js' => array('//cdn.jsdelivr.net/bootstrap/' . BOOTSTRAP_VERSION . '/js/bootstrap.min.js'),
175
        ),
176
      ),
177
    );
178
    return;
179
  }
180

    
181
  // Populate the provider array with the versions and themes available.
182
  foreach (array_keys($libraries[$bootstrap]) as $version) {
183
    $provider['versions'][$version] = $version;
184

    
185
    if (!isset($provider['themes'][$version])) {
186
      $provider['themes'][$version] = array();
187
    }
188

    
189
    // Extract Bootstrap themes.
190
    $provider['themes'][$version] = drupal_array_merge_deep($provider['themes'][$version], _bootstrap_cdn_provider_jsdelivr_extract_themes($libraries[$bootstrap][$version], "//cdn.jsdelivr.net/bootstrap/$version"));
191

    
192
    // Extract Bootswatch themes.
193
    if (isset($libraries[$bootswatch][$version])) {
194
      $provider['themes'][$version] = drupal_array_merge_deep($provider['themes'][$version], _bootstrap_cdn_provider_jsdelivr_extract_themes($libraries[$bootswatch][$version], "//cdn.jsdelivr.net/bootswatch/$version"));
195
    }
196
  }
197

    
198
  // Post process the themes to fill in any missing assets.
199
  foreach (array_keys($provider['themes']) as $version) {
200
    foreach (array_keys($provider['themes'][$version]) as $theme) {
201
      // Some themes actually require Bootstrap framework assets to still
202
      // function properly.
203
      if ($theme !== 'bootstrap') {
204
        foreach (array('css', 'js') as $type) {
205
          // Bootswatch themes include the Bootstrap framework in their CSS.
206
          // Skip the CSS portions.
207
          if ($theme !== 'bootstrap_theme' && $type === 'css') {
208
            continue;
209
          }
210
          if (!isset($provider['themes'][$version][$theme][$type]) && !empty($provider['themes'][$version]['bootstrap'][$type])) {
211
            $provider['themes'][$version][$theme][$type] = array();
212
          }
213
          $provider['themes'][$version][$theme][$type] = drupal_array_merge_deep($provider['themes'][$version]['bootstrap'][$type], $provider['themes'][$version][$theme][$type]);
214
          if (!isset($provider['themes'][$version][$theme]['min'][$type]) && !empty($provider['themes'][$version]['bootstrap']['min'][$type])) {
215
            $provider['themes'][$version][$theme]['min'][$type] = array();
216
          }
217
          $provider['themes'][$version][$theme]['min'][$type] = drupal_array_merge_deep($provider['themes'][$version]['bootstrap']['min'][$type], $provider['themes'][$version][$theme]['min'][$type]);
218
        }
219
      }
220
      // Some themes do not have a non-minified version, clone them to the
221
      // "normal" css/js arrays to ensure that the theme still loads if
222
      // aggregation (minification) is disabled.
223
      foreach (array('css', 'js') as $type) {
224
        if (!isset($provider['themes'][$version][$theme][$type]) && isset($provider['themes'][$version][$theme]['min'][$type])) {
225
          $provider['themes'][$version][$theme][$type] = $provider['themes'][$version][$theme]['min'][$type];
226
        }
227
      }
228
    }
229
  }
230
}
231

    
232
/**
233
 * Extracts theme information from files provided by the jsDelivr API.
234
 *
235
 * This will place the raw files into proper "css", "js" and "min" arrays
236
 * (if they exist) and prepends them with a base URL provided.
237
 *
238
 * @param array $files
239
 *   An array of files to process.
240
 * @param string $base_url
241
 *   The base URL each one of the $files are relative to, this usually
242
 *   should also include the version path prefix as well.
243
 *
244
 * @return array
245
 *   An associative array containing the following keys, if there were
246
 *   matching files found:
247
 *   - css
248
 *   - js
249
 *   - min:
250
 *     - css
251
 *     - js
252
 */
253
function _bootstrap_cdn_provider_jsdelivr_extract_themes($files, $base_url = '') {
254
  $themes = array();
255
  foreach ($files as $file) {
256
    preg_match('`([^/]*)/bootstrap(-theme)?(\.min)?\.(js|css)$`', $file, $matches);
257
    if (!empty($matches[1]) && !empty($matches[4])) {
258
      $path = $matches[1];
259
      $min = $matches[3];
260
      $filetype = $matches[4];
261

    
262
      // Determine the "theme" name.
263
      if ($path === 'css' || $path === 'js') {
264
        $theme = 'bootstrap';
265
        $title = t('Bootstrap');
266
      }
267
      else {
268
        $theme = $path;
269
        $title = ucfirst($path);
270
      }
271
      if ($matches[2]) {
272
        $theme = 'bootstrap_theme';
273
        $title = t('Bootstrap Theme');
274
      }
275

    
276
      $themes[$theme]['title'] = $title;
277
      if ($min) {
278
        $themes[$theme]['min'][$filetype][] = "$base_url/$path/bootstrap{$matches[2]}$min.$filetype";
279
      }
280
      else {
281
        $themes[$theme][$filetype][] = "$base_url/$path/bootstrap{$matches[2]}$min.$filetype";
282
      }
283
    }
284
  }
285
  return $themes;
286
}
287

    
288
/**
289
 * Implements hook_bootstrap_cdn_provider_PROVIDER_assets_alter().
290
 *
291
 * @todo Create documentation when hook is real.
292
 */
293
function bootstrap_bootstrap_cdn_provider_custom_assets_alter(&$provider, $theme = NULL) {
294
  foreach (array('css', 'js') as $type) {
295
    if ($setting = bootstrap_setting('cdn_custom_' . $type, $theme)) {
296
      $provider[$type][] = $setting;
297
    }
298
    if ($setting = bootstrap_setting('cdn_custom_' . $type . '_min', $theme)) {
299
      $provider['min'][$type][] = $setting;
300
    }
301
  }
302
}
303

    
304
/**
305
 * Implements hook_bootstrap_cdn_provider_PROVIDER_assets_alter().
306
 *
307
 * @todo Create documentation when hook is real.
308
 */
309
function bootstrap_bootstrap_cdn_provider_jsdelivr_assets_alter(&$provider, $theme = NULL) {
310
  $error = !empty($provider['error']);
311
  $version = $error ? BOOTSTRAP_VERSION : bootstrap_setting('cdn_jsdelivr_version', $theme);
312
  $theme = $error ? 'bootstrap' : bootstrap_setting('cdn_jsdelivr_theme', $theme);
313
  if (isset($provider['themes'][$version][$theme])) {
314
    $provider = drupal_array_merge_deep($provider, $provider['themes'][$version][$theme]);
315
  }
316
}
317

    
318
/**
319
 * Custom callback for CDN provider settings.
320
 *
321
 * @see bootstrap_form_system_theme_settings_alter()
322
 */
323
function bootstrap_cdn_provider_settings_form(&$form, &$form_state, $theme) {
324
  // Retrieve the provider from form values or the setting.
325
  $provider = isset($form_state['values']['bootstrap_cdn_provider']) ? $form_state['values']['bootstrap_cdn_provider'] : bootstrap_setting('cdn_provider', $theme);
326

    
327
  // Intercept possible manual import of API data via AJAX callback.
328
  if (isset($form_state['clicked_button']['#value']) && $form_state['clicked_button']['#value'] === t('Save provider data')) {
329
    $provider_path = BOOTSTRAP_CDN_PROVIDER_PATH;
330
    file_prepare_directory($provider_path, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
331
    $provider_data = isset($form_state['values']['bootstrap_cdn_provider_import_data']) ? $form_state['values']['bootstrap_cdn_provider_import_data'] : FALSE;
332
    $provider_file = "$provider_path/$provider.json";
333
    if ($provider_data) {
334
      file_unmanaged_save_data($provider_data, $provider_file, FILE_EXISTS_REPLACE);
335
    }
336
    elseif ($provider_file && file_exists($provider_file)) {
337
      file_unmanaged_delete($provider_file);
338
    }
339
    bootstrap_cdn_provider(NULL, TRUE);
340
  }
341

    
342
  $form['advanced']['cdn'] = array(
343
    '#type' => 'fieldset',
344
    '#title' => t('CDN (Content Delivery Network)'),
345
    '#description' => '<div class="alert alert-info messages warning"><strong>' . t('NOTE') . ':</strong> ' . t('Using one of the "CDN Provider" options below is the preferred method for loading Bootstrap CSS and JS on simpler sites that do not use a site-wide CDN. Using a "CDN Provider" for loading Bootstrap, however, does mean that it depends on a third-party service. There is no obligation or commitment by these third-parties that guarantees any up-time or service quality. If you need to customize Bootstrap and have chosen to compile the source code locally (served from this site), you must disable the "CDN Provider" option below by choosing "- None -" and alternatively enable a site-wide CDN implementation. All local (served from this site) versions of Bootstrap will be superseded by any enabled "CDN Provider" below. <strong>Do not do both</strong>.') . '</div>',
346
    '#collapsible' => TRUE,
347
    '#collapsed' => !$provider,
348
  );
349

    
350
  $providers = bootstrap_cdn_provider();
351
  $options = array();
352
  foreach ($providers as $key => $data) {
353
    $options[$key] = $data['title'];
354
  }
355
  $form['advanced']['cdn']['bootstrap_cdn_provider'] = array(
356
    '#type' => 'select',
357
    '#title' => t('CDN Provider'),
358
    '#default_value' => $provider,
359
    '#empty_value' => '',
360
    '#options' => $options,
361
  );
362

    
363
  // Render each provider.
364
  foreach ($providers as $name => $data) {
365
    $form['advanced']['cdn']['provider'][$name] = array(
366
      '#type' => 'container',
367
      '#prefix' => '<div id="bootstrap-cdn-provider-' . $name . '">',
368
      '#suffix' => '</div>',
369
      '#states' => array(
370
        'visible' => array(
371
          ':input[name="bootstrap_cdn_provider"]' => array('value' => $name),
372
        ),
373
      ),
374
    );
375
    $element = &$form['advanced']['cdn']['provider'][$name];
376

    
377
    $element['description']['#markup'] = '<div class="lead">' . $data['description'] . '</div>';
378

    
379
    // Indicate there was an error retrieving the provider's API data.
380
    if (!empty($data['error']) || !empty($data['imported'])) {
381
      if (!empty($data['error'])) {
382
        $element['#prefix'] .= '<div class="alert alert-danger messages error"><strong>' . t('ERROR') . ':</strong> ' . t('Unable to reach or parse the data provided by the @title API. Ensure the server this website is hosted on is able to initiate HTTP requests via <a href="!drupal_http_request" target="_blank">drupal_http_request()</a>. If the request consistently fails, it is likely that there are certain PHP functions that have been disabled by the hosting provider for security reasons. It is possible to manually copy and paste the contents of the following URL into the "Imported @title data" section below.<br /><br /><a href="!provider_api" target="_blank">!provider_api</a>.', array(
383
            '@title' => $data['title'],
384
            '!provider_api' => $data['api'],
385
            '!drupal_http_request' => 'https://api.drupal.org/api/drupal/includes%21common.inc/function/drupal_http_request/7',
386
          )) . '</div>';
387
      }
388
      $element['import'] = array(
389
        '#type' => 'fieldset',
390
        '#title' => t('Imported @title data', array(
391
          '@title' => $data['title'],
392
        )),
393
        '#description' => t('The provider will attempt to parse the data entered here each time it is saved. If no data has been entered, any saved files associated with this provider will be removed and the provider will again attempt to request the API data normally through the following URL: <a href="!provider_api" target="_blank">!provider_api</a>.', array(
394
          '!provider_api' => $data['api'],
395
        )),
396
        '#weight' => 10,
397
        '#collapsible' => TRUE,
398
        '#collapsed' => TRUE,
399
      );
400
      $element['import']['bootstrap_cdn_provider_import_data'] = array(
401
        '#type' => 'textarea',
402
        '#default_value' => file_exists(BOOTSTRAP_CDN_PROVIDER_PATH . '/' . $name . '.json') ? file_get_contents(BOOTSTRAP_CDN_PROVIDER_PATH . '/' . $name . '.json') : NULL,
403
      );
404
      $element['import']['submit'] = array(
405
        '#type' => 'submit',
406
        '#value' => t('Save provider data'),
407
        '#executes_submit_callback' => FALSE,
408
        '#ajax' => array(
409
          'callback' => 'bootstrap_cdn_provider_settings_form_ajax_reload_provider',
410
          'wrapper' => 'bootstrap-cdn-provider-' . $name,
411
        ),
412
      );
413
    }
414

    
415
    // Alter the specific provider.
416
    $function = 'bootstrap_bootstrap_cdn_provider_' . $name . '_settings_form_alter';
417
    if (function_exists($function)) {
418
      $function($element, $form_state, $data, $theme);
419
    }
420
    // @todo Use drupal_alter() once CDN is in bootstrap_core companion module.
421
    // drupal_alter('bootstrap_cdn_provider_' . $name . '_settings_form', $element, $form_state, $data, $theme);
422
  }
423
}
424

    
425
/**
426
 * AJAX callback for reloading CDN provider elements.
427
 */
428
function bootstrap_cdn_provider_settings_form_ajax_reload_provider($form, $form_state) {
429
  return $form['advanced']['cdn']['provider'][$form_state['values']['bootstrap_cdn_provider']];
430
}
431

    
432
/**
433
 * Implements hook_bootstrap_cdn_provider_PROVIDER_settings_form_alter().
434
 */
435
function bootstrap_bootstrap_cdn_provider_custom_settings_form_alter(&$element, &$form_state, $provider = array(), $theme = NULL) {
436
  foreach (array('css', 'js') as $type) {
437
    $setting = bootstrap_setting('cdn_custom_' . $type, $theme);
438
    $setting_min = bootstrap_setting('cdn_custom_' . $type . '_min', $theme);
439
    $element['bootstrap_cdn_custom_' . $type] = array(
440
      '#type' => 'textfield',
441
      '#title' => t('Bootstrap @type URL', array(
442
        '@type' => drupal_strtoupper($type),
443
      )),
444
      '#description' => t('It is best to use protocol relative URLs (i.e. without http: or https:) here as it will allow more flexibility if the need ever arises.'),
445
      '#default_value' => $setting,
446
    );
447
    $element['bootstrap_cdn_custom_' . $type . '_min'] = array(
448
      '#type' => 'textfield',
449
      '#title' => t('Minified Bootstrap @type URL', array(
450
        '@type' => drupal_strtoupper($type),
451
      )),
452
      '#description' => t('Additionally, you can provide the minimized version of the file. It will be used instead if site aggregation is enabled.'),
453
      '#default_value' => $setting_min,
454
    );
455
  }
456
}
457

    
458
/**
459
 * Implements hook_bootstrap_cdn_provider_PROVIDER_settings_form_alter().
460
 */
461
function bootstrap_bootstrap_cdn_provider_jsdelivr_settings_form_alter(&$element, &$form_state, $provider = array(), $theme = NULL) {
462
  $version = isset($form_state['values']['bootstrap_cdn_jsdelivr_version']) ? $form_state['values']['bootstrap_cdn_jsdelivr_version'] : bootstrap_setting('cdn_jsdelivr_version', $theme);
463
  $element['bootstrap_cdn_jsdelivr_version'] = array(
464
    '#type' => 'select',
465
    '#title' => t('Version'),
466
    '#options' => isset($provider['versions']) ? $provider['versions'] : array(),
467
    '#default_value' => $version,
468
    '#ajax' => array(
469
      'callback' => 'bootstrap_cdn_provider_settings_form_ajax_reload_provider',
470
      'wrapper' => 'bootstrap-cdn-provider-jsdelivr',
471
    ),
472
  );
473
  if (empty($provider['error']) && empty($provider['imported'])) {
474
    $element['bootstrap_cdn_jsdelivr_version']['#description'] = t('These versions are automatically populated by the @provider API upon cache clear and newer versions may appear over time. It is highly recommended the version that the site was built with stays at that version. Until a newer version has been properly tested for updatability by the site maintainer, you should not arbitrarily "update" just because there is a newer version. This can cause many inconsistencies and undesired effects with an existing site.', array(
475
      '@provider' => $provider['title'],
476
    ));
477
  }
478

    
479
  // Bootswatch.
480
  $themes = array();
481
  if (isset($provider['themes'][$version])) {
482
    foreach ($provider['themes'][$version] as $_theme => $data) {
483
      $themes[$_theme] = $data['title'];
484
    }
485
  }
486
  $element['bootstrap_cdn_jsdelivr_theme'] = array(
487
    '#type' => 'select',
488
    '#title' => t('Theme'),
489
    '#description' => t('Choose the example <a href="!bootstrap_theme" target="_blank">Bootstrap Theme</a> provided by Bootstrap or one of the many, many <a href="!bootswatch" target="_blank">Bootswatch</a> themes!', array(
490
      '!bootswatch' => 'https://bootswatch.com',
491
      '!bootstrap_theme' => 'http://getbootstrap.com/examples/theme/',
492
    )),
493
    '#default_value' => bootstrap_setting('cdn_jsdelivr_theme', $theme),
494
    '#options' => $themes,
495
    '#empty_option' => t('Bootstrap (default)'),
496
    '#empty_value' => 'bootstrap',
497
    '#suffix' => '<div id="bootstrap-theme-preview"></div>',
498
  );
499
}