You are here

magic.module in Magic 7

Same filename and directory in other branches
  1. 7.2 magic.module

Keep Frontend DRY; sprinkle it with MAGIC!

File

magic.module
View source
<?php

/**
 * @file
 * Keep Frontend DRY; sprinkle it with MAGIC!
 */

// We set a specific group for jquery, so our CDN work actually works.
define('JS_JQUERY', -10000);
define('JS_DRUPAL', -9000);
require_once "includes/styles.inc";

/**
 * Implements hook_menu().
 */
function magic_menu() {
  $items = array();
  $items['admin/appearance/settings/%/export'] = array(
    'title' => 'Export theme settings',
    'description' => 'Export theme settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'magic_export_settings',
      3,
    ),
    'access arguments' => array(
      'administer themes',
    ),
  );
  return $items;
}

/**
 * Implements hook_help().
 */
function magic_help($path, $arg) {
  $output = '';
  switch ($path) {
    case 'admin/help#magic':
      $output = '';
      $output .= '<p>' . t('All Drupal sites need some magic, so give yours some! Magic is a set of tools for front-end best practices and general front-end goodies to make your life as a front-end developer happier.') . '</p>';
      $output .= '<h2>' . t('Theme Settings Export') . '</h2>';
      $output .= '<p>' . t('To ensure the most flexibility, Drupal pulls your theme settings from a variety of places before returning the current setting to the module or theme requesting it. It will first pull from the base theme\'s .info file, then the active theme\'s .info file, then the global theme settings form, then the active theme\'s setting form. Each check will override the previous, so the active theme settings form settings will always override the theme\'s .info file.') . '</p>';
      $output .= '<p>' . t('To facilitate settings export, magic provides two methods of exporting your theme settings to version control. First, you can export your settings to a new .info file, although it should be noted that this will be overridden once settings are saved within the database. To keep control of variables within the database, you can either use !features and !strongarm to maintain the code, or add a $conf[] variable within your settings.php. Currently, we are working on exporting the $conf[] variable needed to put it within your settings.php.', array(
        '!features' => l(t('Features'), 'http://drupal.org/project/features'),
        '!strongarm' => l(t('Strongarm'), 'http://drupal.org/project/strongarm'),
      )) . '</p>';
      $output .= '<h2>' . t('Advanced CSS / JS Aggregation') . '</h2>';
      $output .= '<p>' . t('Documentation coming soon!') . '</p>';
      $output .= '<h2>' . t('Selective CSS / JS Removal') . '</h2>';
      $output .= '<p>' . t('Documentation coming soon!') . '</p>';
      break;
  }
  return $output;
}

/**
 * Exports theme settings.
 */
function magic_export_settings($form, &$form_state, $theme) {
  $path = drupal_get_path('theme', $theme);
  if ($path == '') {

    // The theme doesn't actually exist, return a 404 page.
    drupal_not_found();
    return;
  }
  $theme_info = drupal_parse_info_file($path . '/' . $theme . '.info');
  unset($theme_info['settings']);
  $name = filter_xss($theme_info['name']);
  drupal_set_title(t('Export theme settings for ' . $name));
  $settings = isset($theme_info['settings']) ? $theme_info['settings'] : array();
  $settings = variable_get('theme_' . $theme . '_settings', $settings);
  $theme_info['settings'] = $settings;
  $built_info = _magic_build_info_file($theme_info);
  $lines = substr_count($built_info, "\n");
  if ($lines > 40) {
    $lines = 40;
  }
  $form = array();
  $form['info'] = array(
    '#title' => t($name . '.info Contents'),
    '#description' => t('Make any changes you want to ' . $name . '.info then press the Save button to overwrite ' . $name . '.info. If the file is saved correctly, the variable settings within the database will be automatically removed.'),
    '#type' => 'textarea',
    '#rows' => $lines,
    '#default_value' => $built_info,
  );
  $form['settings'] = array(
    '#title' => t('Settings.php export'),
    '#type' => 'textarea',
    '#default_value' => _magic_build_settings_php_export($theme_info, $theme),
    '#description' => t('For a permanent settings save, copy and paste this into your settings.php file. If you do this, you cannot change the settings with the UI any longer.'),
  );
  $form['theme'] = array(
    '#type' => 'hidden',
    '#value' => $theme,
  );
  $form['save'] = array(
    '#type' => 'submit',
    '#value' => t('Save ' . $name . '.info'),
  );
  return $form;
}

/**
 * Submit hook for magic_export_settings().
 */
function magic_export_settings_submit($form, &$form_state) {
  $info = $form_state['values']['info'];
  $theme = $form['theme']['#value'];
  $destination = drupal_get_path('theme', $theme);
  $file = file_unmanaged_save_data($info, $destination . '/' . $theme . '.info', FILE_EXISTS_REPLACE);

  // Only mark that the settings have updated if the file saved.
  if ($file) {
    drupal_set_message(t('@theme has been updated.', array(
      '@theme' => $theme . '.info',
    )));

    // Since we exported the file, we want to delete the settings variable.
    // This will get remade whenever we want to edit the settings in the future,
    // but can always be re-removed.
    variable_del('theme_' . $theme . '_settings');

    // Rebuild the system .info file information.
    system_rebuild_theme_data();
    drupal_goto('admin/appearance/settings/' . $theme);
  }
  else {
    drupal_set_message(t('@theme has not been saved, an error has occured.', array(
      '@theme' => $theme . '.info',
    )), 'error');
  }
  return $file;
}

/**
 * Helper function to build the contents of the .info file.
 *
 * This function will take the current theme infomation, and create the export
 * based upon the current info file and settings saved within the database.
 *
 * @param array $array
 *   An array of all the current theme settings to be parsed.
 * @param string $prefix
 *   The prefix of something @fubhy made.
 *
 * @return string
 *   The full info file all prettified.
 */
function _magic_build_info_file($array, $prefix = '') {
  $info = '';
  $info_regions = array(
    'stylesheets',
    'regions',
    'settings',
    'scripts',
    'modernizr',
  );
  foreach ($array as $key => $value) {
    foreach ($info_regions as $region) {
      if ($key === $region) {
        $info .= "\n; ========================================";
        $info .= "\n" . '; ' . ucfirst($key) . "\n";
        $info .= "; ========================================\n\n";
      }
    }
    if (is_array($value)) {
      $info .= _magic_build_info_file($value, empty($prefix) ? $key : "{$prefix}[{$key}]");
    }
    else {
      $info .= $prefix ? "{$prefix}[" . $key . ']' : $key;
      $info .= " = '" . str_replace("'", "\\'", $value) . "'\n";
    }
  }
  return $info;
}

/**
 * A helper function to print the theme settings withing settings.php.
 *
 * @param array $array
 *   An array of the current settings saved.
 * @param string $theme
 *   The theme key we are building the export for.
 *
 * @return string
 */
function _magic_build_settings_php_export($array, $theme) {
  $export = '$conf[\'theme_' . $theme . '_settings\'] = ';
  $export .= var_export($array['settings'], TRUE);
  $export .= ';';
  return $export;
}

/**
 * Implements hook_form_alter().
 */
function magic_form_system_theme_settings_alter(&$form, &$form_state) {
  $theme = isset($form_state['build_info']['args'][0]) ? $form_state['build_info']['args'][0] : '';

  // Are we the global form? We do NOT wanna touch that craziness.
  if (empty($theme)) {
    return;
  }

  // Magic Performance Vertical Tabs set
  $magic_settings = array(
    '#type' => 'vertical_tabs',
    '#attached' => array(
      'css' => array(
        drupal_get_path('module', 'magic') . '/css/magic.admin.css',
      ),
    ),
    '#weight' => 100,
  );

  // CSS Magic Grouping
  $magic_settings['css'] = array(
    '#type' => 'fieldset',
    '#title' => t('CSS Enhancements'),
  );

  // Embedded Media Query Option
  $magic_settings['css']['magic_embedded_mqs'] = array(
    '#type' => 'checkbox',
    '#title' => t('Embed Media Queries'),
    '#description' => t('Embed media queries instead of attaching them to the <pre>&lt;link&gt;</pre> tags, reducing the number of separate CSS aggregates.'),
    '#default_value' => theme_get_setting('magic_embedded_mqs', $theme),
  );

  // CSS Exclude Options
  $magic_settings['css']['magic_css_excludes'] = array(
    '#type' => 'textarea',
    '#title' => t('Exclude CSS files'),
    '#description' => t("<p>One entry per line.</p> <p>The <strong><code>*</code></strong> character is a wildcard to match all similar items, for instance <code>system/*.css</code> will remove all CSS provided by the System module. <p>The <strong><code>~</code></strong> character is a reserved character to keep all similar items if they would otherwise be removed, for instance <code>~system/system.menus.css</code> to keep System module's menu CSS even if we remove the rest of System module's CSS. </p> <p>You may use: <br><strong><code>:all</code></strong> to target all CSS files. <br><strong><code>:core</code></strong> to target all Core provided CSS files. <br><strong><code>:contrib</code></strong> to target all Contrib provided CSS files. <br><strong><code>:base-theme</code></strong> to target all base theme provided CSS files. <br><strong><code>:current-theme</code></strong> to target all CSS files provided by the current theme."),
    '#default_value' => theme_get_setting('magic_css_excludes', $theme),
  );

  // JavaScript Magic Grouping
  $magic_settings['js'] = array(
    '#type' => 'fieldset',
    '#title' => t('JavaScript Enhancements'),
  );

  // Footer JavaScript Option
  $magic_settings['js']['magic_footer_js'] = array(
    '#type' => 'checkbox',
    '#title' => t('Move JavaScript to the Footer'),
    '#description' => t("Will move all JavaScript to the bottom of your page. This can be overridden on an individual basis by setting the <pre>'force header' => true</pre> option in <pre>drupal_add_js</pre> or by using <pre>hook_js_alter</pre> to add the option to other JavaScript files."),
    '#default_value' => theme_get_setting('magic_footer_js', $theme),
  );

  // Keep Libraries in Head JavaScript Option
  $magic_settings['js']['magic_library_head'] = array(
    '#type' => 'checkbox',
    '#title' => t('Keep Libraries in the Head'),
    '#description' => t("If you have JavaScript inline in the body of your document, such as if you are displaying ads, you may need to keep Drupal JS Libraries in the head instead of moving them to the footer. This will keep Drupal libraries in the head while still moving all other JavaScript to the footer."),
    '#default_value' => theme_get_setting('magic_library_head', $theme),
    '#states' => array(
      'visible' => array(
        ':input[name=magic_footer_js]' => array(
          'checked' => TRUE,
        ),
      ),
    ),
  );

  // Experimental JavaScript Option
  $magic_settings['js']['magic_experimental_js'] = array(
    '#type' => 'checkbox',
    '#title' => t('Use Experimental JavaScript Handling'),
    '#description' => t("This will enable additional options for JavaScript, including browser options, <pre>async</pre> and <pre>defer</pre> attributes, and optional additional attributes."),
    '#default_value' => theme_get_setting('magic_experimental_js', $theme),
  );

  // JavaScript Exclude Options
  $magic_settings['js']['magic_js_excludes'] = array(
    '#type' => 'textarea',
    '#title' => t('Exclude JavaScript files'),
    '#description' => t("<p>One entry per line.</p> <p>The <strong><code>*</code></strong> character is a wildcard to match all similar items, for instance <code>overlay/*.js</code> will remove all JS provided by the Overlay module.</p> <p>The <strong><code>~</code></strong> character is a reserved character to keep all similar items if they would otherwise be removed, for instance <code>~overlay/overlay-child.js</code> to keep Overlay module's overlay-child JS even if we remove the rest of Overlay module's JS.</p> <p>You may use: <br><strong><code>:all</code></strong> to target all JS files. <br><strong><code>:core</code></strong> to target all Core provided JS files. <br><strong><code>:contrib</code></strong> to target all Contrib provided JS files. <br><strong><code>:base-theme</code></strong> to target all base theme provided JS files. <br><strong><code>:current-theme</code></strong> to target all JS files provided by the current theme. <br><strong><code>:settings</code></strong> to remove the settings array."),
    '#default_value' => theme_get_setting('magic_js_excludes', $theme),
  );

  // We are add an extra submit / validate handler, other can add their own.
  $magic_settings['#submit'] = array();
  $magic_settings['#vaidate'] = array();

  // Run the magic settings through a hook. We do this because magic itself is
  // weighted lower than other modules for the CSS/JS work we do. This will
  // allow modules and themes to actually alter the form array we use.
  $magic_settings_hook = module_invoke_all('magic', $magic_settings, $theme);
  $magic_settings = array_merge_recursive($magic_settings, $magic_settings_hook);

  // Now, the fun part. Drupal doesn't actually allow the theme we are editing
  // to alter it. Because it does weird things. We are going to fix that.
  $all_themes = list_themes();
  $this_theme = $all_themes[$theme];
  $functions = array(
    $theme . '_magic_alter',
  );
  if (isset($this_theme->base_themes)) {
    foreach ($this_theme->base_themes as $base_theme_key => $base_theme) {
      $functions[] = $base_theme_key . '_magic_alter';
    }
  }
  $functions = array_reverse($functions);
  foreach ($functions as $function) {
    if (function_exists($function)) {
      $function($magic_settings, $theme);
    }
  }

  // We now need to merge any custom #submit or #validate hooks and add them to
  // the form. We then unset
  if (!empty($magic_settings['#submit'])) {
    $form['#submit'] = array_merge($form['#submit'], $magic_settings['#submit']);
  }
  unset($magic_settings['#submit']);
  if (!empty($magic_settings['#validate'])) {
    $form['#validate'] = array_merge($form['#validate'], $magic_settings['#validate']);
  }
  unset($magic_settings['#submit']);
  $form['magic_performance'] = $magic_settings;

  // Add export button to export current theme settings
  $form['actions']['export'] = array(
    '#type' => 'button',
    '#value' => t('Export Settings'),
    '#executes_submit_callback' => TRUE,
    '#submit' => array(
      'magic_goto_export',
    ),
  );

  // We need a custom submit handler to store the CSS and JS paths as arrays.
  array_unshift($form['#submit'], 'magic_extension_assets_theme_settings_form_submit');
}

/**
 * A helper function to send the form to the settings export page.
 */
function magic_goto_export($form, $form_state) {
  drupal_goto('admin/appearance/settings/' . arg(3) . '/export');
}

/**
 * Submit handler for the theme settings form.
 *
 * This form will take the theme settings, and for the css and js excludes,
 * create the regex that will be required to remove the css and js files at
 * will. It will then selectively clear the caches for those specific cache
 * items.
 */
function magic_extension_assets_theme_settings_form_submit($form, &$form_state) {
  $values = $form_state['values'];
  $clear_caches = array(
    'css' => FALSE,
    'js' => FALSE,
  );
  foreach (array(
    'magic_css_excludes',
    'magic_js_excludes',
  ) as $item) {

    // Check to see if this has changed since the last save. If so, we have to
    // rebuild the regex and clear our caches.
    $current_settings = variable_get($values['var'], FALSE);
    if (!isset($current_settings[$item . '_regex']) || $values[$item] !== $current_settings[$item]) {

      // Since we are updating the script, we must clear our caches after. We
      // set it this way, so that we don't clear them twice if both the CSS and
      // JS changed.
      $type = $item == 'magic_css_excludes' ? 'css' : 'js';
      $clear_caches[$type] = TRUE;

      // Explode and trim the values for the exclusion rules.
      $excludes = array_filter(array_map('trim', explode("\n", $values[$item])));
      if (!empty($excludes)) {
        module_load_include('inc', 'magic', 'includes/magic');

        // Now we get the regex and set that.
        $excludes = magic_generate_exclude_full($excludes);
        $excludes['exclude'] = magic_generate_path_regex($excludes['exclude']);
        $excludes['include'] = magic_generate_path_regex($excludes['include']);
        if ($item == 'magic_css_excludes') {

          // Make sure that RTL styles are excluded as well when a file name has been
          // specified with it's full .css file extension.
          $excludes['exclude'] = preg_replace('/\\\\.css$/', '(\\.css|-rtl\\.css)', $excludes['exclude']);
          $excludes['include'] = preg_replace('/\\\\.css$/', '(\\.css|-rtl\\.css)', $excludes['include']);
        }

        // Last check, if we didnt actually have anything in excludes, we don't
        // save a thing.
        if ($excludes['exclude'] == FALSE) {
          $excludes = FALSE;
        }
      }
      else {

        // We have nothing we are excluding, ignore the setting.
        $excludes = FALSE;
      }
    }
    else {

      // If we dont re-set the form value, it will get removed.
      $excludes = $current_settings[$item . '_regex'];
    }
    form_set_value(array(
      '#parents' => array(
        $item . '_regex',
      ),
    ), $excludes, $form_state);
  }

  // Now we will actually clear the caches.
  if (!empty($clear_caches)) {
    _magic_clear_cache(substr($values['var'], '6', '-9'), $clear_caches);
  }
}

/**
 * Implements hook_theme_registry_alter().
 */
function magic_theme_registry_alter(&$registry) {
  if (($index = array_search('template_process_html', $registry['html']['process functions'], TRUE)) !== FALSE) {
    array_splice($registry['html']['process functions'], $index, 1, 'magic_template_process_html_override');
  }
}

/**
 * Overrides template_process_html() in order to provide support for awesome new stuffzors!
 *
 * Huge, amazing ups to the wizard Sebastian Siemssen (fubhy) for showing me how to do this.
 */
function magic_template_process_html_override(&$variables) {

  // Render page_top and page_bottom into top level variables.
  $variables['page_top'] = drupal_render($variables['page']['page_top']);
  $variables['page_bottom'] = drupal_render($variables['page']['page_bottom']);

  // Place the rendered HTML for the page body into a top level variable.
  $variables['page'] = $variables['page']['#children'];
  $variables['head'] = drupal_get_html_head();
  $variables['css'] = drupal_add_css();
  $variables['styles'] = drupal_get_css();
  if (theme_get_setting('magic_experimental_js')) {
    module_load_include('inc', 'magic', 'includes/scripts-experimental');
    $variables['page_bottom'] .= magic_experimental_js('footer');
    $variables['scripts'] = magic_experimental_js('header');
  }
  elseif (theme_get_setting('magic_footer_js')) {
    module_load_include('inc', 'magic', 'includes/scripts');
    $variables['page_bottom'] .= magic_get_js('footer');
    $variables['scripts'] = magic_get_js('header');
  }
  else {
    $variables['page_bottom'] .= drupal_get_js('footer');
    $variables['scripts'] = drupal_get_js();
  }
}

/**
 * Implements hook_element_info_alter().
 *
 * This changes Drupal's default css aggregation to use magic's version.
 */
function magic_element_info_alter(&$types) {
  if (isset($types['styles'])) {
    $types['styles']['#group_callback'] = 'magic_group_css';
    $types['styles']['#aggregate_callback'] = 'magic_aggregate_css';
  }
}

/**
 * Checks the magic cache.
 *
 * @param string $cid
 *   A string cache id. {theme_name}_{css/js}_{md4_hash}
 *
 * @return array
 *   Either the array of the cached data, or FALSE.
 */
function _magic_check_cache($cid) {
  $data = cache_get($cid, 'cache_magic');
  return $data ? unserialize($data->data) : FALSE;
}

/**
 * Sets the magic cache.
 *
 * @param string $cid
 *   A string cache id. {theme_name}_{css/js}_{md4_hash}
 * @param array $data
 *   The array of data to store within the cache.
 *
 * @return object
 *   A cache data object.
 */
function _magic_set_cache($cid, $data) {
  $data = serialize($data);
  return cache_set($cid, $data, 'cache_magic', CACHE_TEMPORARY);
}

/**
 * A helper function to clear the magic cache.
 *
 * @param mixed $theme (default: FALSE)
 *   The theme name, or FALSE to delete all caches.
 * @param array $types (default: array())
 *   The type of caches to remove. May have an array keyed with 'css' or 'js' or
 *   leave empty to clear both.
 */
function _magic_clear_cache($theme = FALSE, $types = array()) {
  if ($theme) {

    // We are only clearing the cache for a specific theme, not all caches.
    if (empty($types) || $types['css'] == TRUE) {
      cache_clear_all($theme . '_css_', 'cache_magic', TRUE);
    }
    if (empty($types) || $types['js'] == TRUE) {
      cache_clear_all($theme . '_js_', 'cache_magic', TRUE);
    }
  }
  else {

    // No settings were passed, we will clear all caches.
    cache_clear_all(NULL, 'cache_magic');
  }
}

/**
 * Implements hook_flush_caches().
 */
function magic_flush_caches() {
  return array(
    'cache_magic',
  );
}

/**
 * Implements hook_drush_cache_clear().
 */
function magic_drush_cache_clear(&$types) {
  $types['magic'] = '_magic_clear_cache';
}

/**
 * Implements hook_css_alter().
 */
function magic_css_alter(&$css) {
  magic_css_js_alter($css, 'css');
}

/**
 * Implements hook_js_alter().
 */
function magic_js_alter(&$js) {
  magic_css_js_alter($js, 'js');
}

/**
 * Helper function to remove unwanted css or js.
 *
 * @param array $data
 *   The css or js array.
 * @param string $type (default: 'css')
 *   Either 'css' or 'js' depending on the file array.
 */
function magic_css_js_alter(&$data, $type = 'css') {
  global $theme_key;

  // First check to see if we are even going to exclude anything.
  $excludes = theme_get_setting("magic_{$type}_excludes_regex");
  if (is_array($excludes)) {

    // We get the hash of the css array to check if it has been passed already.
    $cid = $theme_key . "_{$type}_" . hash('md4', serialize($data));
    $cache = _magic_check_cache($cid);
    if ($cache) {

      // We have this array cached, use it.
      $data = $cache;
    }
    else {
      module_load_include('inc', 'magic', 'includes/magic');
      magic_exclude_assets($data, $excludes['exclude'], $excludes['include']);
      _magic_set_cache($cid, $data);
    }
  }
}

Functions

Namesort descending Description
magic_css_alter Implements hook_css_alter().
magic_css_js_alter Helper function to remove unwanted css or js.
magic_drush_cache_clear Implements hook_drush_cache_clear().
magic_element_info_alter Implements hook_element_info_alter().
magic_export_settings Exports theme settings.
magic_export_settings_submit Submit hook for magic_export_settings().
magic_extension_assets_theme_settings_form_submit Submit handler for the theme settings form.
magic_flush_caches Implements hook_flush_caches().
magic_form_system_theme_settings_alter Implements hook_form_alter().
magic_goto_export A helper function to send the form to the settings export page.
magic_help Implements hook_help().
magic_js_alter Implements hook_js_alter().
magic_menu Implements hook_menu().
magic_template_process_html_override Overrides template_process_html() in order to provide support for awesome new stuffzors!
magic_theme_registry_alter Implements hook_theme_registry_alter().
_magic_build_info_file Helper function to build the contents of the .info file.
_magic_build_settings_php_export A helper function to print the theme settings withing settings.php.
_magic_check_cache Checks the magic cache.
_magic_clear_cache A helper function to clear the magic cache.
_magic_set_cache Sets the magic cache.

Constants

Namesort descending Description
JS_DRUPAL
JS_JQUERY