You are here

less.module in Less CSS Preprocessor 7.3

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');
require_once dirname(__FILE__) . '/less.libraries.inc';
require_once dirname(__FILE__) . '/less.cron.inc';
require_once dirname(__FILE__) . '/less.wysiwyg.inc';

/**
 * Implements hook_menu().
 */
function less_menu() {
  $items = array();
  $items['admin/config/development/less'] = array(
    'title' => 'LESS settings',
    'description' => 'Administer LESS settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'less_settings_form',
    ),
    'access arguments' => array(
      LESS_PERMISSION,
    ),
    'file' => 'less.admin.inc',
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/config/development/less/settings'] = array(
    'title' => $items['admin/config/development/less']['title'],
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['ajax/less/watch'] = array(
    'title' => 'LESS watch callback',
    'type' => MENU_CALLBACK,
    'page callback' => '_less_watch',
    'access callback' => TRUE,
    'delivery callback' => 'drupal_json_output',
    'file' => '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 />.
 */
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['data']);
      }
    }
  }
  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 = variable_get('less_devel', FALSE);
  $less_dir = _less_get_dir();
  if ($less_devel) {
    if (variable_get('less_watch', TRUE)) {
      drupal_add_js(drupal_get_path('module', 'less') . '/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_path = 'public://less/' . $less_dir;
  foreach (_less_children($styles['#items']) as $less_filepath) {
    require_once dirname(__FILE__) . '/less.process.inc';
    _less_process_file($styles['#items'][$less_filepath]);
  }
  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_flush_caches().
 *
 * Flushes compiled 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);
  }
  return array();
}

/**
 * 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;
}

/**
 * Copied functionality from drupal_build_css_cache() for our own purposes.
 * 
 * This function processes $contents and rewrites relative paths to be absolute
 * from web root. This is mainly used to ensure that compiled .less files still
 * reference images at their original paths.
 * 
 * @return string
 *   Processed styles with replaced paths.
 * 
 * @see drupal_build_css_cache()
 */
function _less_rewrite_paths($input_filepath, $contents) {
  $output = '';

  // Build the base URL of this CSS file: start with the full URL.
  $css_base_url = file_create_url($input_filepath);

  // Move to the parent.
  $css_base_url = substr($css_base_url, 0, strrpos($css_base_url, '/'));

  // Simplify to a relative URL if the stylesheet URL starts with the
  // base URL of the website.
  if (substr($css_base_url, 0, strlen($GLOBALS['base_root'])) == $GLOBALS['base_root']) {
    $css_base_url = substr($css_base_url, strlen($GLOBALS['base_root']));
  }
  _drupal_build_css_path(NULL, $css_base_url . '/');

  // Anchor all paths in the CSS with its base URL, ignoring external and absolute paths.
  $output .= preg_replace_callback('/url\\(\\s*[\'"]?(?![a-z]+:|\\/+)([^\'")]+)[\'"]?\\s*\\)/i', '_drupal_build_css_path', $contents);
  return $output;
}

/**
 * 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 (drupal_substr($stylesheet, -5) == '.less') {
                  $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_defaults = module_invoke($system_item_name, 'less_variables')) {
            $static_defaults[$system_item_name] = array_merge((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 or theme system name.
 */
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;
    $theme_settings = theme_get_setting('less', $theme);
    $defaults_cache =& drupal_static('less_defaults');
    if (!isset($defaults_cache)) {
      _less_registry();
    }
    $data = array(
      'variables' => array(),
      'functions' => array(
        'token' => '_less_token_replace',
      ),
      'paths' => array(),
    );

    /*
     * Compile the LESS variables.
     */

    // Cached default variables from .info files and hook_less_variables().
    if (!empty($defaults_cache[$system_name])) {
      $data['variables'] = array_merge($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_merge($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 (module_hook($system_name, 'less_functions')) {
      $data['functions'] = array_merge($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 (module_hook($system_name, 'less_paths')) {
      $data['paths'] = 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_functions_alter().
    drupal_alter('less_paths', $data['paths'], $alter_system_name);

    // Invoke hook_less_functions_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().
 */
function _less_token_replace($arg) {
  list($type, $delimeter, $value) = $arg;
  return array(
    $type,
    $delimeter,
    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;
}

/**
 * Implements hook_form_alter().
 * 
 * Includes files that have hook_form_FORM_ID_alter() implementations within.
 */
function less_form_alter(&$form, &$form_state, $form_id) {
  $group = NULL;
  switch ($form_id) {
    case 'system_theme_settings':
      $group = 'theme';
      break;
    case 'wysiwyg_profile_form':
      $group = 'wysiwyg';
      break;
  }
  if (isset($group)) {
    module_load_include('inc', 'less', 'less.' . $group);
  }
}

/**
 * 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';
}

Functions

Namesort descending Description
less_admin_menu_cache_info Implements hook_admin_menu_cache_info().
less_element_info_alter Implements hook_element_info_alter().
less_flush_caches Implements hook_flush_caches().
less_form_alter Implements hook_form_alter().
less_get_settings Returns the compiled list of variables and functions for a module/theme.
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_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_rewrite_paths Copied functionality from drupal_build_css_cache() for our own purposes.
_less_token_replace Handler for LESS function token().

Constants

Namesort descending Description
LESS_PERMISSION @file Handles compiling of .less files.