diff --git a/libraries.api.php b/libraries.api.php index a54b7b4..54033d3 100644 --- a/libraries.api.php +++ b/libraries.api.php @@ -16,14 +16,35 @@ * - name: The official, human-readable name of the library. * - vendor url: The URL of the homepage of the library. * - download url: The URL of a web page on which the library can be obtained. - * - path: (optional) A relative path from the directory of the library to the - * actual library. Only required if the extracted download package contains - * the actual library files in a sub-directory. - * - library path: (optional) The absolute path to the library directory. This + * - path: (optional) The absolute path to the library directory. This * should not be declared normally, as it is automatically detected, to * allow for multiple possible library locations. A valid use-case is an * external library, in which case the full URL to the library should be * specified here. + * - path callback: (optional) The name of a function that detects and + * returns the full path to the library. If the library cannot be found, it + * returns FALSE. The first argument is always $library, an array containing + * all library information as described here. There are two ways to declare + * the path callback's additional arguments, either as a single $options + * parameter or as multiple parameters, which correspond to the two ways to + * specify the argument values (see 'path arguments'). Defaults to + * libraries_get_path(). + * - path arguments: (optional) A list of arguments to pass to the path + * callback. Path arguments can be declared either as an associative array + * whose keys are the argument names or as an indexed array without + * specifying keys. If declared as an associative array, the arguments get + * passed to the location callback as a single $options parameter whose keys + * are the argument names (i.e. $options is identical to the specified + * array). If declared as an indexed array, the array values get passed to + * the version callback as seperate arguments in the order they were + * declared. The default location callback libraries_get_path() expects a + * single, associative array with the following named keys: + * - fallback: (optional) A fallback path to use in case the library is not + * found locally. This can be used for libraries that can be downloaded, + * but are also available externally. + * - directory: (optional) A relative path from the directory of the library + * to the actual library. Only required if the extracted download package + * contains the actual library files in a sub-directory. * - version: (optional) The version of the library. This should not be * declared normally, as it is automatically detected (see 'version * callback' below) to allow for version changes of libraries without code @@ -47,7 +68,7 @@ * declared as an indexed array, the array values get passed to the version * callback as seperate arguments in the order they were declared. The * default version callback libraries_get_version() expects a single, - * associative array with named keys: + * associative array with the following 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 @@ -159,7 +180,7 @@ function hook_libraries_info() { 'download url' => 'http://example.com/download', // Optional: If, after extraction, the actual library files are contained in // 'sites/all/libraries/example/lib', specify the relative path here. - 'path' => 'lib', + 'directory' => 'lib', // Optional: Define a custom version detection callback, if required. 'version callback' => 'mymodule_get_version', // Specify arguments for the version callback. By default, @@ -286,7 +307,7 @@ function hook_libraries_info() { 'name' => 'TinyMCE', 'vendor url' => 'http://tinymce.moxiecode.com', 'download url' => 'http://tinymce.moxiecode.com/download.php', - 'path' => 'jscripts/tiny_mce', + 'directory' => 'jscripts/tiny_mce', // The regular expression catches two parts (the major and the minor // version), which libraries_get_version() doesn't allow. 'version callback' => 'tinymce_get_version', @@ -356,6 +377,50 @@ function hook_libraries_info() { ), ), ); + + // A simple external library. + $libraries['external'] = array( + 'name' => 'External library', + // This library does not have a download url! + 'vendor url' => 'http://example.com/external', + // It is only availably externally so we must declare the library path. + 'path' => 'http://example.com/external/source', + // The version cannot be detected programmatically, so it must be declared + // upfront. + 'version' => '1.0.1', + // The library only consists of one file, which is located at + // http://example.com/external/source/external.js. We have already declared + // the library path above, so we only need to specify the filename here. + 'files' => array( + 'js' => array( + 'external.js', + ), + ), + ); + + // A library that is both available locally and externally, i.e. you can + // download it to your server for performance or use the online version for + // convenience. + $libraries['openlayers'] = array( + 'name' => 'OpenLayers', + 'vendor url' => 'http://openlayers.org/', + 'download url' => 'http://trac.osgeo.org/openlayers/wiki/HowToDownload', + // Because we want people to be able to use the library locally, but fall + // back to the external library, we declare the optional 'fallback' option. + // The path we specify is then used as a fallback if the library is not + // found locally. + 'path arguments' => array( + 'fallback' => 'http://openlayers.org/api', + ), + // Regardless of whether it is available locally or externally, the files + // are the same. + 'files' => array( + 'js' => array( + 'OpenLayers.js', + ), + ), + ); + return $libraries; } diff --git a/libraries.module b/libraries.module index 65f2bf1..a81eb5d 100644 --- a/libraries.module +++ b/libraries.module @@ -15,32 +15,31 @@ function libraries_flush_caches() { /** * Gets the path of a library. * - * @param $name - * The machine name of a library to return the path for. - * @param $base_path - * Whether to prefix the resulting path with base_path(). + * @param $library + * An associative array containing all information about the library. + * @param $options + * An associative array containing with the following keys: + * - fallback: An external URL that, if specified, is returned if the library + * is not found locally. * * @return * The path to the specified library or FALSE if the library wasn't found. * * @ingroup libraries */ -function libraries_get_path($name, $base_path = FALSE) { - $libraries = &drupal_static(__FUNCTION__); +function libraries_get_path($library, $options = array()) { + $directories = &drupal_static(__FUNCTION__); - if (!isset($libraries)) { - $libraries = libraries_get_libraries(); + if (!isset($directories)) { + $directories = libraries_get_directories(); } - $path = ($base_path ? base_path() : ''); - if (!isset($libraries[$name])) { - return FALSE; + if (!isset($directories[$library['machine name']])) { + return (isset($options['fallback']) ? $options['fallback'] : FALSE); } else { - $path .= $libraries[$name]; + return $directories[$library['machine name']]; } - - return $path; } /** @@ -59,7 +58,7 @@ function libraries_get_path($name, $base_path = FALSE) { * * @ingroup libraries */ -function libraries_get_libraries() { +function libraries_get_directories() { $directory = 'libraries'; $searchdir = array(); $profile = drupal_get_profile(); @@ -334,8 +333,9 @@ function libraries_info_defaults(&$library, $name) { 'name' => $name, 'vendor url' => '', 'download url' => '', - 'path' => '', - 'library path' => NULL, + 'path callback' => 'libraries_get_path', + 'path arguments' => array(), + 'directory' => '', 'version callback' => 'libraries_get_version', 'version arguments' => array(), 'files' => array(), @@ -394,10 +394,18 @@ function libraries_detect($name) { $library['installed'] = FALSE; // Check whether the library exists. - if (!isset($library['library path'])) { - $library['library path'] = libraries_get_path($library['machine name']); + if (!isset($library['path'])) { + // We support both a single parameter, which is an associative array, and an + // indexed array of multiple parameters. + if (isset($library['path arguments'][0])) { + // Add the library name as the first argument. + $library['path'] = call_user_func_array($library['path callback'], array_merge(array($library), $library['path arguments'])); + } + else { + $library['path'] = $library['path callback']($library, $library['path arguments']); + } } - if ($library['library path'] === FALSE || !file_exists($library['library path'])) { + if ($library['path'] == FALSE) { $library['error'] = 'not found'; $library['error message'] = t('The %library library could not be found.', array( '%library' => $library['name'], @@ -564,15 +572,15 @@ function libraries_load_files($library) { foreach ($library['integration files'] as $module => $files) { libraries_load_files(array( 'files' => $files, - 'path' => '', - 'library path' => drupal_get_path('module', $module), + 'path' => drupal_get_path('module', $module), + 'directory' => '', )); } } // Construct the full path to the library for later use. - $path = $library['library path']; - $path = ($library['path'] !== '' ? $path . '/' . $library['path'] : $path); + $path = $library['path']; + $path = ($library['directory'] !== '' ? $path . '/' . $library['directory'] : $path); // Count the number of loaded files for the return value. $count = 0; @@ -654,7 +662,7 @@ function libraries_get_version($library, $options) { 'cols' => 200, ); - $file = DRUPAL_ROOT . '/' . $library['library path'] . '/' . $options['file']; + $file = DRUPAL_ROOT . '/' . $library['path'] . '/' . $options['file']; if (empty($options['file']) || !file_exists($file)) { return; } diff --git a/tests/libraries.test b/tests/libraries.test index 2a2e2cf..336f971 100644 --- a/tests/libraries.test +++ b/tests/libraries.test @@ -53,7 +53,7 @@ class LibrariesTestCase extends DrupalWebTestCase { // Test that library information is found correctly. $expected = array( 'name' => 'Example files', - 'library path' => drupal_get_path('module', 'libraries') . '/tests/example', + 'path' => drupal_get_path('module', 'libraries') . '/tests/example', 'version' => '1', 'files' => array( 'js' => array('example_1.js' => array()), @@ -160,7 +160,7 @@ class LibrariesTestCase extends DrupalWebTestCase { // Test the applying of callbacks. $expected = array( 'name' => 'Example callback', - 'library path' => drupal_get_path('module', 'libraries') . '/tests/example', + 'path' => drupal_get_path('module', 'libraries') . '/tests/example', 'version' => '1', 'versions' => array( '1' => array( @@ -274,6 +274,25 @@ class LibrariesTestCase extends DrupalWebTestCase { // Test version overloading and variant loading. $this->drupalGet('libraries_test/versions_and_variants'); $this->assertLibraryFiles('example_4', 'Concurrent version and variant overloading'); + + // Test caching. + variable_set('libraries_test_cache', TRUE); + cache_clear_all('example_callback', 'cache_libraries'); + // When the library information is not cached, all callback groups should be + // invoked. + $this->drupalGet('libraries_test/cache'); + $this->assertRaw('The info callback group was invoked.', 'Info callback invoked for uncached libraries.'); + $this->assertRaw('The pre-detect callback group was invoked.', 'Pre-detect callback invoked for uncached libraries.'); + $this->assertRaw('The post-detect callback group was invoked.', 'Post-detect callback invoked for uncached libraries.'); + $this->assertRaw('The load callback group was invoked.', 'Load callback invoked for uncached libraries.'); + // When the library information is cached only the load callback group should + // be invoked. + $this->drupalGet('libraries_test/cache'); + $this->assertNoRaw('The info callback group was not invoked.', 'Info callback not invoked for cached libraries.'); + $this->assertNoRaw('The pre-detect callback group was not invoked.', 'Pre-detect callback not invoked for cached libraries.'); + $this->assertNoRaw('The post-detect callback group was not invoked.', 'Post-detect callback not invoked for cached libraries.'); + $this->assertRaw('The load callback group was invoked.', 'Load callback invoked for cached libraries.'); + variable_set('libraries_test_cache', FALSE); } /** diff --git a/tests/libraries_test.module b/tests/libraries_test.module index 4893b11..d819d15 100644 --- a/tests/libraries_test.module +++ b/tests/libraries_test.module @@ -12,17 +12,18 @@ function libraries_test_libraries_info() { // Test library detection. $libraries['example_missing'] = array( 'name' => 'Example missing', - 'library path' => drupal_get_path('module', 'libraries') . '/tests/missing', + // Not specifying a path invokes libraries_get_path(), which will return + // FALSE. ); $libraries['example_undetected_version'] = array( 'name' => 'Example undetected version', - 'library path' => drupal_get_path('module', 'libraries') . '/tests', + 'path' => drupal_get_path('module', 'libraries') . '/tests', 'version callback' => '_libraries_test_return_version', 'version arguments' => array(FALSE), ); $libraries['example_unsupported_version'] = array( 'name' => 'Example unsupported version', - 'library path' => drupal_get_path('module', 'libraries') . '/tests', + 'path' => drupal_get_path('module', 'libraries') . '/tests', 'version callback' => '_libraries_test_return_version', 'version arguments' => array('1'), 'versions' => array( @@ -32,7 +33,7 @@ function libraries_test_libraries_info() { $libraries['example_supported_version'] = array( 'name' => 'Example supported version', - 'library path' => drupal_get_path('module', 'libraries') . '/tests', + 'path' => drupal_get_path('module', 'libraries') . '/tests', 'version callback' => '_libraries_test_return_version', 'version arguments' => array('1'), 'versions' => array( @@ -43,7 +44,7 @@ function libraries_test_libraries_info() { // Test the default version callback. $libraries['example_default_version_callback'] = array( 'name' => 'Example default version callback', - 'library path' => drupal_get_path('module', 'libraries') . '/tests/example', + 'path' => drupal_get_path('module', 'libraries') . '/tests/example', 'version arguments' => array( 'file' => 'README.txt', // Version 1 @@ -55,7 +56,7 @@ function libraries_test_libraries_info() { // Test a multiple-parameter version callback. $libraries['example_multiple_parameter_version_callback'] = array( 'name' => 'Example multiple parameter version callback', - 'library path' => drupal_get_path('module', 'libraries') . '/tests/example', + 'path' => drupal_get_path('module', 'libraries') . '/tests/example', // Version 1 'version callback' => '_libraries_test_get_version', 'version arguments' => array('README.txt', '/Version (\d+)/', 5), @@ -64,7 +65,7 @@ function libraries_test_libraries_info() { // Test a top-level files property. $libraries['example_files'] = array( 'name' => 'Example files', - 'library path' => drupal_get_path('module', 'libraries') . '/tests/example', + 'path' => drupal_get_path('module', 'libraries') . '/tests/example', 'version' => '1', 'files' => array( 'js' => array('example_1.js'), @@ -78,7 +79,7 @@ function libraries_test_libraries_info() { // these files should be automatically loaded when the library is loaded. $libraries['example_integration_files'] = array( 'name' => 'Example integration files', - 'library path' => drupal_get_path('module', 'libraries') . '/tests/example', + 'path' => drupal_get_path('module', 'libraries') . '/tests/example', 'version' => '1', 'integration files' => array( 'libraries_test' => array( @@ -92,7 +93,7 @@ function libraries_test_libraries_info() { // Test version overloading. $libraries['example_versions'] = array( 'name' => 'Example versions', - 'library path' => drupal_get_path('module', 'libraries') . '/tests/example', + 'path' => drupal_get_path('module', 'libraries') . '/tests/example', 'version' => '2', 'versions' => array( '1' => array( @@ -115,7 +116,7 @@ function libraries_test_libraries_info() { // Test variant detection. $libraries['example_variant_missing'] = array( 'name' => 'Example variant missing', - 'library path' => drupal_get_path('module', 'libraries') . '/tests/example', + 'path' => drupal_get_path('module', 'libraries') . '/tests/example', 'version' => '1', 'variants' => array( 'example_variant' => array( @@ -132,7 +133,7 @@ function libraries_test_libraries_info() { $libraries['example_variant'] = array( 'name' => 'Example variant', - 'library path' => drupal_get_path('module', 'libraries') . '/tests/example', + 'path' => drupal_get_path('module', 'libraries') . '/tests/example', 'version' => '1', 'variants' => array( 'example_variant' => array( @@ -150,7 +151,7 @@ function libraries_test_libraries_info() { // Test correct behaviour with multiple versions and multiple variants. $libraries['example_versions_and_variants'] = array( 'name' => 'Example versions and variants', - 'library path' => drupal_get_path('module', 'libraries') . '/tests/example', + 'path' => drupal_get_path('module', 'libraries') . '/tests/example', 'version' => '2', 'versions' => array( '1' => array( @@ -203,7 +204,7 @@ function libraries_test_libraries_info() { // Test the applying of callbacks. $libraries['example_callback'] = array( 'name' => 'Example callback', - 'library path' => drupal_get_path('module', 'libraries') . '/tests/example', + 'path' => drupal_get_path('module', 'libraries') . '/tests/example', 'version' => '1', 'versions' => array( '1' => array( @@ -299,8 +300,7 @@ function _libraries_test_return_version($library, $version) { * @see libraries_get_version() */ function _libraries_test_get_version($library, $file, $pattern, $lines = 20, $cols = 200) { - - $file = DRUPAL_ROOT . '/' . $library['library path'] . '/' . $file; + $file = DRUPAL_ROOT . '/' . $library['path'] . '/' . $file; if (!file_exists($file)) { return; } @@ -406,6 +406,13 @@ function _libraries_test_callback(&$library, $version, $variant, $group) { $string .= ' (top-level)'; } $library["$group callback"] = $string; + + // The following is used to test caching of library information. + // Only set the message for the top-level library to prevent confusing, + // duplicate messages. + if (!isset($version) && !isset($variant) && variable_get('libraries_test_cache', FALSE)) { + drupal_set_message("The $group callback group was invoked."); + } } /** @@ -414,34 +421,34 @@ function _libraries_test_callback(&$library, $version, $variant, $group) { function libraries_test_menu() { $items['libraries_test/files'] = array( 'title' => 'Test files', - 'page callback' => '_libraries_test_load', 'page arguments' => array('example_files'), - 'access callback' => TRUE, ); $items['libraries_test/integration_files'] = array( 'title' => 'Test integration files', - 'page callback' => '_libraries_test_load', 'page arguments' => array('example_integration_files'), - 'access callback' => TRUE, ); $items['libraries_test/versions'] = array( 'title' => 'Test version loading', - 'page callback' => '_libraries_test_load', 'page arguments' => array('example_versions'), - 'access callback' => TRUE, ); $items['libraries_test/variant'] = array( 'title' => 'Test variant loading', - 'page callback' => '_libraries_test_load', 'page arguments' => array('example_variant', 'example_variant'), - 'access callback' => TRUE, ); $items['libraries_test/versions_and_variants'] = array( 'title' => 'Test concurrent version and variant loading', - 'page callback' => '_libraries_test_load', 'page arguments' => array('example_versions_and_variants', 'example_variant_2'), - 'access callback' => TRUE, ); + $items['libraries_test/cache'] = array( + 'title' => 'Test caching of library information', + 'page arguments' => array('example_callback'), + ); + foreach ($items as &$item) { + $item += array( + 'page callback' => '_libraries_test_load', + 'access callback' => TRUE, + ); + } return $items; }