Commit a483bb17 authored by Tobias Stoeckler's avatar Tobias Stoeckler

#719896: by tstoeckler, sun: First implementation of full libraries detection...

#719896: by tstoeckler, sun: First implementation of full libraries detection and loading (including tests).
parent 93fff2bd
......@@ -22,20 +22,26 @@
* the actual library files in a sub-directory.
* - version callback: (optional) The name of a function that detects and
* returns the full version string of the library. Defaults to
* libraries_get_version().
* libraries_get_version(). The first argument is always $library, an array
* containing all library information as described here. The following
* argument(s) can either be:
* - options: An associative array of additional information to pass to the
* version callback. In this case the version arguments (see below) must
* be declared as an associative array.
* - or any number of independent arguments. In this case the version
* arguments (see below) must be declared as an indexed array.
* - version arguments: A list of arguments to pass to the version callback.
* The default version callback libraries_get_version() expects a single,
* single, associative array with named keys:
* associative array with named keys:
* - file: The filename to parse for the version, relative to the library
* path. For example: 'docs/changelog.txt'.
* - pattern: A string containing a regular expression (PCRE) to match the
* library version. For example: '/@version (\d+)\.(\d+)/'.
* - lines: The maximum number of lines to search the pattern in. For
* example: 20.
* - lines: (optional) The maximum number of lines to search the pattern in.
* Defaults to 20.
* - cols: (optional) The maximum number of characters per line to take into
* account. For example: 40. Defaults to unlimited. To be used if the file
* containing the library version is minified/compressed, i.e. reading a
* single line would read the entire library into memory.
* account. Defaults to 200. In case of minified or compressed files, this
* prevents reading the entire file into memory.
* - files: An associative array of library files to load. Supported keys are:
* - js: A list of JavaScript files to load, using the same syntax as Drupal
* core's hook_library().
......@@ -48,7 +54,22 @@
* uncompressed/source variant, those can be defined here. Each key should
* describe the variant type, e.g. 'minified' or 'source'. Each value is an
* associative array of top-level properties that are entirely overridden by
* the variant, most often just 'files'. Variants can be version specific.
* the variant, most often just 'files'. Additionally, each variant can
* contain following properties:
* - variant callback: (optional) The name of a function that detects
* returns TRUE or FALSE, depending on whether the variant is available or
* not. The first argument is always $library, an array containing all
* library information as described here. The seconds argument is always
* $name, a string containing the name of the variant. The following
* argument(s) can either be:
* - options: An associative array of additional information to pass to
* the version callback. In this case the version arguments (see below)
* must be declared as an associative array.
* - or any number of independent arguments. In this case the version
* arguments (see below) must be declared as an indexed array.
* If ommitted, the variant is expected to always be available. Variants
* can be version specific.
* - variant arguments: A list of arguments to pass to the variant callback.
* - versions: (optional) An associative array of supported library versions.
* Naturally, external libraries evolve over time and so do their APIs. In
* case a library changes between versions, different 'files' may need to be
......@@ -62,6 +83,8 @@
* the same notion as the top-level 'files' property. Each specified file
* should contain the full path to the file.
* Additional top-level properties can be registered as needed.
*
* @see hook_library()
*/
function hook_libraries_info() {
// The following is a full explanation of all properties. See below for more
......@@ -122,6 +145,10 @@ function hook_libraries_info() {
'skin/example.css',
),
),
'variant callback' => 'mymodule_check_variant',
'variant arguments' => array(
'variant' => 'minified',
),
),
),
// Optional, but usually required: Override top-level properties for later
......@@ -203,9 +230,9 @@ function hook_libraries_info() {
'download url' => 'http://tinymce.moxiecode.com/download.php',
'path' => 'jscripts/tiny_mce',
'version arguments' => array(
// It can be easier to parse the first chars of a minified file instead of
// doing a multi-line pattern matching in a source file. See 'lines' and
// 'cols' below.
// It can be easier to parse the first characters of a minified file
// instead of doing a multi-line pattern matching in a source file. See
// 'lines' and 'cols' below.
'file' => 'jscripts/tiny_mce/tiny_mce.js',
// Best practice: Document the actual version strings for later reference.
// 2.x: this.majorVersion="2";this.minorVersion="1.3"
......@@ -278,3 +305,20 @@ function hook_libraries_info() {
);
return $libraries;
}
/**
* Alter the library information before detection and caching takes place.
*
* The library definitions are passed by reference. A common use-case is adding
* a module's integration files to the library array, so that the files are
* loaded whenever the library is. As noted above, it is important to declare
* integration files inside of an array, whose key is the module name.
*
* @see hook_libraries_info()
*/
function hook_libraries_info_alter(&$libraries) {
$files = array(
'php' => array('example_module.php_spellchecker.inc'),
);
$libraries['php_spellchecker']['integration files']['example_module'] = $files;
}
......@@ -210,18 +210,22 @@ function libraries_detect_library(&$library) {
}
if (!file_exists($library['library path'])) {
$library['error'] = t('%library could not be found.', array('%library' => $library['title']));
continue;
return;
}
// Detect library version.
// Special handling for named arguments (single array).
if (!isset($library['version arguments'][0])) {
$library['version arguments'] = array($library['version arguments']);
// We support both a single parameter, which is an associative array, and an
// indexed array of multiple parameters.
if (isset($library['version arguments'][0])) {
// Add the library as the first argument.
$library['version'] = call_user_func_array($library['version callback'], array_merge(array($library), $library['version arguments']));
}
else {
$library['version'] = $library['version callback']($library, $library['version arguments']);
}
$library['version'] = call_user_func_array($library['version callback'], $library['version arguments']);
if (empty($library['version'])) {
$library['error'] = t('The version of %library could not be detected.', array('%library' => $library['title']));
continue;
return;
}
// Determine to which supported version the installed version maps.
......@@ -229,13 +233,16 @@ function libraries_detect_library(&$library) {
ksort($library['versions']);
$version = 0;
foreach ($library['versions'] as $supported_version => $version_properties) {
if (version_compare($library['installed version'], $supported_version, '>=')) {
if (version_compare($library['version'], $supported_version, '>=')) {
$version = $supported_version;
}
}
if (!$version) {
$library['error'] = t('The installed version %version of %library is not supported.', array('%version' => $library['installed version'], '%library' => $library['title']));
continue;
$library['error'] = t('The installed version %version of %library is not supported.', array(
'%version' => $library['version'],
'%library' => $library['title'],
));
return;
}
// Apply version specific definitions and overrides.
......@@ -243,10 +250,161 @@ function libraries_detect_library(&$library) {
unset($library['versions']);
}
// Check each variant if it is installed.
if (!empty($library['variants'])) {
foreach ($library['variants'] as $name => &$variant) {
// If no variant callback has been set, assume the variant to be
// installed.
$variant['installed'] = TRUE;
if (!empty($variant['variant callback'])) {
// We support both a single parameter, which is an associative array,
// and an indexed array of multiple parameters.
if (isset($variant['variant arguments'][0])) {
// Add the library as the first argument, and the variant name as the second.
$variant['installed'] = call_user_func_array($variant['variant callback'], array_merge(array($library, $name), $variant['variant arguments']));
}
else {
$variant['installed'] = $variant['variant callback']($library, $name, $variant['variant arguments']);
}
if (empty($variant['installed'])) {
$variant['error'] = t('The %variant variant of %library could not be found.', array(
'%variant' => $name,
'%library' => $library['title'],
));
}
}
}
}
// If we end up here, the library should be usable.
$library['installed'] = TRUE;
if (!empty($library['path'])) {
$library['library path'] .= '/' . $library['path'];
}
/**
* Loads a library.
*
* @param $library
* The name of the library to load.
* @param $variant
* The name of the variant to load.
*/
function libraries_load($library, $variant = NULL) {
$library = libraries_info($library);
libraries_detect_library($library);
libraries_load_files($library, $variant);
}
/**
* Loads a library's files.
*
* @param $library
* The name of the library to load.
* @param $variant
* The name of the variant to load.
*/
function libraries_load_files($library, $variant = NULL) {
// Construct the full path to the library for later use.
$path = !empty($library['path']) ? $library['library path'] . '/' . $library['path'] : $library['library path'];
// If a variant was specified, override the top-level properties with the
// variant properties.
if (!empty($variant) && !empty($library['variants'][$variant]['installed'])) {
$library = array_merge($library, $library['variants'][$variant]);
}
// Load integration files.
if (!empty($library['integration files'])) {
foreach ($library['integration files'] as $module => $files) {
libraries_load_files(array(
'files' => $files,
'library path' => drupal_get_path('module', $module),
));
}
}
// Load both the JavaScript and the CSS files.
// The parameters for drupal_add_js() and drupal_add_css() require special
// handling.
// @see drupal_process_attached()
foreach (array('js', 'css') as $type) {
if (!empty($library['files'][$type])) {
foreach ($library['files'][$type] as $data => $options) {
// If the value is not an array, it's a filename and passed as first
// (and only) argument.
if (!is_array($options)) {
// Prepend the library path to the file name.
$data = "$path/$options";
$options = NULL;
}
// In some cases, the first parameter ($data) is an array. Arrays can't be
// passed as keys in PHP, so we have to get $data from the value array.
if (is_numeric($data)) {
$data = $options['data'];
unset($options['data']);
}
// Apply the default weight if the weight isn't explicitly given.
if (!isset($options['weight'])) {
$options['weight'] = ($type == 'js') ? JS_DEFAULT : CSS_DEFAULT;
}
call_user_func('drupal_add_' . $type, $data, $options);
}
}
}
// Load PHP files.
if (!empty($library['files']['php'])) {
foreach ($library['files']['php'] as $file) {
$file_path = DRUPAL_ROOT . '/' . $path . '/' . $file;
if (file_exists($file_path)) {
require_once $file_path;
}
}
}
}
/**
* Gets the version information from an arbitrary library.
*
* @param $library
* An associative array containing all information about the library.
* @param $options
* An associative array containing with the following keys:
* - file: The filename to parse for the version, relative to the library
* path. For example: 'docs/changelog.txt'.
* - pattern: A string containing a regular expression (PCRE) to match the
* library version. For example: '/@version (\d+)\.(\d+)/'.
* - lines: (optional) The maximum number of lines to search the pattern in.
* Defaults to 20.
* - cols: (optional) The maximum number of characters per line to take into
* account. Defaults to 200. In case of minified or compressed files, this
* prevents reading the entire file into memory.
*
* @return
* A string containing the version of the library.
*
* @see libraries_get_path()
*/
function libraries_get_version($library, $options) {
// Provide defaults.
$options += array(
'file' => '',
'pattern' => '',
'lines' => 20,
'cols' => 200,
);
$file = DRUPAL_ROOT . '/' . $library['library path'] . '/' . $options['file'];
if (empty($options['file']) || !file_exists($file)) {
return;
}
$file = fopen($file, 'r');
while ($options['lines'] && $line = fgets($file, $options['cols'])) {
if (preg_match($options['pattern'], $line, $version)) {
fclose($file);
return $version[1];
}
$options['lines']--;
}
fclose($file);
}
......@@ -4,3 +4,4 @@ Example library
Version 2
This file is an example file to test version detection.
/* $Id$ */
/**
* @file
* Test CSS file for Libraries loading.
*
* Because we cannot test CSS programatically with SimpleTest, the CSS below can
* be useful for debugging with SimpleTest's verbose mode. Note that since the
* DOM cannot be manipulated via CSS, JavaScript loading needs to be functional
* for this to have any visible effect.
*/
div#libraries-test {
color: red;
}
// $Id$
/**
* @file
* Test JavaScript file for Libraries loading.
*
* Because we cannot test JavaScript programatically with SimpleTest, the
* JavaScript below can be useful for debugging with SimpleTest's verbose mode.
*/
(function ($) {
Drupal.behaviors.librariesTest = {
attach: function(context, settings) {
$('h1#page-title').after('<div id="libraries-test">If this text shows up, the JavaScript file was loaded successfully. If this text is red, the CSS file was loaded successfully.</div>')
}
};
})(jQuery);
<?php
// $Id$
/**
* @file
* Test PHP file for Libraries loading.
*/
/**
* Dummy function to see if this file was loaded.
*/
function _libraries_example_installed_variant() {
function _libraries_example_installed_1() {
}
/* $Id$ */
/**
* @file
* Test CSS file for Libraries loading.
*
* Because we cannot test CSS programatically with SimpleTest, the CSS below can
* be useful for debugging with SimpleTest's verbose mode. Note that since the
* DOM cannot be manipulated via CSS, JavaScript loading needs to be functional
* for this to have any visible effect.
*/
div#libraries-test {
color: green;
}
// $Id$
/**
* @file
* Test JavaScript file for Libraries loading.
*
* Because we cannot test JavaScript programatically with SimpleTest, the
* JavaScript below can be useful for debugging with SimpleTest's verbose mode.
*/
(function ($) {
Drupal.behaviors.librariesTest = {
attach: function(context, settings) {
$('h1#page-title').after('<div id="libraries-test">If this text shows up, the JavaScript file was loaded successfully. If this text is green, the CSS file was loaded successfully.</div>')
}
};
})(jQuery);
<?php
// $Id$
/**
* @file
* Test PHP file for Libraries loading.
*/
/**
* Dummy function to see if this file was loaded.
*/
......
/* $Id$ */
/**
* @file
* Test CSS file for Libraries loading.
*
* Because we cannot test CSS programatically with SimpleTest, the CSS below can
* be useful for debugging with SimpleTest's verbose mode. Note that since the
* DOM cannot be manipulated via CSS, JavaScript loading needs to be functional
* for this to have any visible effect.
*/
div#libraries-test {
color: orange;
}
// $Id$
/**
* @file
* Test JavaScript file for Libraries loading.
*
* Because we cannot test JavaScript programatically with SimpleTest, the
* JavaScript below can be useful for debugging with SimpleTest's verbose mode.
*/
(function ($) {
Drupal.behaviors.librariesTest = {
attach: function(context, settings) {
$('h1#page-title').after('<div id="libraries-test">If this text shows up, the JavaScript file was loaded successfully. If this text is orange, the CSS file was loaded successfully.</div>')
}
};
})(jQuery);
<?php
// $Id$
/**
* @file
* Test PHP file for Libraries loading.
*/
/**
* Dummy function to see if this file was loaded.
*/
function _libraries_example_installed_variant_1() {
}
/* $Id$ */
/**
* @file
* Test CSS file for Libraries loading.
*
* Because we cannot test CSS programatically with SimpleTest, the CSS below can
* be useful for debugging with SimpleTest's verbose mode. Note that since the
* DOM cannot be manipulated via CSS, JavaScript loading needs to be functional
* for this to have any visible effect.
*/
div#libraries-test {
color: blue;
}
// $Id$
/**
* @file
* Test JavaScript file for Libraries loading.
*
* Because we cannot test JavaScript programatically with SimpleTest, the
* JavaScript below can be useful for debugging with SimpleTest's verbose mode.
*/
(function ($) {
Drupal.behaviors.librariesTest = {
attach: function(context, settings) {
$('h1#page-title').after('<div id="libraries-test">If this text shows up, the JavaScript file was loaded successfully. If this text is blue, the CSS file was loaded successfully.</div>')
}
};
})(jQuery);
<?php
// $Id$
/**
* @file
* Test PHP file for Libraries loading.
*/
/**
* Dummy function to see if this file was loaded.
*/
function _libraries_example_installed_variant_2() {
}
......@@ -51,41 +51,103 @@ class LibrariesTestCase extends DrupalWebTestCase {
));
$this->assertEqual($library['error'], $error, 'Unsupported library version found.');
// Test supported library version.
$library = libraries_info('example_supported_version');
libraries_detect_library($library);
$this->assertEqual($library['installed'], TRUE, 'Supported library version found.');
// Test libraries_get_version().
$library = libraries_info('example_installed');
$library = libraries_info('example_default_version_callback');
libraries_detect_library($library);
$version = '2';
$this->assertEqual($library['version'], $version, 'Expected version returned by default version callback.');
// Test a multiple-parameter version callback.
$library = libraries_info('example_multiple_parameter_version_callback');
libraries_detect_library($library);
$version = '2';
$this->assertEqual($library['version'], $version, 'Expected version returned by multiple parameter version callback.');
// Test a top-level files property.
$library = libraries_info('example_simple');
libraries_detect_library($library);
$files = array(
'js' => array('example_installed_1.js'),
'css' => array('example_installed_1.css'),
'php' => array('example_installed_1.php'),
);
$this->assertEqual($library['files'], $files, 'Top-level files property works.');
// Test version-specific library files.
$library = libraries_info('example_versions');
libraries_detect_library($library);
$files = array(
'js' => array('example_installed_2.js'),
'css' => array('example_installed_2.css'),
'php' => array('example_installed_2.php'),
);
$this->assertEqual($library['files'], $files, 'Version-specific library files found.');
// @todo Ensure that default files are not contained.
// Test library loading.
$this->drupalGet('libraries_test');
// Test missing variant.
$library = libraries_info('example_variant_missing');
libraries_detect_library($library);
$variants = array_keys($library['variants']);
$error = t('The %variant variant of %library could not be found.', array(
'%variant' => $variants[0],
'%library' => $library['title'],
));
$this->assertEqual($library['variants']['example_variant_1']['error'], $error, 'Missing variant not found.');
// Test existing variant.
$library = libraries_info('example_variant');
libraries_detect_library($library);
$this->assertEqual($library['variants']['example_variant_1']['installed'], TRUE, 'Existing variant found.');
// Test loading of a simple library with a top-level files property.
$this->drupalGet('libraries_test/simple');
$this->assertRaw('example_installed_1.js', 'A JavaScript file is loaded correctly.');
$this->assertRaw('example_installed_1.css', 'A CSS file is loaded correctly.');
$this->assertText('example_installed_1.php', 'A PHP file is loaded correctly.');
$this->assertRaw('example_installed_2.js', 'The JavaScript file is loaded.');
$this->assertRaw('example_installed_2.css', 'The CSS file is loaded.');
$this->assertText('example_installed_2.php', 'The PHP file is loaded.');
// Test loading of integration files.
$this->drupalGet('libraries_test/integration_files');
$this->assertRaw('libraries_test.js', 'The JavaScript integration file is loaded.');
$this->assertRaw('libraries_test.css', 'The CSS integration file is loaded.');
$this->assertText('libraries_test.inc', 'The PHP integration file is loaded.');
// Test version overloading.
$this->drupalGet('libraries_test/versions');
$this->assertNoRaw('example_installed_1.js', 'The JavaScript file of the wrong library version is not loaded.');
$this->assertNoRaw('example_installed_1.css', 'The CSS file of the wrong library version is not loaded.');
$this->assertNoText('example_installed_1.php', 'The PHP file of the wrong library version is not loaded.');
$this->assertRaw('example_installed_2.js', 'The JavaScript file of the correct library version is loaded.');
$this->assertRaw('example_installed_2.css', 'The CSS file of the correct library version is loaded.');
$this->assertText('example_installed_2.php', 'The PHP file of the correct library version is loaded.');