'less', )); } /** * Implements hook_menu(). */ function less_menu() { $items = array(); $items['admin/config/development/less'] = array( 'title' => 'LESS', 'description' => 'Administer LESS settings', 'page callback' => 'drupal_get_form', 'page arguments' => array('less_settings_form'), 'access arguments' => array(LESS_PERMISSION), 'file' => 'includes/less.admin.inc', 'type' => MENU_NORMAL_ITEM, ); $items['admin/config/development/less/settings'] = array( 'title' => 'LESS Settings', 'type' => MENU_DEFAULT_LOCAL_TASK, ); $items['ajax/less/watch'] = array( 'title' => 'LESS watch callback', 'type' => MENU_CALLBACK, 'page callback' => '_less_watch', 'access callback' => 'variable_get', 'access arguments' => array(LESS_WATCH, FALSE), 'delivery callback' => 'drupal_json_output', 'file' => 'includes/less.watch.inc', ); return $items; } /** * Implements hook_permission(). */ function less_permission() { return array( LESS_PERMISSION => array( 'title' => t('Administer LESS'), 'description' => t('Access the LESS settings page and view debug messages.'), ), ); } /** * Implements hook_element_info_alter(). */ function less_element_info_alter(&$type) { // Prepend to the list of #pre_render functions so it runs first. array_unshift($type['styles']['#pre_render'], '_less_pre_render'); if (variable_get(LESS_DEVEL, FALSE)) { // Must run after drupal_pre_render_styles() to attach any attributes. array_push($type['styles']['#pre_render'], '_less_attach_src'); } } /** * Add original .less file path as 'src' attribute to . * * @param array $styles * CSS style tags after drupal_pre_render_styles() has run. * * @return array * Styles array with 'src' attributes on LESS files. * * @see drupal_pre_render_styles() */ function _less_attach_src($styles) { foreach (element_children($styles) as $key) { // If its a , then most likely its a compiled .less file. if ($styles[$key]['#tag'] == 'link') { // Hashes are generated based on the URL without the query portion. $file_url_parts = drupal_parse_url($styles[$key]['#attributes']['href']); // If we have a match, it means it is a compiled .less file. if ($cache = cache_get('less:watch:' . drupal_hash_base64($file_url_parts['path']))) { // Some inspectors allow 'src' attribute to open from a click. $styles[$key]['#attributes']['src'] = url($cache->data['less']['input_file']); } } } return $styles; } /** * Pre-render function for 'style' elements. * * Key place where .less files are detected and processed. * * @param array $styles * All 'style' elements that are to display on the page. * * @return array * Modified style elements pointing to compiled LESS output. */ function _less_pre_render($styles) { $less_devel = (bool) variable_get(LESS_DEVEL, FALSE); if ($less_devel) { if (variable_get(LESS_WATCH, FALSE)) { drupal_add_js(drupal_get_path('module', 'less') . '/scripts/less.watch.js'); } // Warn users once every hour that less is checking for file modifications. if (user_access(LESS_PERMISSION) && flood_is_allowed('less_devel_warning', 1)) { flood_register_event('less_devel_warning'); $message_vars = array( '@url' => url('admin/config/development/less'), ); drupal_set_message(t('LESS files are being checked for modifications on every request. Remember to turn off this feature on production websites.', $message_vars), 'status'); } } $less_items = array_intersect_key($styles['#items'], array_flip(_less_children($styles['#items']))); if (!empty($less_items)) { require_once dirname(__FILE__) . '/includes/less.process.inc'; // Attach settings to each item. array_walk($less_items, '_less_attach_settings'); // Determine output path for each item. array_walk($less_items, '_less_output_path'); // Check for rebuild each page. if ($less_devel) { array_walk($less_items, '_less_check_build'); } // Compile '.less' files. array_walk($less_items, '_less_process_file'); // Store cache information. if ($less_devel) { array_walk($less_items, '_less_store_cache_info'); } $styles['#items'] = array_replace($styles['#items'], $less_items); } return $styles; } /** * Implements hook_admin_menu_cache_info(). */ function less_admin_menu_cache_info() { $caches = array(); // Add item to admin_menu's flush caches menu. $caches['less'] = array( 'title' => t('LESS compiled files'), 'callback' => 'less_flush_caches', ); return $caches; } /** * Implements hook_cron_queue_info(). * * This hook runs before cache flush during cron. Reliably lets us know if its * cron or not. */ function less_cron_queue_info() { drupal_static('less_cron', TRUE); } /** * Implements hook_flush_caches(). * * Triggers rebuild of all LESS files during cache flush, except during cron. */ function less_flush_caches() { if (!drupal_static('less_cron')) { // Rebuild the less files directory. _less_get_dir(TRUE); cache_clear_all('less:', 'cache', TRUE); } less_clear_css_cache(); return array(); } /** * Deletes all stale compiled LESS files that are no longer in use. * * @see drupal_delete_file_if_stale(). */ function less_clear_css_cache() { file_scan_directory(LESS_DIRECTORY, '/.+/', array('callback' => 'drupal_delete_file_if_stale')); } /** * Get/(re)generate current 'less_dir' variable. * * @param bool $rebuild * Flag to rebuild compiled output. * * @return string * current 'less_dir' Drupal variable value. */ function _less_get_dir($rebuild = FALSE) { $less_dir = variable_get('less_dir'); // If drupal variable 'less_dir' is not set, empty, or manually reset, then // generate a new unique id and save it. if ($rebuild || empty($less_dir)) { // Set the less directory variable. variable_set('less_dir', drupal_hash_base64(uniqid('', TRUE))); } return variable_get('less_dir'); } /** * Loads the selected LESS engine, or 'lessphp' for legacy reasons. * * @return bool * TRUE if selected LESS engine is loaded. */ function _less_inc() { static $loaded = NULL; if (!isset($loaded)) { $less_engine = variable_get('less_engine', 'lessphp'); if (($less_engine_library = libraries_load($less_engine)) && $less_engine_library['installed']) { $loaded = $less_engine; } } return $loaded; } /** * Keeps track of .less file "ownership". * * This keeps track of which modules and themes own which .less files, and any * variable defaults those system items define. * * Only tracks .less files that are added through .info files. */ function _less_registry() { $static_stylesheets = &drupal_static('less_stylesheets'); $static_defaults = &drupal_static('less_defaults'); if (!isset($static_stylesheets) || !isset($static_defaults)) { if (($cache_stylesheets = cache_get('less:stylesheets')) && ($cache_defaults = cache_get('less:defaults'))) { $static_stylesheets = $cache_stylesheets->data; $static_defaults = $cache_defaults->data; } else { $system_types = array( 'module_enabled', 'theme', ); foreach ($system_types as $system_type) { $system_items = system_list($system_type); foreach ($system_items as $system_item_name => $system_item) { // Register all globally included .less stylesheets. if (!empty($system_item->info['stylesheets'])) { foreach ($system_item->info['stylesheets'] as $stylesheets) { foreach ($stylesheets as $stylesheet) { if (_less_is_less_filename($stylesheet)) { $static_stylesheets[$stylesheet] = $system_item_name; } } } } // Process LESS settings from .info files. if (isset($system_item->info['less']) && is_array($system_item->info['less'])) { // Register all non-global stylesheets. if (isset($system_item->info['less']['sheets']) && is_array($system_item->info['less']['sheets'])) { $system_item_path = drupal_get_path($system_item->type, $system_item->name); foreach ($system_item->info['less']['sheets'] as $stylesheet) { $static_stylesheets[$system_item_path . '/' . $stylesheet] = $system_item_name; } } // Register variable defaults. if (isset($system_item->info['less']['vars']) && is_array($system_item->info['less']['vars'])) { $static_defaults[$system_item_name] = $system_item->info['less']['vars']; } } // Invoke hook_less_variables(), results should be static. if (module_exists($system_item_name) && ($module_defaults = module_invoke($system_item_name, 'less_variables'))) { $static_defaults[$system_item_name] = array_replace((array) $static_defaults[$system_item_name], array_filter($module_defaults)); } } } cache_set('less:stylesheets', $static_stylesheets); cache_set('less:defaults', $static_defaults); } } } /** * Returns .less file "owner". * * Returns the owning module/theme for a passed in .less file, or NULL. * Only can resolve .less files that are added using .info files. * * @param string $filepath * System path to .less file, relative to DRUPAL_ROOT. * * @return string|NULL * System name of .less file "owner" or NULL in case of no known "owner". */ function _less_file_owner($filepath) { // Use the advanced drupal_static() pattern, since this is called very often. static $drupal_static_fast; if (!isset($drupal_static_fast)) { $drupal_static_fast['cache'] = &drupal_static('less_stylesheets'); if (!isset($drupal_static_fast['cache'])) { _less_registry(); } } $stylesheets_cache = &$drupal_static_fast['cache']; return isset($stylesheets_cache[$filepath]) ? $stylesheets_cache[$filepath] : NULL; } /** * Returns the compiled list of variables and functions for a module/theme. * * @param string $system_name * Module/theme system name. NULL is cast to empty string for array indexes. */ function less_get_settings($system_name = NULL) { // Use the advanced drupal_static() pattern, since this is called very often. static $drupal_static_fast; if (!isset($drupal_static_fast)) { $drupal_static_fast['cache'] = &drupal_static(__FUNCTION__); } $less_settings_static = &$drupal_static_fast['cache']; if (!isset($less_settings_static[$system_name])) { global $theme; $valid_module = !empty($system_name) && module_exists($system_name); $theme_settings = theme_get_setting('less', $theme); $defaults_cache = &drupal_static('less_defaults'); if (!isset($defaults_cache)) { _less_registry(); } // Defaults. $data = array( 'build_cache_id' => _less_get_dir(), 'variables' => array(), 'functions' => array( 'token' => '_less_token_replace', ), 'paths' => array(), LESS_AUTOPREFIXER => (bool) variable_get(LESS_AUTOPREFIXER, FALSE), LESS_DEVEL => (bool) variable_get(LESS_DEVEL, FALSE), LESS_SOURCE_MAPS => (bool) variable_get(LESS_SOURCE_MAPS, FALSE), 'theme' => $theme, ); /* * Compile the LESS variables. */ // Cached default variables from .info files and hook_less_variables(). if (!empty($defaults_cache[$system_name])) { $data['variables'] = array_replace($data['variables'], array_filter($defaults_cache[$system_name])); } // Saved variable values from current theme. if (!is_null($theme_settings) && !empty($theme_settings[$system_name])) { $data['variables'] = array_replace($data['variables'], array_filter($theme_settings[$system_name])); } // Prevent $system_name from being altered. $alter_system_name = $system_name; // Invoke hook_less_variables_alter(). drupal_alter('less_variables', $data['variables'], $alter_system_name); // Invoke hook_less_variables_SYSTEM_NAME_alter(). drupal_alter('less_variables_' . $system_name, $data['variables']); /* * Grab the LESS functions. * * LESS functions are not stored in the cache table since they could be * anonymous functions. */ if ($valid_module && module_hook($system_name, 'less_functions')) { $data['functions'] = array_replace($data['functions'], (array) module_invoke($system_name, 'less_functions')); } // Prevent $system_name from being altered. $alter_system_name = $system_name; // Invoke hook_less_functions_alter(). drupal_alter('less_functions', $data['functions'], $alter_system_name); // Invoke hook_less_functions_SYSTEM_NAME_alter(). drupal_alter('less_functions_' . $system_name, $data['functions']); /* * Grab the LESS include paths. * */ if ($valid_module && module_hook($system_name, 'less_paths')) { $data['paths'] = array_unique(array_merge($data['paths'], (array) module_invoke($system_name, 'less_paths'))); } // Prevent $system_name from being altered. $alter_system_name = $system_name; // Invoke hook_less_paths_alter(). drupal_alter('less_paths', $data['paths'], $alter_system_name); // Invoke hook_less_paths_SYSTEM_NAME_alter(). drupal_alter('less_paths_' . $system_name, $data['paths']); $data['paths'] = array_unique($data['paths']); $less_settings_static[$system_name] = $data; } // Don't need to test isset(), there will always be data at $system_name. return $less_settings_static[$system_name]; } /** * Handler for LESS function token(). * * @param string[] $arg * * @return array */ function _less_token_replace($arg) { list($type, $delimiter, $value) = $arg; return array($type, $delimiter, array(token_replace($value[0]))); } /** * Helper function that attempts to create a folder if it doesn't exist. * * Locks are used to help avoid concurrency collisions. * * @param string $directory_path * Directory of which to create/confirm existence. * * @return bool * Value indicating existence of directory. */ function _less_ensure_directory($directory_path) { $is_dir = is_dir($directory_path); if (!$is_dir) { $lock_id = 'less_directory_' . md5($directory_path); // Attempt to create directory only 3 times, else delay is too long. for ($i = 0; $i < 3; $i++) { if (lock_acquire($lock_id) && $is_dir = file_prepare_directory($directory_path, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) { // Creation was successful, cancel the 'for' loop; break; } lock_wait($lock_id, 1); } lock_release($lock_id); if (!$is_dir) { // There is a problem with the directory. $message_vars = array( '%dir' => $directory_path, ); watchdog('LESS', 'LESS could not create a directory in %dir', $message_vars, WATCHDOG_ERROR); if (user_access(LESS_PERMISSION)) { drupal_set_message(t('LESS could not create a directory in %dir', $message_vars), 'error', FALSE); } } } return $is_dir; } /** * Return keys from array that match '.less' file extension. * * @param array $items * An array where keys are expected to be filepaths. * * @return array * Array of matching filepaths. */ function _less_children($items) { return array_filter(array_keys($items), '_less_is_less_filename'); } /** * Check if filename has '.less' extension. * * @param string $filename * File name/path to search for '.less' extension. * * @return bool * TRUE if $filename does end with '.less'. */ function _less_is_less_filename($filename) { return drupal_substr($filename, -5) === '.less'; } /** * Implements hook_less_engines(). * * @return string[] */ function less_less_engines() { return array( 'less.php' => 'LessEngineLess_php', 'lessphp' => 'LessEngineLessphp', 'less.js' => 'LessEngineLess_js', ); } /** * @return \LessEngineInterface[] */ function _less_get_engines() { $registered_engines = module_invoke_all('less_engines'); drupal_alter('less_engines', $registered_engines); return $registered_engines; } /** * @param $input_file_path * * @return \LessEngine * * @throws Exception */ function less_get_engine($input_file_path) { $engines = _less_get_engines(); $selected_engine = _less_inc(); if (!empty($engines[$selected_engine])) { $class_name = $engines[$selected_engine]; return new $class_name($input_file_path); } else { throw new Exception('Unable to load LessEngine.'); } }