Advanced aggregation font module.

File

advagg_font/advagg_font.module

View source
<?php


/**
 * @file
 * Advanced aggregation font module.
 */

/**
 * @addtogroup default_variables
 * @{
 */

/**
 * Default value to use font face observer for asynchronous font loading.
 */
define('ADVAGG_FONT_FONTFACEOBSERVER', 0);

/**
 * Default value to include font info in critical css.
 */
define('ADVAGG_FONT_ADD_TO_CRITICAL_CSS', 1);

/**
 * Default value to use localStorage in order to prevent the FOUT.
 */
define('ADVAGG_FONT_STORAGE', 1);

/**
 * Default value to use a cookie in order to prevent the FOUT.
 */
define('ADVAGG_FONT_COOKIE', 1);

/**
 * Default value to only replace the font if it's been downloaded.
 */
define('ADVAGG_FONT_NO_FOUT', 0);

/**
 * @} End of "addtogroup default_variables".
 */

/**
 * @addtogroup hooks
 * @{
 */

/**
 * Implements hook_module_implements_alter().
 */
function advagg_font_module_implements_alter(&$implementations, $hook) {
    // Move advagg to the bottom.
    if ($hook === 'page_alter' && array_key_exists('advagg_font', $implementations)) {
        $item = $implementations['advagg_font'];
        unset($implementations['advagg_font']);
        $implementations['advagg_font'] = $item;
    }
}

/**
 * Implements hook_page_alter().
 */
function advagg_font_page_alter() {
    // Skip if advagg is disabled.
    if (!advagg_enabled()) {
        return;
    }
    $advagg_font_ffo = variable_get('advagg_font_fontfaceobserver', ADVAGG_FONT_FONTFACEOBSERVER);
    // Fontface Observer is disabled.
    if (empty($advagg_font_ffo)) {
        return;
    }
    // Add settings.
    drupal_add_js(array(
        'advagg_font' => array(),
        'advagg_font_storage' => variable_get('advagg_font_storage', ADVAGG_FONT_STORAGE),
        'advagg_font_cookie' => variable_get('advagg_font_cookie', ADVAGG_FONT_COOKIE),
        'advagg_font_no_fout' => variable_get('advagg_font_no_fout', ADVAGG_FONT_NO_FOUT),
    ), array(
        'type' => 'setting',
    ));
    // Add inline script for reading the cookies and adding the fonts already
    // loaded to the html class.
    if (variable_get('advagg_font_cookie', ADVAGG_FONT_COOKIE) || variable_get('advagg_font_storage', ADVAGG_FONT_STORAGE)) {
        $inline_script_min = 'for(var fonts=document.cookie.split("advaggf"),i=0;i<fonts.length;i++){var font=fonts[i].split("="),pos=font[0].indexOf("ont_");-1!==pos&&(window.document.documentElement.className+=" "+font[0].substr(4).replace(/[^a-zA-Z0-9\\-]/g,""))}if(void 0!==Storage){fonts=JSON.parse(localStorage.getItem("advagg_fonts"));var current_time=(new Date).getTime();for(var key in fonts)fonts[key]>=current_time&&(window.document.documentElement.className+=" "+key.replace(/[^a-zA-Z0-9\\-]/g,""))}';
        drupal_add_js($inline_script_min, array(
            'type' => 'inline',
            'group' => JS_LIBRARY - 1,
            'weight' => -50000,
            'scope' => 'above_css',
            'scope_lock' => TRUE,
            'movable' => FALSE,
            'no_defer' => TRUE,
        ));
    }
    // Get library data for fontfaceobserver.
    $library = advagg_get_library('fontfaceobserver', 'advagg_font');
    // If libraries_load() does not exist load library externally.
    if (!is_callable('libraries_load')) {
        $advagg_font_ffo = 6;
    }
    // Add fontfaceobserver.js.
    if ($advagg_font_ffo != 6 && empty($library['installed'])) {
        // The fontfaceobserver library is not installed; use external variant.
        $advagg_font_ffo = 6;
    }
    if ($advagg_font_ffo == 6) {
        // Use the external variant.
        foreach ($library['variants']['external']['files']['js'] as $data => $options) {
            drupal_add_js($data, $options);
        }
    }
    else {
        // Load the fontfaceobserver library.
        if ($advagg_font_ffo == 2) {
            // Use the inline variant.
            libraries_load('fontfaceobserver', 'inline');
        }
        else {
            libraries_load('fontfaceobserver');
        }
    }
    // Add advagg_font.js; sets cookie and changes the class of the top level
    // element once a font has been downloaded.
    $file_path = drupal_get_path('module', 'advagg_font') . '/advagg_font.js';
    drupal_add_js($file_path, array(
        'async' => TRUE,
        'defer' => TRUE,
    ));
}

/**
 * Implements hook_css_alter().
 */
function advagg_css_alter(&$css) {
    // Skip if advagg is disabled.
    if (!advagg_enabled()) {
        return;
    }
    // Skip if fontface is disabled.
    if (empty(variable_get('advagg_font_fontfaceobserver', ADVAGG_FONT_FONTFACEOBSERVER))) {
        return;
    }
    // Skip if fonts added to critical css is disabled.
    if (empty(variable_get('advagg_font_add_to_critical_css', ADVAGG_FONT_ADD_TO_CRITICAL_CSS))) {
        return;
    }
    $critical_css_key = NULL;
    foreach ($css as $key => $values) {
        if (!empty($values['critical-css']) && $values['type'] === 'inline') {
            $critical_css_key = $key;
        }
    }
    // Skip if no critical css.
    if (is_null($critical_css_key)) {
        return;
    }
    module_load_include('inc', 'advagg', 'advagg');
    $css_to_add = '';
    foreach ($css as $key => $values) {
        if ($values['type'] === 'file') {
            $info = advagg_get_info_on_file($key);
            if (!empty($info['advagg_font'])) {
                // Get the file contents.
                $file_contents = (string) @advagg_file_get_contents($info['data']);
                if (empty($file_contents)) {
                    continue;
                }
                list($replacements) = advagg_font_get_replacements_array($file_contents);
                foreach ($replacements as $replace) {
                    $css_to_add .= $replace[2];
                }
            }
        }
    }
    if (!empty($css_to_add)) {
        $css[$critical_css_key]['data'] .= "\n{$css_to_add}";
    }
}

/**
 * Implements hook_menu().
 */
function advagg_font_menu() {
    $file_path = drupal_get_path('module', 'advagg_font');
    $config_path = advagg_admin_config_root_path();
    $items[$config_path . '/advagg/font'] = array(
        'title' => 'Async Font Loader',
        'description' => 'Load external fonts in a non blocking manner.',
        'page callback' => 'drupal_get_form',
        'page arguments' => array(
            'advagg_font_admin_settings_form',
        ),
        'type' => MENU_LOCAL_TASK,
        'access arguments' => array(
            'administer site configuration',
        ),
        'file path' => $file_path,
        'file' => 'advagg_font.admin.inc',
        'weight' => 10,
    );
    return $items;
}

/**
 * @} End of "addtogroup hooks".
 */

/**
 * @addtogroup 3rd_party_hooks
 * @{
 */

/**
 * Implements hook_libraries_info().
 */
function advagg_font_libraries_info() {
    $libraries['fontfaceobserver'] = array(
        // Only used in administrative UI of Libraries API.
'name' => 'fontfaceobserver',
        'vendor url' => 'https://github.com/bramstein/fontfaceobserver',
        'download url' => 'https://github.com/bramstein/fontfaceobserver/archive/master.zip',
        'version arguments' => array(
            'file' => 'package.json',
            // 1.50. : "version": "1.5.0".
'pattern' => '/"version":\\s+"([0-9\\.]+)"/',
            'lines' => 100,
            'default_version' => '2.1.0',
        ),
        'remote' => array(
            'callback' => 'advagg_get_github_version_json',
            'url' => 'https://cdn.jsdelivr.net/gh/bramstein/fontfaceobserver@master/package.json',
        ),
        'files' => array(
            'js' => array(
                'fontfaceobserver.js' => array(
                    'type' => 'file',
                    'group' => JS_LIBRARY,
                    'async' => TRUE,
                    'defer' => TRUE,
                ),
            ),
        ),
        'variants' => array(),
    );
    // Get the latest tagged version for external file loading.
    $version = advagg_get_remote_libraries_version('fontfaceobserver', $libraries['fontfaceobserver']);
    $libraries['fontfaceobserver']['variants'] += array(
        'external' => array(
            'files' => array(
                'js' => array(
                    "https://cdn.jsdelivr.net/gh/bramstein/fontfaceobserver@v{$version}/fontfaceobserver.js" => array(
                        'type' => 'external',
                        'data' => "https://cdn.jsdelivr.net/gh/bramstein/fontfaceobserver@v{$version}/fontfaceobserver.js",
                        'async' => TRUE,
                        'defer' => TRUE,
                    ),
                ),
            ),
        ),
    );
    // Inline if local js is there.
    $libraries_paths = array();
    if (is_callable('libraries_get_libraries')) {
        $libraries_paths = libraries_get_libraries();
    }
    if (!empty($libraries_paths['fontfaceobserver']) && is_readable($libraries_paths['fontfaceobserver'] . '/fontfaceobserver.js')) {
        $libraries['fontfaceobserver']['variants'] += array(
            'inline' => array(
                'files' => array(
                    'js' => array(
                        'loadCSS_inline' => array(
                            'type' => 'inline',
                            'data' => (string) @advagg_file_get_contents($libraries_paths['fontfaceobserver'] . '/fontfaceobserver.js'),
                            'no_defer' => TRUE,
                        ),
                    ),
                ),
            ),
        );
    }
    return $libraries;
}

/**
 * @} End of "addtogroup 3rd_party_hooks".
 */

/**
 * @addtogroup advagg_hooks
 * @{
 */

/**
 * Implements hook_advagg_current_hooks_hash_array_alter().
 */
function advagg_font_advagg_current_hooks_hash_array_alter(&$aggregate_settings) {
    $aggregate_settings['variables']['advagg_font_fontfaceobserver'] = variable_get('advagg_font_fontfaceobserver', ADVAGG_FONT_FONTFACEOBSERVER);
}

/**
 * @} End of "addtogroup advagg_hooks".
 */

/**
 * Get the replacements array for the css.
 *
 * @param string $css_string
 *   String of CSS.
 *
 * @return array
 *   An array containing the replacemnts and the font class name.
 */
function advagg_font_get_replacements_array($css_string) {
    // Get the CSS that contains a font-family rule.
    $length = strlen($css_string);
    $property_position = 0;
    $property = 'font';
    $property_alt = 'font-family';
    $replacements = array();
    $fonts_with_no_replacements = array();
    $lower = strtolower($css_string);
    $safe_fonts_list = array(
        'georgia' => TRUE,
        'palatino' => TRUE,
        'times new roman' => TRUE,
        'times' => TRUE,
        'arial' => TRUE,
        'helvetica' => TRUE,
        'gadget' => TRUE,
        'verdana' => TRUE,
        'geneva' => TRUE,
        'tahoma' => TRUE,
        'garamond' => TRUE,
        'bookman' => TRUE,
        'comic sans ms' => TRUE,
        'cursive' => TRUE,
        'trebuchet ms' => TRUE,
        'arial black' => TRUE,
        'impact' => TRUE,
        'charcoal' => TRUE,
        'courier new' => TRUE,
        'courier' => TRUE,
        'monaco' => TRUE,
        'system' => TRUE,
    );
    while (($property_position = strpos($lower, $property, $property_position)) !== FALSE) {
        // Find the start of the values for the property.
        $start_of_values = strpos($css_string, ':', $property_position);
        // Get the property at this location of the css.
        $property_in_loop = trim(substr($css_string, $property_position, $start_of_values - $property_position));
        // Make sure this property is one of the ones we're looking for.
        if ($property_in_loop !== $property && $property_in_loop !== $property_alt) {
            $property_position += strlen($property);
            continue;
        }
        // Get position of the last closing bracket plus 1 (start of this section).
        $start = strrpos($css_string, '}', -($length - $property_position));
        if ($start === FALSE) {
            // Property is in the first selector and a declaration block (full rule
            // set).
            $start = 0;
        }
        else {
            // Add one to start after the }.
            $start++;
        }
        // Get closing bracket (end of this section).
        $end = strpos($css_string, '}', $property_position);
        if ($end === FALSE) {
            // The end is the end of this file.
            $end = $length;
        }
        // Get closing ; in order to get the end of the declaration of the property.
        $declaration_end_a = strpos($css_string, ';', $property_position);
        $declaration_end_b = strpos($css_string, '}', $property_position);
        if ($declaration_end_a === FALSE) {
            $declaration_end = $declaration_end_b;
        }
        else {
            $declaration_end = min($declaration_end_a, $declaration_end_b);
        }
        if ($declaration_end > $end) {
            $declaration_end = $end;
        }
        // Add one in order to capture the } when we ge the full rule set.
        $end++;
        // Advance position for the next run of the while loop.
        $property_position = $end;
        // Get values assigned to this property.
        $values_string = substr($css_string, $start_of_values + 1, $declaration_end - ($start_of_values + 1));
        // Parse values string into an array of values.
        $values_array = explode(',', $values_string);
        if (empty($values_array)) {
            continue;
        }
        // Values array, first element is a quoted string.
        $dq = strpos($values_array[0], '"');
        $sq = strpos($values_array[0], "'");
        $quote_pos = $sq !== FALSE ? $sq : $dq;
        // Skip if the first font is not quoted.
        if ($quote_pos === FALSE) {
            continue;
        }
        $values_array[0] = trim($values_array[0]);
        // Skip if only one font is listed.
        if (count($values_array) === 1) {
            $fonts_with_no_replacements[$values_array[0]] = '';
            continue;
        }
        // Save the first value to a variable; starting at the quote.
        $removed_value_original = substr($values_array[0], max($quote_pos - 1, 0));
        // Resave first value.
        if ($quote_pos > 1) {
            $values_array[0] = trim(substr($values_array[0], 0, $quote_pos - 1));
        }
        // Get value as a classname. Remove quotes, trim, lowercase, and replace
        // spaces with dashes.
        $removed_value_classname = strtolower(trim(str_replace(array(
            '"',
            "'",
        ), '', $removed_value_original)));
        $removed_value_classname = str_replace(' ', '-', $removed_value_classname);
        // Remove value if it contains a quote.
        $values_array_copy = $values_array;
        foreach ($values_array as $key => $value) {
            if (strpos($value, '"') !== FALSE || strpos($value, "'") !== FALSE) {
                unset($values_array[$key]);
            }
            elseif ($key !== 0) {
                break;
            }
        }
        if (empty($values_array)) {
            // See if there's a "safe" fallback that is quoted.
            $values_array = $values_array_copy;
            foreach ($values_array as $key => $value) {
                if (strpos($value, '"') !== FALSE || strpos($value, "'") !== FALSE) {
                    if ($key !== 0) {
                        $lower_key = trim(trim(strtolower(trim($value)), '"'), "'");
                        if (!empty($safe_fonts_list[$lower_key])) {
                            break;
                        }
                    }
                    unset($values_array[$key]);
                }
                elseif ($key !== 0) {
                    break;
                }
            }
            if (empty($values_array)) {
                // No unquoted values left; do not modify the css.
                $key = array_shift($values_array_copy);
                $fonts_with_no_replacements[$key] = implode(',', $values_array_copy);
                continue;
            }
        }
        $extra = '';
        if (isset($values_array[0])) {
            $extra = $values_array[0] . ' ';
            unset($values_array[0]);
        }
        // Rezero the keys.
        $values_array = array_values($values_array);
        // Save next value.
        $next_value_original = trim($values_array[0]);
        // Create the values string.
        $new_values_string = $extra . implode(',', $values_array);
        // Get all selectors.
        $end_of_selectors = strpos($css_string, '{', $start);
        $selectors = substr($css_string, $start, $end_of_selectors - $start);
        // Ensure selectors is not a media query.
        if (stripos($selectors, "@media") !== FALSE) {
            // Move the start to the end of the media query.
            $start = $end_of_selectors + 1;
            // Get the selectors again.
            $end_of_selectors = strpos($css_string, '{', $start);
            $selectors = substr($css_string, $start, $end_of_selectors - $start);
        }
        // From advagg_load_stylesheet_content().
        // Perform some safe CSS optimizations.
        // Regexp to match comment blocks.
        // Regexp to match double quoted strings.
        // Regexp to match single quoted strings.
        $comment = '/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*/';
        $double_quot = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"';
        $single_quot = "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'";
        // Strip all comment blocks, but keep double/single quoted strings.
        $selectors_stripped = preg_replace("<({$double_quot}|{$single_quot})|{$comment}>Ss", "\$1", $selectors);
        // Add css class to all the selectors.
        $selectors_array = explode(',', $selectors_stripped);
        foreach ($selectors_array as &$selector) {
            // Remove extra whitespace.
            $selector = trim($selector);
            $selector = " .{$removed_value_classname} {$selector}";
        }
        $new_selectors = implode(',', $selectors_array);
        // Get full rule set.
        $full_rule_set = substr($css_string, $start, $end - $start);
        // Replace values.
        $new_values_full_rule_set = str_replace($values_string, $new_values_string, $full_rule_set);
        // Add in old rule set with new selectors.
        $new_selectors_full_rule_set = $new_selectors . '{' . $property_in_loop . ': ' . $values_string . ';}';
        // Record info.
        $replacements[] = array(
            $full_rule_set,
            $new_values_full_rule_set,
            $new_selectors_full_rule_set,
            $removed_value_original,
            $removed_value_classname,
            $next_value_original,
        );
    }
    return array(
        $replacements,
        $fonts_with_no_replacements,
    );
}

Functions

Title Deprecated Summary
advagg_css_alter Implements hook_css_alter().
advagg_font_advagg_current_hooks_hash_array_alter Implements hook_advagg_current_hooks_hash_array_alter().
advagg_font_get_replacements_array Get the replacements array for the css.
advagg_font_libraries_info Implements hook_libraries_info().
advagg_font_menu Implements hook_menu().
advagg_font_module_implements_alter Implements hook_module_implements_alter().
advagg_font_page_alter Implements hook_page_alter().

Constants

Title Deprecated Summary
ADVAGG_FONT_ADD_TO_CRITICAL_CSS Default value to include font info in critical css.
ADVAGG_FONT_COOKIE Default value to use a cookie in order to prevent the FOUT.
ADVAGG_FONT_FONTFACEOBSERVER Default value to use font face observer for asynchronous font loading.
ADVAGG_FONT_NO_FOUT Default value to only replace the font if it's been downloaded.
ADVAGG_FONT_STORAGE Default value to use localStorage in order to prevent the FOUT.