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. |