diff --git a/advagg_mod/advagg_mod.admin.inc b/advagg_mod/advagg_mod.admin.inc index fe1477d..a18b065 100644 --- a/advagg_mod/advagg_mod.admin.inc +++ b/advagg_mod/advagg_mod.admin.inc @@ -54,9 +54,61 @@ function advagg_mod_admin_settings_form() { '#description' => module_exists('advagg_bundler') ? t('You might want to increase the CSS Bundles Per Page if this is checked.', array('@link' => url($config_path . '/advagg/bundler'))) : '', ); + $form['remove_files'] = array( + '#type' => 'fieldset', + '#title' => t('Remove CSS/JS files'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form['remove_files']['instructions'] = array( + '#type' => 'item', + '#title' => t('Instructions'), + '#markup' => t("

Enter one file per line.

The * character is a wildcard to match all similar items, for instance system/*.css will remove all CSS provided by the System module; overlay/*.js will remove all JS provided by the Overlay module.

The ~ character is a reserved character to keep all similar items if they would otherwise be removed, for instance ~system/system.menus.css to keep System module's menu CSS even if we remove the rest of System module's CSS; ~overlay/overlay-child.js to keep Overlay module's overlay-child JS even if we remove the rest of Overlay module's JS.

You may use
:all to target all CSS/JS files
:core to target all Core provided CSS/JS files
:contrib to target all Contrib provided CSS/JS files
:base-theme to target all base theme provided CSS/JS files
:current-theme to target all CSS/JS files provided by the current theme."), + ); + $list_themes = list_themes(); + foreach ($list_themes as $values) { + $form['remove_files'][$values->name] = array( + '#type' => 'fieldset', + '#title' => t('@name - @version', array( + '@name' => $values->info['name'], + '@version' => $values->info['version'], + )), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form['remove_files'][$values->name]['theme_info'] = array( + '#type' => 'item', + '#title' => $values->status ? t('Enabled') : t('Disabled') , + '#markup' => t('@filename
@description', array( + '@description' => $values->info['description'], + '@filename' => $values->filename, + )), + ); + $form['remove_files'][$values->name]['advagg_mod_css_excludes_' . $values->name] = array( + '#type' => 'textarea', + '#title' => t('Remove CSS files'), + '#default_value' => variable_get('advagg_mod_css_excludes_' . $values->name, ''), + ); + $form['remove_files'][$values->name]['advagg_mod_css_excludes_' . $values->name . '_regex'] = array( + '#type' => 'hidden', + '#default_value' => variable_get('advagg_mod_css_excludes_' . $values->name . '_regex', array()), + ); + $form['remove_files'][$values->name]['advagg_mod_js_excludes_' . $values->name] = array( + '#type' => 'textarea', + '#title' => t('Remove JS files'), + '#default_value' => variable_get('advagg_mod_js_excludes_' . $values->name, ''), + ); + $form['remove_files'][$values->name]['advagg_mod_js_excludes_' . $values->name . '_regex'] = array( + '#type' => 'hidden', + '#default_value' => variable_get('advagg_mod_js_excludes_' . $values->name . '_regex', array()), + ); + } + $form['landing_page'] = array( '#type' => 'fieldset', '#title' => t('Inline CSS/JS on specific pages'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, ); // Taken from block_admin_configure(). $access = user_access('use PHP for settings'); @@ -113,6 +165,8 @@ function advagg_mod_admin_settings_form() { '#maxlength' => 128, ); + // Set excludes values. + $form['#submit'][] = 'advagg_mod_admin_settings_form_submit_excludes'; // Clear the cache bins on submit. $form['#submit'][] = 'advagg_mod_admin_settings_form_submit'; @@ -129,3 +183,199 @@ function advagg_mod_admin_settings_form_submit($form, &$form_state) { cache_clear_all('*', $bin, TRUE); } } + +/** + * 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 advagg_mod_admin_settings_form_submit_excludes($form, &$form_state) { + // Pull out the values we care about. + $exclude_list = array(); + foreach ($form_state['values'] as $key => $value) { + // Reset for this run. + $theme_name = ''; + $type = ''; + $saved_value = ''; + + // Extract info. + if (strpos($key, 'advagg_mod_css_excludes_') === 0 && strpos($key, '_regex') === FALSE) { + $theme_name = substr($key, strlen('advagg_mod_css_excludes_')); + $type = 'css'; + $saved_value = variable_get('advagg_mod_css_excludes_' . $theme_name, ''); + } + elseif (strpos($key, 'advagg_mod_js_excludes_') === 0 && strpos($key, '_regex') === FALSE) { + $theme_name = substr($key, strlen('advagg_mod_js_excludes_')); + $type = 'js'; + $saved_value = variable_get('advagg_mod_js_excludes_' . $theme_name, ''); + } + + // Skip if not the right setting. + if (empty($theme_name)) { + continue; + } + + // Explode and trim the values for the exclusion rules. + $value = implode("\n", array_filter(array_map('trim', explode("\n", $value)))); + + // Skip if nothing to do. + if ($saved_value === $value) { + continue; + } + + $excludes = array(); + if (!empty($value)) { + $excludes = explode("\n", $value); + } + if (!empty($excludes)) { + // Now we get the regex and set that. + $excludes = advagg_mod_generate_exclude_full($excludes); + + $excludes['exclude'] = advagg_mod_generate_path_regex($excludes['exclude']); + $excludes['include'] = advagg_mod_generate_path_regex($excludes['include']); + + if ($type = 'css') { + // 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 = array(); + } + } + $form_state['values'][$key . '_regex'] = $excludes; + } +} + +/** + * Helper function to change magic keywords into the exclude array. + * + * This helper function will remove kewords such as :contrib or :base-theme and + * change them into the paths that they represent. + * + * @param $exclude array + * An array of paths to remove. + * + * @return array + * An array with 'exclude' and 'include' as the keys. Exclude contains a set + * of files to remove, while 'include' has the files to keep. + */ +function advagg_mod_generate_exclude_full($array) { + global $base_theme_info, $theme_info; + + $return = array( + 'exclude' => array(), + 'include' => array(), + ); + + foreach ($array as $item) { + $invert = substr($item, 0, 1) == '~' ? TRUE : FALSE; + + if ($invert) { + $item = substr($item, 1); + } + + $items = array(); + + // We now check the string against a set of standard variables. + if ($item == ':all') { + $items[] = '*'; + } + elseif ($item == ':core') { + // This is unique as there are several areas where core files might be. + $items = array( + 'misc/*', + 'modules/*', + 'themes/*', + ); + } + elseif ($item == ':contrib') { + $items[] = 'sites/all/modules/*'; + } + elseif ($item == ':base-theme') { + if (empty($base_theme_info)) { + // We do not actually have a base theme. + continue ; + } + + $items[] = drupal_get_path('theme', $base_theme_info[0]->name) . '/*'; + } + elseif ($item == ':current-theme') { + $items[] = drupal_get_path('theme', $theme_info->name) . '/*'; + } + else { + $items[] = $item; + } + + if ($invert) { + $return['include'] = array_merge($return['include'], $items); + } + else { + $return['exclude'] = array_merge($return['exclude'], $items); + } + } + + + return $return; +} + +/** + * Helper function for generating a regex from a list of paths. + * + * Generates a single regex from a list of file paths that can be used to match + * JS or CSS files using preg_grep() for example in hook_css_alter() or + * hook_js_alter(). The '*' (asterisk) character can be used as a wild-card. + * + * @param $paths + * An array of file paths. + * + * @return string + * The generated regex. + * + * @see hook_js_alter() + * @see hook_css_alter() + */ +function advagg_mod_generate_path_regex($paths) { + foreach ($paths as &$item) { + // The first segment (everything before the first slash) is the namespace. + // This rule only applies to local files... So if the namespace can not be + // mapped to a module, profile or theme engine we assume that the we are + // trying to target an external file. + list($namespace) = explode('/', $item); + + // Check if the namespace refers to a file residing in the 'misc' folder. + if ($namespace != '*') { + if ($namespace == 'misc') { + $prefix = DRUPAL_ROOT . '/misc'; + $item = substr_replace($item, $prefix, 0, strlen($namespace)); + } + else { + // Otherwise, check if it refers to a theme, module, profile or theme + // engine. + foreach (array('theme', 'module', 'profile', 'theme_engine') as $type) { + // We can't use drupal_get_path() directly because that uses dirname() + // internally which returns '.' if no filename was found. + if ($filename = drupal_get_filename($type, $namespace)) { + $prefix = dirname($filename); + $item = substr_replace($item, $prefix, 0, strlen($namespace)); + break; + } + } + } + } + + // Escape any regex characters and turn asterisk wildcards into actual regex + // wildcards. + $item = preg_quote($item, '/'); + $item = str_replace('\*', '(.*)', $item); + } + + return empty($paths) ? FALSE : '/^(' . implode('|', $paths) . ')$/'; +} diff --git a/advagg_mod/advagg_mod.module b/advagg_mod/advagg_mod.module index 5c7efe6..857e89a 100644 --- a/advagg_mod/advagg_mod.module +++ b/advagg_mod/advagg_mod.module @@ -91,6 +91,9 @@ function advagg_mod_menu() { * Implements hook_js_alter(). */ function advagg_mod_js_alter(&$js) { + // Remove any JS files if configured to do to. + advagg_mod_remove_files($js, 'js'); + // Move all JS to the footer. $move_js_to_footer = variable_get('advagg_mod_js_footer', ADVAGG_MOD_JS_FOOTER); if (!empty($move_js_to_footer)) { @@ -162,6 +165,9 @@ function advagg_mod_js_alter(&$js) { * Implements hook_css_alter(). */ function advagg_mod_css_alter(&$css) { + // Remove any CSS files if configured to do to. + advagg_mod_remove_files($css, 'css'); + // Do not use preprocessing if CSS is inlined. if (advagg_mod_inline_page()) { advagg_mod_inline_css($css); @@ -361,3 +367,92 @@ function advagg_mod_match_path($pages, $visibility) { return $page_match; } + +/** + * Helper function to remove unwanted css or js. + * + * @param array $data + * The CSS or JS array. + * @param string $type + * Either 'css' or 'js' depending on the file array. + */ +function advagg_mod_remove_files(&$data, $type) { + // First check to see if we are even going to exclude anything. + $excludes = variable_get('advagg_mod_' . $type . '_excludes_' . $GLOBALS['theme_key'] . '_regex', array()); + if (empty($excludes) || empty($excludes['exclude'])) { + return; + } + + // Using regex, remove files from the CSS/JS array. + advagg_mod_exclude_assets($data, $excludes['exclude'], $excludes['include']); +} + +/** + * Helper function for eliminating elements from an array using a simplified + * regex pattern. + * + * @param $elements + * The array of elements that should have some of its items removed. + * @param $regex + * A regex as generated by omega_generate_path_regex(). + */ +function advagg_mod_exclude_assets(&$elements, $exclude, $include) { + $mapping = advagg_mod_generate_asset_mapping($elements); + + // We first check to see if we have an include array. If not, we don't need to + // check it as well. + if (!empty($include)) { + // We do a grep on each the exclude list, and include list and return the keys + // of the exclude list minus those that are specifically included. + + $full_exclude = array_diff_key(preg_grep($exclude, $mapping), preg_grep($include, $mapping)); + + // Finally, implode the array of items to exclude into a proper regex and + // invoke in on the array of files to be excluded. + $elements = array_diff_key($elements, $full_exclude); + } + else { + // We only have an exclude array, so we only have to do a preg_grep for it. + $elements = array_diff_key($elements, preg_grep($exclude, $mapping)); + } +} + +/** + * Helper function for generating a map of assets based on the data attribute. + * + * We can not rely on the array keys of the JS and CSS file arrays in Drupal + * because in case of inline JS or CSS (which uses numerical array keys) and due + * to potential overrides of the 'data' attribute which holds the actual, + * reliable path of the file. This function returns a single-level array of + * reliable JS/CSS file paths using the original array keys as keys. Elements of + * type 'inline' or 'setting' are ignored. + * + * @param $elements + * An array of JS or CSS files as given in hook_css_alter() or + * hook_js_alter(). + * + * @return array + * A map of file paths generated from $elements. + * + * @see hook_js_alter() + * @see hook_css_alter() + */ +function advagg_mod_generate_asset_mapping($elements) { + $mapping = array(); + foreach ($elements as $key => $item) { + if ($item['type'] == 'inline' || $item['type'] == 'setting') { + // in-line CSS/JS is not supported. + continue; + } + + // We need to build an array containing just the 'data' attribute because + // that's the actual path of the file. The array key of the elements can + // be something else if someone is sneaky enough to use drupal_add_js() or + // drupal_add_css() with a bogus first argument (normally, that is the + // path to the file) and then specify the actual path through the 'data' + // attribute in the $options array. + $mapping[$key] = $item['data']; + } + + return $mapping; +}