You are here

less.module in Less CSS Preprocessor 8

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