You are here

less.module in Less CSS Preprocessor 7.4

Handles compiling of .less files.

The theme system allows for nearly all output of the Drupal system to be customized by user themes.

File

less.module
View source
<?php

/**
 * @file
 * Handles compiling of .less files.
 *
 * The theme system allows for nearly all output of the Drupal system to be
 * customized by user themes.
 */
define('LESS_PERMISSION', 'administer less');
define('LESS_AUTOPREFIXER', 'less_autoprefixer');
define('LESS_DEVEL', 'less_devel');
define('LESS_WATCH', 'less_watch');
define('LESS_SOURCE_MAPS', 'less_source_maps');
define('LESS_DIRECTORY', 'public://less');
require_once dirname(__FILE__) . '/includes/less.libraries.inc';
require_once dirname(__FILE__) . '/includes/less.wysiwyg.inc';
require_once dirname(__FILE__) . '/includes/less.theme.inc';

/**
 * Implements hook_hook_info().
 */
function less_hook_info() {
  $less_hooks = array(
    'engines',
    'variables',
    'paths',
    'functions',
  );
  $hooks = array();

  /**
   * We don't have to worry about less_HOOK_SYSTEM_NAME_alter variations here
   * as less_HOOK_alter is run immediately before and should include the
   * MODULE.less.inc file containing any
   * less_HOOK_SYSTEM_NAME_alter() implementations.
   */
  foreach ($less_hooks as $hook) {
    $hooks[] = 'less_' . $hook;
    $hooks[] = 'less_' . $hook . '_alter';
  }
  return array_fill_keys($hooks, array(
    'group' => '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 <link />.
 * 
 * @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 <link />, 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 <a href="@url">turn off</a> 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.');
  }
}

Functions

Namesort descending Description
less_admin_menu_cache_info Implements hook_admin_menu_cache_info().
less_clear_css_cache Deletes all stale compiled LESS files that are no longer in use.
less_cron_queue_info Implements hook_cron_queue_info().
less_element_info_alter Implements hook_element_info_alter().
less_flush_caches Implements hook_flush_caches().
less_get_engine
less_get_settings Returns the compiled list of variables and functions for a module/theme.
less_hook_info Implements hook_hook_info().
less_less_engines Implements hook_less_engines().
less_menu Implements hook_menu().
less_permission Implements hook_permission().
_less_attach_src Add original .less file path as 'src' attribute to <link />.
_less_children Return keys from array that match '.less' file extension.
_less_ensure_directory Helper function that attempts to create a folder if it doesn't exist.
_less_file_owner Returns .less file "owner".
_less_get_dir Get/(re)generate current 'less_dir' variable.
_less_get_engines
_less_inc Loads the selected LESS engine, or 'lessphp' for legacy reasons.
_less_is_less_filename Check if filename has '.less' extension.
_less_pre_render Pre-render function for 'style' elements.
_less_registry Keeps track of .less file "ownership".
_less_token_replace Handler for LESS function token().

Constants

Namesort descending Description
LESS_AUTOPREFIXER
LESS_DEVEL
LESS_DIRECTORY
LESS_PERMISSION @file Handles compiling of .less files.
LESS_SOURCE_MAPS
LESS_WATCH