You are here

sassy.module in Sassy 7

Same filename and directory in other branches
  1. 7.3 sassy.module
  2. 7.2 sassy.module

Handles compiling of .sass / .scss files.

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

File

sassy.module
View source
<?php

/**
 * @file
 * Handles compiling of .sass / .scss files.
 *
 * The theme system allows for nearly all output of the Drupal system to be
 * customized by user themes.
 */

/**
 * Implementation of hook_flush_caches().
 */
function sassy_flush_caches() {
  sassy_clear_cache();
}

/**
 * Implementation of hook_element_info_alter().
 */
function sassy_element_info_alter(&$type) {
  array_unshift($type['styles']['#pre_render'], 'sassy_pre_render');
}

/**
 * Implementation of hook_form_FORM_ID_alter().
 */
function sassy_form_system_performance_settings_alter(&$form, &$form_states) {
  $form['sassy'] = array(
    '#type' => 'fieldset',
    '#title' => t('Development settings for the SASSY module'),
  );
  $form['sassy']['sassy_devel'] = array(
    '#type' => 'checkbox',
    '#title' => t('Recompile all SASS / SCSS files on every page request.'),
    '#description' => t('Disables the caching of SASS / SCSS files. Useful for when regularly changing the stylesheets (during theme development).'),
    '#default_value' => variable_get('sassy_devel', FALSE),
  );
}

/**
 * Builds the SASS cache. Should only be invoked by drupal_render().
 *
 * @param $elements
 *   A render array containing:
 *   '#items': The CSS items as returned by drupal_add_css() and altered by
 *   drupal_get_css().
 *   '#group_callback': A function to call to group #items to enable the use of
 *   fewer tags by aggregating files and/or using multiple @import statements
 *   within a single tag.
 *   '#aggregate_callback': A function to call to aggregate the items within the
 *   groups arranged by the #group_callback function.
 *
 * @return $elements
 *   The modified (pre-rendered) $elements parameter.
 */
function sassy_pre_render($elements) {
  $devel = variable_get('sassy_devel', FALSE);
  $map = $original = variable_get('sassy_cache', array());
  $files = sassy_pick_files($elements['#items']);

  // We can bail out here if there are no SCSS files anyways.
  if (empty($files['#stylesheets']) || !module_load_include('php', 'sassy', 'phamlp/sass/SassParser')) {

    // Remove the files from the array of stylesheets.
    $elements['#items'] = array_diff_key($elements['#items'], $files['#stylesheets']);
    return $elements;
  }
  foreach ($files['#stylesheets'] as $key => $file) {

    // Create a unique identifier for the file.
    $hash = hash('sha256', serialize($file));
    $path = isset($map[$hash]) ? $map[$hash] : NULL;

    // We recompile this file if recompile equals TRUE, array (and thereby the
    // hash value) changed, if the file doesn't exist, or if we are in development
    // mode. NOTE: You can use the 'recompile' array for your CSS files to cache
    // them based on advanced criteria.
    if ($devel || $file['recompile'] === TRUE || !isset($map[$hash]) || !file_exists($path)) {
      if (!isset($includes[$file['syntax']])) {
        $includes[$file['syntax']] = !empty($files['#includes'][$file['syntax']]) ? implode("\n\n", array_map('sassy_load_stylesheet', $files['#includes'][$file['syntax']])) : '';
      }
      $data = sassy_load_stylesheet($file['data']);
      $output = sassy_parse($file, $includes[$file['syntax']] . "\n\n" . $data, $file['syntax']);
      $directory = 'public://sassy';
      $file['data'] = $directory . '/' . drupal_hash_base64($output) . '.css';

      // Create the CSS file.
      file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
      if (!file_exists($file['data']) && !file_unmanaged_save_data($output, $file['data'], FILE_EXISTS_REPLACE)) {
        unset($elements['#items'][$key]);
        continue;
      }
    }

    // Update the item in the stylesheets array.
    $elements['#items'][$key] = $file;
    if ($file['recompile'] !== TRUE) {

      // Add this file to the cache if it is not set to recompile on every page load.
      $map[$hash] = $file['data'];
    }
  }

  // If $map and $original don't match anymore that means we need to update the
  // CSS cache.
  if ($original !== $map) {

    // Sort CSS items, so that they appear in the correct order.
    variable_set('sassy_cache', $map);
  }
  return $elements;
}

/**
 * Deletes old cached SCSS files.
 */
function sassy_clear_cache() {
  variable_del('sassy_cache');
  file_scan_directory('public://sassy', '/.*/', array(
    'callback' => 'drupal_delete_file_if_stale',
  ));
}

/**
 * Picks all SCSS and SASS files from an array of stylesheets.
 *
 * @param $items
 *   An array of stylesheets.
 *
 * @return
 *   The extracted files as an array.
 */
function sassy_pick_files(&$items) {
  $files = array(
    '#stylesheets' => array(),
    '#includes' => array(),
  );
  foreach ($items as $key => $file) {
    $extension = drupal_substr($file['data'], -5);
    if ($file['type'] == 'file' && in_array($extension, array(
      '.scss',
      '.sass',
    ))) {
      $syntax = drupal_substr($extension, -4);
      if ($file['media'] == 'include') {
        $files['#includes'][$syntax][$key] = $file;
        unset($items[$key]);
      }
      else {
        $file['syntax'] = $syntax;
        $file['recompile'] = isset($file['recompile']) ? $file['recompile'] : FALSE;

        // If the file is set to recompile on every page load then we don't want
        // it to be aggregated.
        $file['preprocess'] = !empty($file['recompile']) ? FALSE : $file['preprocess'];
        $files['#stylesheets'][$key] = $file;
      }
    }
  }
  return $files;
}

/**
 * Loads a stylesheet and writes the base path to all url declarations.
 *
 * @param $file
 *   A filepath or an array representing a stylesheet.
 *
 * @return
 *   A string that represents the processed contents of the stylesheet.
 */
function sassy_load_stylesheet($file) {
  $file = is_array($file) ? $file['data'] : $file;
  $data = drupal_load_stylesheet($file);

  // Build the base URL of this CSS file. Start with the full URL.
  $base = file_create_url($file);

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

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

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

/**
 * Parse a SCSS string and transform it into CSS.
 *
 * @params $data
 *   A SCSS string.
 *
 * @return
 *   The transformed CSS as a string.
 */
function sassy_parse($file, $data, $syntax) {
  drupal_alter('sassy_pre', $data, $file, $syntax);
  $placeholders = _sassy_match_media_queries($data, $syntax);
  $variables = _sassy_match_variables($data, $syntax);
  $data = str_replace($placeholders, array_keys($placeholders), $data);

  // Quote all URLs here so PhamlP doesn't remove them.
  $data = preg_replace("/url\\(([^'\")]+)\\)/i", "url('\$1')", $data);

  // Execute the compiler.
  $parser = new SassParser(array(
    'style' => 'nested',
    'cache' => FALSE,
    'syntax' => $syntax,
    'extensions' => array(
      'compass' => array(),
    ),
  ));
  $output = $parser
    ->toCss($data, FALSE);
  $output = str_replace(array_keys($placeholders), $placeholders, $output);
  $output = str_replace(array_keys($variables), $variables, $output);
  drupal_alter('sassy_post', $data, $syntax);
  return $output;
}

/**
 * Extract SCSS variables from a SCSS string.
 *
 * @param $data
 *   A SCSS string.
 *
 * @return
 *   An array of variable values, indexed by the variable name.
 */
function _sassy_match_variables($data, $syntax) {
  $variables = array();
  preg_match_all('/(^|\\n)\\$([^\\s]+): (.+);/', $data, $matches);
  foreach ($matches[2] as $key => $value) {
    $variables['$' . $value] = $matches[3][$key];
  }
  return $variables;
}

/**
 * Extracts all media queries from an SCSS string and replace them with named
 * placeholders.
 *
 * @param $data
 *   A SCSS string.
 *
 * @return
 *   An array of placeholders values, indexed by the placeholder token.
 */
function _sassy_match_media_queries($data, $syntax) {
  $placeholders = array();
  preg_match_all('/@media\\s*(.+)\\s*\\{/', $data, $matches);
  foreach ($matches[1] as $key => $value) {
    $placeholders['sassy_media_query_' . $key] = $value;
  }
  return $placeholders;
}

Functions

Namesort descending Description
sassy_clear_cache Deletes old cached SCSS files.
sassy_element_info_alter Implementation of hook_element_info_alter().
sassy_flush_caches Implementation of hook_flush_caches().
sassy_form_system_performance_settings_alter Implementation of hook_form_FORM_ID_alter().
sassy_load_stylesheet Loads a stylesheet and writes the base path to all url declarations.
sassy_parse Parse a SCSS string and transform it into CSS.
sassy_pick_files Picks all SCSS and SASS files from an array of stylesheets.
sassy_pre_render Builds the SASS cache. Should only be invoked by drupal_render().
_sassy_match_media_queries Extracts all media queries from an SCSS string and replace them with named placeholders.
_sassy_match_variables Extract SCSS variables from a SCSS string.