jsDelivr is a free multi-CDN infrastructure that uses MaxCDN, Cloudflare and many others to combine their powers for the good of the open source community... read more', array(
'!jsdelivr' => 'https://www.jsdelivr.com',
'!jsdelivr_about' => 'https://www.jsdelivr.com/about',
'!maxcdn' => 'https://www.maxcdn.com',
'!cloudflare' => 'https://www.cloudflare.com',
));
}
/**
* {@inheritdoc}
*/
public function getLabel() {
return t('jsDelivr');
}
/**
* {@inheritdoc}
*/
protected function discoverCdnAssets($version, $theme = 'bootstrap') {
$themes = $this->getCdnThemes($version);
return isset($themes[$theme]) ? $themes[$theme] : array();
}
/**
* {@inheritdoc}
*/
public function getCdnThemes($version = NULL) {
if (!isset($version)) {
$version = $this->getCdnVersion();
}
if (!isset($this->themes[$version])) {
$instance = $this;
$this->themes[$version] = $this->cacheGet('themes.' . static::escapeDelimiter($version), array(), function ($themes) use ($version, $instance) {
return $instance->getCdnThemePhp53Callback($themes, $version);
});
}
return $this->themes[$version];
}
/**
* Callback to get around PHP 5.3's limitation of automatic binding of $this.
*
* @see https://www.drupal.org/project/bootstrap/issues/3054809
*
* {@inheritdoc}
*/
public function getCdnThemePhp53Callback($themes, $version) {
foreach (array('bootstrap', 'bootswatch') as $package) {
$mappedVersion = $this->mapVersion($version, $package);
$files = $this->requestApiV1($package, $mappedVersion);
$themes = $this->parseThemes($files, $package, $mappedVersion, $themes);
}
return $themes;
}
/**
* {@inheritdoc}
*/
public function getCdnVersions($package = 'bootstrap') {
if (!isset($this->versions[$package])) {
$instance = $this;
$this->versions[$package] = $this->cacheGet("versions.$package", array(), function ($versions) use ($package, $instance) {
return $instance->getCdnVersionsPhp53Callback($versions, $package);
});
}
return $this->versions[$package];
}
/**
* Callback to get around PHP 5.3's limitation of automatic binding of $this.
*
* @see https://www.drupal.org/project/bootstrap/issues/3054809
*
* {@inheritdoc}
*/
public function getCdnVersionsPhp53Callback($versions, $package) {
$json = $this->requestApiV1($package) + array('versions' => array());
foreach ($json['versions'] as $version) {
// Skip irrelevant versions.
if (!preg_match('/^' . substr(BOOTSTRAP_VERSION, 0, 1) . '\.\d+\.\d+$/', $version)) {
continue;
}
$versions[$version] = $version;
}
return $versions;
}
/**
* {@inheritdoc}
*/
protected function mapVersion($version, $package = NULL) {
// While the Bootswatch project attempts to maintain version parity with
// Bootstrap, it doesn't always happen. This causes issues when the system
// expects a 1:1 version match between Bootstrap and Bootswatch.
// @see https://github.com/thomaspark/bootswatch/issues/892#ref-issue-410070082
if ($package === 'bootswatch') {
switch ($version) {
// This version is "broken" because of jsDelivr's API limit.
case '3.4.1':
$version = '3.4.0';
break;
// This version doesn't exist.
case '3.1.1':
$version = '3.2.0';
break;
}
}
return $version;
}
/**
* Parses JSON from the API and retrieves valid files.
*
* @param array $json
* The JSON data to parse.
*
* @return array
* An array of files parsed from provided JSON data.
*/
protected function parseFiles(array $json) {
// Immediately return if malformed.
if (!isset($json['files']) || !is_array($json['files'])) {
return array();
}
$files = array();
foreach ($json['files'] as $file) {
// Skip old bootswatch file structure.
if (preg_match('`^/2|/bower_components`', $file['name'], $matches)) {
continue;
}
preg_match('`([^/]*)/bootstrap(-theme)?(\.min)?\.(js|css)$`', $file['name'], $matches);
if (!empty($matches[1]) && !empty($matches[4])) {
$files[] = $file['name'];
}
}
return $files;
}
/**
* Extracts assets from files provided by the jsDelivr API.
*
* This will place the raw files into proper "css", "js" and "min" arrays
* (if they exist) and prepends them with a base URL provided.
*
* @param array $files
* An array of files to process.
* @param string $package
* The base URL each one of the $files are relative to, this usually
* should also include the version path prefix as well.
* @param string $version
* A specific version to use.
* @param array $themes
* An existing array of themes. This is primarily used when building a
* complete list of themes.
*
* @return array
* An associative array containing the following keys, if there were
* matching files found:
* - css
* - js
* - min:
* - css
* - js
*/
protected function parseThemes(array $files, $package, $version, array $themes = array()) {
$baseUrl = static::BASE_CDN_URL . "/$package@$version";
foreach ($files as $file) {
preg_match('`([^/]*)/bootstrap(-theme)?(\.min)?\.(js|css)$`', $file, $matches);
if (!empty($matches[1]) && !empty($matches[4])) {
$path = $matches[1];
$min = $matches[3];
$filetype = $matches[4];
// Determine the "theme" name.
if ($path === 'css' || $path === 'js') {
$theme = 'bootstrap';
$title = (string) t('Bootstrap');
}
else {
$theme = $path;
$title = ucfirst($path);
}
if ($matches[2]) {
$theme = 'bootstrap_theme';
$title = (string) t('Bootstrap Theme');
}
$themes[$theme]['title'] = $title;
if ($min) {
$themes[$theme]['min'][$filetype][] = "$baseUrl/" . ltrim($file, '/');
}
else {
$themes[$theme][$filetype][] = "$baseUrl/" . ltrim($file, '/');
}
}
}
// Post process the themes to fill in any missing assets.
foreach (array_keys($themes) as $theme) {
// Some themes do not have a non-minified version, clone them to the
// "normal" css/js arrays to ensure that the theme still loads if
// aggregation (minification) is disabled.
foreach (array('css', 'js') as $type) {
if (!isset($themes[$theme][$type]) && isset($themes[$theme]['min'][$type])) {
$themes[$theme][$type] = $themes[$theme]['min'][$type];
}
}
// Prepend the main Bootstrap styles before the Bootstrap theme.
if ($theme === 'bootstrap_theme') {
if (isset($themes['bootstrap']['css'])) {
$themes[$theme]['css'] = array_unique(array_merge($themes['bootstrap']['css'], isset($themes[$theme]['css']) ? $themes[$theme]['css'] : array()));
}
if (isset($themes['bootstrap']['min']['css'])) {
$themes[$theme]['min']['css'] = array_unique(array_merge($themes['bootstrap']['min']['css'], isset($themes[$theme]['min']['css']) ? $themes[$theme]['min']['css'] : array()));
}
}
// Populate missing JavaScript.
if (!isset($themes[$theme]['js']) && isset($themes['bootstrap']['js'])) {
$themes[$theme]['js'] = $themes['bootstrap']['js'];
}
if (!isset($themes[$theme]['min']['js']) && isset($themes['bootstrap']['min']['js'])) {
$themes[$theme]['min']['js'] = $themes['bootstrap']['min']['js'];
}
}
return $themes;
}
/**
* Requests JSON from jsDelivr's API V1.
*
* @param string $package
* The NPM package being requested.
* @param string $version
* A specific version of $package to request. If not provided, a list of
* available versions will be returned.
*
* @return array
* The JSON data from the API.
*/
protected function requestApiV1($package, $version = NULL) {
$url = static::BASE_API_URL . "/$package";
// If no version was passed, then all versions are returned.
if (!$version) {
return $this->requestJson($url);
}
$json = $this->requestJson("$url@$version/flat");
// If bootstrap JSON could not be returned, provide defaults.
if (!$json && $package === 'bootstrap') {
return array(
'/dist/css/bootstrap.css',
'/dist/js/bootstrap.js',
'/dist/css/bootstrap.min.css',
'/dist/js/bootstrap.min.js',
);
}
// Parse the files from JSON.
return $this->parseFiles($json);
}
}