'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.');
}
}