20

How to Use Custom Fonts in Oxygen Builder (and Other Platforms)

Powered by WPCodeBox

Effortlessly integrate and select custom fonts in Oxygen Builder and other platforms with this powerful snippet.


<?php 
/*
Plugin Name:  MA Oxygen Custom Fonts
Description:  Load custom fonts and inject to Oxygen
Author:       <a href="https://www.altmann.de/">Matthias Altmann</a>
Project:      Code Snippet: Load custom fonts and inject to Oxygen
Version:      3.2.4
Plugin URI:   https://www.altmann.de/en/blog-en/code-snippet-integrate-custom-fonts-into-oxygen-en/
Description:  en: https://www.altmann.de/en/blog-en/code-snippet-integrate-custom-fonts-into-oxygen-en/
              de: https://www.altmann.de/blog/code-snippet-eigene-schriftarten-in-oxygen-integrieren/
Copyright:    © 2020-2021, Matthias Altmann

Version History:
Date		Version		Description
--------------------------------------------------------------------------------------------------------------
2021-10-26	3.2.4		Changes:
						- Deferred initialization to hook wp_loaded, after incompatibility checks
						- Gutenberg: Apply custom fonts for new posts also
						- Renamed IDs (for scripts and styles) and CSS classes (for font test) for consistency
						Performance:
						- Avoid unneccessary init by separating admin/frontend and more detailed checks 
						Fixes:
						- Fixed an issue comparing hashes for code and file
						  (Thanks to David Sitniansky for reporting)
						- Emit styles for Gutenberg correctly if $cssoutput is configured as 'html'
2021-10-15	3.2.3		Changes:
						- Gutenberg: Use display font for post title
						  (Thanks to Sunny Trochaniak for reporting)
						- Fonts preview: Changed sample text size from 15 to 16 px which is browser standard 
						- Fonts preview: Shortcode output uses WP system fonts for UI instead of custom fonts
						- CSS file link now contains ?ver=... instead of ?hash only
						Performance: 
						- Only create CSS file if contents (font configuration) have changed 
						Fixes:
						- Removed itemprop="stylesheet" from <link rel="stylesheet" ...>
						  (Thanks to Max Gottschalk for reporting and testing)
						- Proper quoting for font families
2021-08-02	3.2.2		Changes:
						- Using scheme-less URL to avoid issues with wrong WordPress URL configuration
						- Added admin notice if folder wp-content/uploads/fonts is not writable.
						Fixes:
						- Fixed issue with uppercase font file extensions.
2021-06-18	3.2.1		Fixes: 
						- Fixed typo in CSS for Gutenberg
2021-06-18	3.2.0		New Features:
						- Display Custom Fonts in Gutenberg (enqueue ma_customfonts.css for font definitions, 
						  add custom style for display and text font from Oxygen global settings)
						Changes:
						- Auto-create folder /wp-content/uploads/fonts
2021-05-17	3.1.3		Changes:
						- Optimized init sequence
						- Emit implementation and version in CSS
						- Reversed Version History order
2021-05-16	3.1.2		Changes:
						- Avoid font swap: Load ma-customfonts.css early; default font-display now "block"
						New Features:
						- Allow space in addition to dashes to detect font weights and styles
2021-03-21	3.1.1		Fixes:
						- Fixed font loading in Gutenberg editor (with Oxygen Gutenberg Integration)
2021-03-20	3.1.0		New Features:
						- "Oblique" in font file name is now detected as italic style
						- Custom Fonts test: Option to show font weights/styles without files as browser would 
						  simulate. 
						Changes:
						- Output Custom Font CSS in head instead of footer to prevent font swap
						- Custom Fonts test: Changed logic for output font samples and related file info
						Fixes:
						- Custom Fonts test: Fixed font file count for fonts provided by Web Font Loader
2021-03-08	3.0.2		Fix:
						- Compatibility with Windows server and local dev environments.
						  (Thanks to Franz Müller for reporting and testing)
2021-02-23	3.0.1		Fixes:
						- Compatibility with WordPress 5.6.2 (doesn't set REQUEST::action anymore)
						- Compatibility check with Swiss Knife's Font Manager feature
						- Compatibility with Swiss Knife (font lists did not display custom fonts light blue)
2021-02-18	3.0.0		New Features:
						- Support for font packages from Web Font Loader (https://webfontloader.altmann.de/)
						- New configuration option: CSS output as inline CSS or external CSS file (cacheable)
						- New configuration option: CSS minimize (was controlled by debug switch before)
						- Changed configuration option: font-display may now be specified as desired, 
						  default is now 'auto'
2021-01-24 	2.5.2		New Features:
						- Custom Fonts test (via Admin panel and shortcode) now allows custom sample text
2021-01-23	2.5.1		Fix:
						- Changed compatibility check process: 
						  Changed Hook for plugin compatibility check from plugins_loaded to init
						  Check only if admin and function is_plugin_active exists
						  (Thanks to Sebastian Albert for reporting and testing)
2021-01-23	2.5.0		New features:
						- WP Admin Menu: Appearance > Custom Fonts 
						  Shows a list of all registered custom fonts, including samples, weights, formats
						  with adaptable sample font size 
						- Detect font weight terms "Book" (400) and "Demi" (600) 
						Changes:
						- Redesign of classes (MA_CustomFonts, ECF_Plugin)
						- Font swap is now a configuration option
						- Cut "-webfont" from font name
2020-12-08	2.2.5		Changes:
						- In CSS, font sources are now listed in a prioritized order (eot,woff2,woff,ttf,otf,svg)
						  (Thanks to Viorel Cosmin Miron for reporting)
						- Test shortcode now also displays available font formats
2020-11-27	2.2.4		Fix:
						- Corrected typo in variable name (2 occurrences) that could cause repeated search 
						  for font files. (Thanks to Viorel Cosmin Miron for reporting)
2020-11-25	2.2.3		Changes:
						- In Oxygen font selectors the custom fonts are now displayed in lightblue 
						  to distinguish from default, websafe and Google Fonts 
2020-11-25	2.2.2		New features:
						- Partial support for fonts with variable weights, detected by "VariableFont" in 
						  filename. CSS output as font-weight:100 900;
2020-11-24	2.2.1		New features:
						- Shortcode [ maltmann_custom_font_test ] for listing all custom fonts with their weights 
						  and styles
						Changes:
						- Fonts are now sorted alphabetically for e.g. CSS output
						- Added more request rules to skipping code execution when not needed
2020-11-23	2.2.0		New features:
						- Detection of font weight from number values 
						- CSS now contains font-display:swap;
2020-10-03 	2.1.1		Fix:
						- Handle empty fonts folder correctly. 
						  (Thanks to Mario Peischl for reporting)
						- Corrected title and file name (typo "cutsom") of Code Snippet
2020-09-16	2.1.0		New features:
						- Detection of font weight and style from file name
						Fixes:
						- EOT: Typo in extension detection
						- EOT: Missing quote in style output
2020-09-15	2.0.0		Improved version
						- Finds all font files (eot, otf, svg, ttf, woff, woff2) in directory wp-content/uploads/fonts/
						- Optionally recursive
						- Takes font name from file name
						- Emits optimized CSS with alternative font formats
						- Special handling for EOT for Internet Explorer
2020-04-10	1.0.0		Initial Release for customer project
--------------------------------------------------------------------------------------------------------------
*/



if (!class_exists('MA_CustomFonts')) :
class MA_CustomFonts {

	const TITLE     	= 'MA Custom Fonts';
	const VERSION   	= '3.2.4';

	// ===== CONFIGURATION =====
	public static $recursive 		= true; 	// enables recursive file scan
	public static $parsename 		= true; 	// enables parsing font weight and style from file name
	public static $fontdisplay		= 'block';	// set font-display to auto, block, swap, fallback, optional or '' (disable)
	public static $cssoutput		= 'file';	// set to 'html' to output CSS inline into page, 
												// set to 'file' to create and reference a CSS file (cacheable by browser)
	public static $cssminimize		= true; 	// minimize CSS (true) or pretty print (false)

	public static $timing			= false; 	// write timing info (a lot!) to wordpress debug.log if WP_DEBUG enabled		
	public static $debug			= false; 	// write debug info (a lot!) to wordpress debug.log if WP_DEBUG enabled	
	public static $sample_text 		= 'The quick brown fox jumps over the lazy dog.';	

	// ===== INTERNAL =====
	public 	static $prioritized_formats	= ['eot','woff2','woff','ttf','otf','svg'];
	private static $fonts 				= null;	// will be populated with fonts and related files we found
	private static $fonts_details_cache	= [];	// cache for already parsed font details
	private static $font_files_cnt		= 0;	// number of font files parsed
	private static $font_css			= null;	// temp storage for custom font css
	private static $guten_oxy_font_css	= null;	// temp storage for Gutenberg css defining oxygen fonts
	
	//-------------------------------------------------------------------------------------------------------------------
	static function init() {

		if (!defined('MA_CustomFonts_Version')) define('MA_CustomFonts_Version',self::VERSION);

		// Pre-fill font definitions
		self::get_font_families();
		// Pre-fill custom font css, and optionally write file
		self::$font_css = self::get_font_css();

		// Frontend: Emit custom font css in head 
		add_action( 'wp_head', function(){ 
			echo self::$font_css; 
		},5);

		// Backend
		if (is_admin()) {
			// Load CSS for calls using Gutenberg Editor. 
			// Requires ma_customfont.css (for font loading) and some Oxygen settings (for font assignment)
			global $pagenow;
			if ( ($pagenow === 'post-new.php') || ($pagenow === 'post.php' && @$_REQUEST['action']==='edit') ) {
				// set up fonts dir and url
				$fonts_base = self::get_fonts_base(); 
				if (!$fonts_base) 	{return false;}

				// enqueue or embed ma_customfonts.css
				if (self::$cssoutput == 'file') {
					wp_enqueue_style('ma-customfonts', $fonts_base->url.'/ma_customfonts.css'); 
				} else {
					$plain_css = self::get_font_css(true); 
					add_action( 'admin_head', function(){ 
						echo self::get_font_css(); 
					},5);
				}

				if (defined("CT_VERSION")) { // Oxygen installed and active?
					// create custom style for body, h1-h6 from Oxygen global settings
					$ct_global_settings = ct_get_global_settings();
					self::$guten_oxy_font_css = sprintf(
						'body .editor-styles-wrapper {font-family:%1$s;}'.
						'body .editor-styles-wrapper :is(h1,h2,h3,h4,h5,h6,.editor-post-title) {font-family:%2$s;}',
						self::quote_font_names($ct_global_settings['fonts']['Text']), 	// text font
						self::quote_font_names($ct_global_settings['fonts']['Display'])	// heading font
					);
					// add custom style to overwrite Gutenberg's default font
					if (self::$cssoutput == 'file') {
						wp_add_inline_style('ma-customfonts',self::$guten_oxy_font_css); 
					} else {
						add_action( 'admin_head', function(){ 
							echo '<style id="ma-customfonts-oxygen">'.self::$guten_oxy_font_css.'</style>'; 
						},5);
					}
				}
			}
		}
		
		// Shortcode for testing custom fonts (listing all fonts with their formats, weights, styles)
		add_shortcode('maltmann_custom_font_test', function( $atts, $content, $shortcode_tag ) {
			return self::get_font_samples('shortcode');
		}); 

		
	}
	//-------------------------------------------------------------------------------------------------------------------
	static function init_admin_menu() {
		$st = microtime(true);
		// Add submenu page to the Appearance menu.
		add_action('admin_menu', function(){
			add_submenu_page(	'themes.php', 										// parent slug of "Appearance"
								_x('Custom Fonts','page title','ma_customfonts'), 	// page title
								_x('Custom Fonts','menu title','ma_customfonts'), 	// menu title
								'manage_options',									// capabilitiy
								'ma_customfonts',									// menu slug
								[__CLASS__, 'admin_customfonts']					// function
							);
		});
		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s::%s() Timing: %.5f sec.',__CLASS__,__FUNCTION__,$et-$st));}
	}
	//-------------------------------------------------------------------------------------------------------------------
	// Proper quoting for font families: Detect multiple families and quote individually if required
	static function quote_font_names($font_spec) {
		$fonts = preg_split('/,s*/', $font_spec, -1, PREG_SPLIT_NO_EMPTY);
		$fonts = array_map(function($name){
			return preg_match('/[^A-Za-z-]/',$name) ? '"'.$name.'"' : $name;
		},$fonts);
		$retval = implode(', ',$fonts);
		return $retval;
	}

	//-------------------------------------------------------------------------------------------------------------------
	static function get_script_version() {
		$implementation = basename(__FILE__) == 'ma-oxygen-custom-fonts.php' ? 'Plugin' : 'Code Snippet';
		return sprintf('%s, %s', $implementation, self::VERSION);
	}
	//-------------------------------------------------------------------------------------------------------------------
	// Admin function Appearance > Custom Fonts to display samples of all detected fonts
	static function admin_customfonts() {
		$output =	'<div class="wrap">'.
						'<h1>' . esc_html(get_admin_page_title()) . '</h1>'.
						self::get_font_samples('admin').
					'</div>';
		echo $output;
		echo self::get_font_css();
	}
	//-------------------------------------------------------------------------------------------------------------------
	// parses weight from a font file name (not used for Web Font Loader packages)
	static function parse_font_name($name) {
		// already in cache?
		if (array_key_exists($name,self::$fonts_details_cache)) {return self::$fonts_details_cache[$name];}
		
		$retval = (object)['name'=>$name, 'weight'=>400, 'style'=>'normal'];
		if (!self::$parsename) {return $retval;}
		$st = microtime(true);
		if (WP_DEBUG && self::$debug) {error_log(sprintf('%s::%s() parsing font file name: "%s"',__CLASS__,__FUNCTION__, $retval->name));}
		$weights = (object)[ // must match from more to less specific !!
			// more specific
			200 => '/[ -]?(200|((extra|ultra)-?light))/i',
			800 => '/[ -]?(800|((extra|ultra)-?bold))/i',
			600 => '/[ -]?(600|([ds]emi(-?bold)?))/i',
			// less specific
			100 => '/[ -]?(100|thin)/i',
			300 => '/[ -]?(300|light)/i',
			400 => '/[ -]?(400|normal|regular|book)/i',
			500 => '/[ -]?(500|medium)/i',
			700 => '/[ -]?(700|bold)/i',
			900 => '/[ -]?(900|black|heavy)/i',
			'var' => '/[ -]?(VariableFont)/i',
		];
		$count = 0;
		// detect & cut style
		$new_name = preg_replace('/[ -]?(italic|oblique)/i', '', $retval->name, -1, $count); 
		if ($new_name && $count) {
			$retval->name = $new_name;
			$retval->style = 'italic';
			if (WP_DEBUG && self::$debug) {error_log(sprintf('%s::%s() detected italic, new font family name: "%s"',__CLASS__,__FUNCTION__, $retval->name));}
		}
		// detect & cut weight
		foreach ($weights as $weight => $pattern) {
			$new_name = preg_replace($pattern, '', $retval->name, -1, $count);
			if ($new_name && $count) {
				$retval->name = $new_name;
				$retval->weight = $weight;
				if (WP_DEBUG && self::$debug) {error_log(sprintf('%s::%s() detected weight %s, new font family name: "%s"',__CLASS__,__FUNCTION__, $retval->weight, $retval->name));}
				break;
			}
		}
		// cut -webfont
		$retval->name = preg_replace('/[ -]?webfont$/i', '', $retval->name); 
		// variable font: detect & cut specifica
		if ($retval->weight == 'var') {
			$retval->name = preg_replace('/_(opsz,wght|opsz|wght)$/i', '', $retval->name); 
		}
		if (WP_DEBUG && self::$debug) {error_log(sprintf('%s::%s() retval: [name:"%s", weigh:%d, style:%s]',__CLASS__,__FUNCTION__, $retval->name, $retval->weight, $retval->style));}
		// store to cache
		self::$fonts_details_cache[$name] = $retval;
		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf(' %s::%s() Timing: %.5f sec.',__CLASS__,__FUNCTION__,$et-$st));}
		return $retval;
	}
	//-------------------------------------------------------------------------------------------------------------------
	// construct CSS block from CSS properties stored in JSON from Web Font Loader
	static 	function create_css_from_ruleset($css_ruleset) {
		$retval = '';
		if (isset($css_ruleset)) {
			if (isset($css_ruleset->{'comment'})) {$retval .= sprintf("/* %s */n",$css_ruleset->{'comment'});}
			$retval .= "@font-face {n";
			$retval .= sprintf("tfont-family: '%s';n",$css_ruleset->{'font-family'});
			$retval .= sprintf("tfont-style: %s;n",$css_ruleset->{'font-style'});
			$retval .= sprintf("tfont-weight: %s;n",$css_ruleset->{'font-weight'});
			$retval .= sprintf("tsrc: url('%s') format('%s');n",$css_ruleset->{'url'}, $css_ruleset->{'format'});
			if (isset($css_ruleset->{'unicode-range'})) {$retval .= sprintf("tunicode-range: %s;n", $css_ruleset->{'unicode-range'});}
			if (self::$fontdisplay) {
				$retval .= sprintf("tfont-display: %s;n",self::$fontdisplay);
			}			
			$retval .= '}';
		}
		return $retval;
	}
	//-------------------------------------------------------------------------------------------------------------------
	// return base dir/url for fonts. Create directory if necessary
	private static function get_fonts_base() {
		$retval = (object)['dir'=>null,'url'=>''];
		$fonts_dir_info = wp_get_upload_dir();
		$retval->dir = $fonts_dir_info['basedir'].'/fonts';
		$retval->url = $fonts_dir_info['baseurl'].'/fonts';
		// create fonts folder if not exists
		if (!file_exists($retval->dir)) {
			if (!@mkdir($retval->dir)) {
				add_action('admin_notices', function(){
					echo '<div class="notice notice-error"><p>['.self::TITLE.'] Error creating fonts base folder <code>wp-content/uploads/fonts</code>.</p></div>';
				});
				error_log(sprintf('%s::%s() Error creating fonts base folder.', __CLASS__, __FUNCTION__)); 
				return null;
			}
		}
		if (!is_writable($retval->dir)) {
			add_action('admin_notices', function(){
				echo '<div class="notice notice-error"><p>['.self::TITLE.'] Folder <code>wp-content/uploads/fonts</code> is not writable. Please correct folder permissions.</p></div>';
			});
		}
		// V3.2.2, create scheme-less URL
		$retval->url = preg_replace('/^https?:/','',$retval->url);

		return $retval;
	}
	//-------------------------------------------------------------------------------------------------------------------
	// find font files in font folder
	static function find_fonts() {
		$st = microtime(true);
		if (isset(self::$fonts)) return;
		self::$fonts = [];
		// set up fonts dir and url
		$fonts_base = self::get_fonts_base(); 
		if (!$fonts_base) 	{return false;}
		// property $recursive either recursive or flat file scan
		if (self::$recursive) {
			// recursive scan for font files (including subdirectories)
			$directory_iterator = new RecursiveDirectoryIterator($fonts_base->dir,  RecursiveDirectoryIterator::SKIP_DOTS | RecursiveDirectoryIterator::UNIX_PATHS);
			$file_iterator = new RecursiveIteratorIterator($directory_iterator);
		} else {
			// flat scan for font files (no subdirectories)
			$file_iterator = new FilesystemIterator($fonts_base->dir);
		}
		// loop through files and collect font and JSON files
		$font_splfiles = [];
		$json_splfiles = [];
		foreach( $file_iterator as $file) {
			// V3: A JSON file might be available from Web Font Loader
			if ($file->getExtension() == 'json') {
				$json_splfiles[] = $file;
			}
			if (in_array(strtolower($file->getExtension()), self::$prioritized_formats)) {
				$font_splfiles[] = $file;
			}
		}
		
		// V3: check JSON files. If it defines "family" read the font name and CSS
		$json_font_families = [];
		foreach ($json_splfiles as $json_splfile) {
			if ($font_details = @json_decode(@file_get_contents($json_splfile->getPathname()))) {
				// It's a JSON from Web Font Loader?
				if (isset($font_details->creator) && (strpos($font_details->creator, 'Web Font Loader')=== 0)) {
					// store font family name 
					$json_font_families[$json_splfile->getBasename('.json')] = $font_details->family;
					// drop all collected font files for that font since they are listed in JSON file
					$font_path = $json_splfile->getPath().'/';
					foreach ($font_splfiles as $idx => $font_splfile) {
						if (strpos($font_splfile->getPath().'/',$font_path) === 0) {
							self::$font_files_cnt ++;
							unset($font_splfiles[$idx]);
						}
					}
					$font_path = str_replace($fonts_base->dir,'',$font_path);
					// encode every single path element since we might have spaces or special chars 
					$font_path = implode('/',array_map('rawurlencode',explode('/',$font_path)));
					
					// add CSS blocks (could be multiple unicode ranges) to fonts list

					$font_baseurl = $fonts_base->url . $font_path;
					foreach ($font_details->css as $css_ruleset) {
						self::$fonts[$css_ruleset->{'font-family'}][$css_ruleset->{'font-weight'}.'/'.$css_ruleset->{'font-style'}]['has_css'] = true;
						// only formats woff and woff2, so just use format as file extension slot
						if (!isset(self::$fonts[$css_ruleset->{'font-family'}][$css_ruleset->{'font-weight'}.'/'.$css_ruleset->{'font-style'}][$css_ruleset->{'format'}])) {
							self::$fonts[$css_ruleset->{'font-family'}][$css_ruleset->{'font-weight'}.'/'.$css_ruleset->{'font-style'}][$css_ruleset->{'format'}] = [];	
						}
						$css_ruleset->url = $font_baseurl . $css_ruleset->url;

						$css_block = self::create_css_from_ruleset($css_ruleset);
						self::$fonts[$css_ruleset->{'font-family'}][$css_ruleset->{'font-weight'}.'/'.$css_ruleset->{'font-style'}][$css_ruleset->{'format'}][] = $css_block;
					}
				}
			}
		}
		// collect font definitions
		foreach ($font_splfiles as $font_splfile) {
			self::$font_files_cnt ++;
			$font_ext = $font_splfile->getExtension();
			$font_details = self::parse_font_name($font_splfile->getbasename('.'.$font_ext));
			$font_name = $font_details->name;
			if (in_array($font_name,array_values($json_font_families))) {
				// already found this font from Web Font Loader. Skip.
				continue;
			}
			$font_weight = $font_details->weight;
			$font_style = $font_details->style;
			$font_path = str_replace($fonts_base->dir,'',$font_splfile->getPath());
			// encode every single path element since we might have spaces or special chars 
			$font_path = implode('/',array_map('rawurlencode',explode('/',$font_path)));
			// create entry for this font name
			if (!array_key_exists($font_name,self::$fonts)) {self::$fonts[$font_name] = [];}
			// create entry for this font weight/style 
			if (!array_key_exists($font_weight.'/'.$font_style,self::$fonts[$font_name])) {self::$fonts[$font_name][$font_weight.'/'.$font_style] = [];}
			// store font details for this file
			self::$fonts[$font_name][$font_weight.'/'.$font_style][strtolower($font_ext)] = $fonts_base->url . $font_path . '/' . rawurlencode($font_splfile->getBasename());
		}
		ksort(self::$fonts, SORT_NATURAL | SORT_FLAG_CASE);
		if (WP_DEBUG && self::$debug) {error_log(sprintf('%s::%s() final fonts: %s]',__CLASS__,__FUNCTION__, print_r(self::$fonts,true)));}
		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s::%s() %d font files, %d font families.',__CLASS__,__FUNCTION__, self::$font_files_cnt, count(self::$fonts)));}
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s::%s() Timing: %.5f sec.',__CLASS__,__FUNCTION__,$et-$st));}
	}
	//-------------------------------------------------------------------------------------------------------------------
	// returns a list of font families
	static function get_font_families() {
		if (!isset(self::$fonts)) self::find_fonts();
		$st = microtime(true);
		$font_family_list = [];
		foreach (array_keys(self::$fonts) as $font_name) {
			$font_family_list[] = $font_name;
		}
		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s::%s() Timing: %.5f sec.',__CLASS__,__FUNCTION__,$et-$st));}
		return $font_family_list;
	}
	//-------------------------------------------------------------------------------------------------------------------
	// we call this function from footer emitter to get font definitions for emitting required files
	static function get_font_definitions() {
		return self::$fonts;
	}
	//-------------------------------------------------------------------------------------------------------------------
	// creates CSS for custom fonts. 
	// For $cssoutput 'file', return <link rel="stylesheet" ...>, for 'html' <style>...</style>
	// If $plaincss is true, return plain css instead
	static function get_font_css($plaincss = false) {
		// emit CSS for fonts in footer
		$version = self::get_script_version();
		$style = '';
		// set up fonts dir and url
		$fonts_base = self::get_fonts_base(); 
		if (!$fonts_base) 	{return false;}
		$st = microtime(true);
		foreach (self::$fonts as $font_name => $font_details) {
			ksort($font_details);
			foreach ($font_details as $weight_style => $file_list) {
				list ($font_weight,$font_style) = explode('/',$weight_style);

				if (isset($file_list['has_css'])) {
					// V3: Google Font package CSS from Web Font Loader already has CSS
					foreach (array_reverse(self::$prioritized_formats) as $font_ext) {
						// we only have woff and woff2
						if (!isset($file_list[$font_ext])) { continue; }
						foreach ($file_list[$font_ext] as $css) {
							$style .= trim($css).PHP_EOL;
						}
					}
				} else {
					// V2: Only have font info and file names. Build CSS
					if ($font_weight == 'var') {
						$font_weight_output = '100 900';
					} else {
						$font_weight_output = $font_weight;
					}
					$style .= 	'@font-face{'.PHP_EOL.
								'  font-family:"'.$font_name.'";'.PHP_EOL.
								'  font-weight:'.$font_weight_output.';'.PHP_EOL.
								'  font-style:'.$font_style.';'.PHP_EOL;
								// .eot needs special handling for IE9 Compat Mode
					if (array_key_exists('eot',$file_list)) {$style .= '  src:url("'.$file_list['eot'].'");'.PHP_EOL;}
					$urls = [];

					// output font sources in prioritized order
					foreach (self::$prioritized_formats as $font_ext) {
						if (array_key_exists($font_ext,$file_list)) {
							$font_url = $file_list[$font_ext];
							$format = '';
							switch ($font_ext) {
								case 'eot': $format = 'embedded-opentype'; break;
								case 'otf': $format = 'opentype'; break;
								case 'ttf': $format = 'truetype'; break;
								// others have same format as extension (svg, woff, woff2)
								default:	$format = strtolower($font_ext);
							}
							if ($font_ext == 'eot') {
								// IE6-IE8
								$urls[] = 'url("'.$font_url.'?#iefix") format("'.$format.'")';
							} else {
								$urls[] = 'url("'.$font_url.'") format("'.$format.'")';
							}
						}
					}
					$style .= '  src:' . join(','.PHP_EOL.'      ',$urls) . ';'.PHP_EOL;
					if (self::$fontdisplay) {
						$style .= sprintf('  font-display: %s;'.PHP_EOL,self::$fontdisplay);
					}
					$style .= '}'.PHP_EOL;
				}
			}
		}
		// if Oxygen Builder is active, emit CSS to show custom fonts in light blue.
		$builder_style = defined('SHOW_CT_BUILDER') ? 'div.oxygen-select-box-option.ng-binding.ng-scope[ng-repeat*="elegantCustomFonts"] {color:lightblue !important;}' : '';
		
		
		if (WP_DEBUG && self::$debug) {error_log(sprintf('%s::%s() style: %s]',__CLASS__,__FUNCTION__, $style));}
		// minimize string if configured
		if (self::$cssminimize) {
			$style = preg_replace('/r?n */','',$style); 
		}

		$retval = '';
		if (self::$cssoutput == 'file') {
			// option: write CSS to file
			$css_path = $fonts_base->dir.'/ma_customfonts.css';
			$css_code = '/* Version: '.$version.' */'.PHP_EOL.$style;
			$css_hash_code = hash('CRC32', $css_code, false);
			$css_hash_file = file_exists($css_path) ? hash_file('CRC32', $css_path, false) : 0;
			if ($css_hash_code !== $css_hash_file) {
				if (WP_DEBUG && (self::$timing || self::$debug)) {error_log(sprintf('%s::%s() Writing CSS file "%s"',__CLASS__,__FUNCTION__,$css_path));}
				$status = file_put_contents($css_path, $css_code);
				if ($status === false) {error_log(sprintf('%s::%s() Error writing CSS file "%s"',__CLASS__,__FUNCTION__,$css_path));}
				$css_hash_file = file_exists($css_path) ? hash_file('CRC32', $css_path, false) : 0;
			}
			$css_url = str_replace($fonts_base->dir,$fonts_base->url ,$css_path);
			$retval = sprintf('<link id="ma-customfonts" href="%s?ver=%s" rel="stylesheet" type="text/css" />%s',$css_url, $css_hash_file, $builder_style?'<style>'.$builder_style.'</style>':'');
		}
		if (self::$cssoutput == 'html') {
			// option: write CSS to html
			$retval = '<style id="ma-customfonts">'.'/* Version: '.$version.' */'.PHP_EOL.$style.PHP_EOL.$builder_style.'</style>';
		}
		if ($plaincss) {
			$retval = $style.PHP_EOL.$builder_style;
		}

		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s::%s() Timing: %.5f sec.',__CLASS__,__FUNCTION__,$et-$st));}
		return $retval;
	}
	//-------------------------------------------------------------------------------------------------------------------
	// parses font file url from CSS block
	static function get_font_file_info_from_css($css) {
		$retval = [];
		if (!is_array($css)) {$css = [$css];}
		foreach($css as $css_block) {
			if (preg_match('/url('(.*?)')/',$css_block,$matches)) {
				$retval[] = $matches[1];
			}
		}
		$retval = array_unique($retval); 
		return $retval;
	}
	//-------------------------------------------------------------------------------------------------------------------
	// returns HTML code to display all registered custom fonts
	// $mode 'admin':		formatting to be displayed on WP Admin > Appearance
	// $mode 'shortcode':	formatting to be displayed as shortcode output
	static function get_font_samples($mode = null) {
		$st = microtime(true);
		$output = '';
		$script_version = self::get_script_version();
		$output = 	'<style>'.
					'.ma-customfonts-test {font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;}'.
					'#ma-customfonts-input-font-size {width:60px;text-align:center;min-height:1em;line-height:1em;padding:0;}'.
					'#ma-customfonts-input-sample-text {width:400px;text-align:left;}'.
					'.ma-customfonts-label {display:inline-block;width:150px;line-height:2em;}'.
					'.ma-customfonts-font-row {display:flex;flex-direction:row;justify-content:space-between;align-items:center;padding:0;line-height:20px;border-bottom:1px solid #e0e0e0;margin:0 1em;}'.
					'.ma-customfonts-font-row:hover {background-color:lightgray;}'.
					'.ma-customfonts-font-info {font-size:10px;line-height:1em;width:100px;}'.
					'.ma-customfonts-font-sample {font-size:16px;line-height:1em;flex-grow:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;}'.
					'.ma-customfonts-format-info {font-size:10px;cursor:help;margin-left:1em;}'.
					'.ma-customfonts-simulated {display: none;}'.
					'</style>'.
					'<div '.($mode=='shortcode'?'class="ma-customfonts-test" style="display:inline-block;border:1px dashed darkgray;padding:10px;"':'').'>'.
						($mode=='shortcode'?'<h2>MA Custom Fonts</h2>':'').
						'<div style="display:inline-block;border:1px solid darkgray;border-radius:10px;padding:10px;">'.
							'<span class="ma-customfonts-label">Version:</span> '.$script_version.'<br/>'.
							'<span class="ma-customfonts-label">Font Families:</span> '.count(self::$fonts).'<br/>'.
							'<span class="ma-customfonts-label">Font Files:</span> '.self::$font_files_cnt.'<br/>'.
							'<span class="ma-customfonts-label">Sample Font Size:</span> '.
								'<input id="ma-customfonts-input-font-size" type="number" value="16" onchange="ma_customfonts_change_font_size();"> px<br/>'.
							'<span class="ma-customfonts-label">Sample Text:</span> '.
								'<input id="ma-customfonts-input-sample-text" value="'.self::$sample_text.'" onkeyup="ma_customfonts_change_sample_text();"><br/>'.
							'<span class="ma-customfonts-label">Simulated:</span> '.
								'<input id="ma-customfonts-input-simulated" type="checkbox" value="simulated" onchange="ma_customfonts_toggle_simulated();"> Show font weights/styles without files as browser would simulate.<br/>'.
						'</div>';

		// controls
		$controls_script = <<<'END_OF_CONTROLS_SCRIPT'
		<script>
		function changeCss(className, classValue) {
			// we need invisible container to store additional css definitions
			var cssMainContainer = jQuery('#css-modifier-container');
			if (cssMainContainer.length == 0) {
				cssMainContainer = jQuery('<div id="css-modifier-container"></div>');
				cssMainContainer.hide().appendTo(jQuery('body'));
			}
			// we need one div for each class
			var classContainer = cssMainContainer.find('div[data-class="' + className + '"]');
			if (classContainer.length == 0) {
				classContainer = jQuery('<div data-class="' + className + '"></div>');
				classContainer.appendTo(cssMainContainer);
			}
			// append additional style
			classContainer.html('<style>' + className + ' {' + classValue + '}</style>');
		}
		function ma_customfonts_change_font_size() {
			var $val = jQuery('#ma-customfonts-input-font-size').val();
			changeCss('.ma-customfonts-font-sample','font-size: '+$val+'px;');
		}
		function ma_customfonts_change_sample_text() {
			var $val = jQuery('#ma-customfonts-input-sample-text').val();
			jQuery('.ma-customfonts-font-sample').text($val);
		}
		function ma_customfonts_toggle_simulated() {
			var $simulated = jQuery('#ma-customfonts-input-simulated').is(':checked');
			jQuery('.ma-customfonts-simulated').css('display',$simulated?'flex':'none');
		}
		</script>
END_OF_CONTROLS_SCRIPT;
		$output .=  $controls_script;
		// prepare tags for every weight/style combination
		$weights = [100,200,300,400,500,600,700,800,900];
		$styles = ['normal','italic'];
		$weights_styles = [];
		foreach ($weights as $weight) { foreach ($styles as $style) { $weights_styles[] = $weight.'/'.$style; } }
		// display fonts in each weight/style combination
		$sample_text = self::$sample_text;

		// build output
		foreach (self::$fonts as $font_name => $font_details) {
			$output .= sprintf('<h3 style="padding-top: 20px;">%1$s</h3>',$font_name);
			ksort($font_details);
			foreach ($weights_styles as $weight_style) {
				list ($weight,$style) = explode('/',$weight_style);;
				$font_file_info = '';
				$font_file_list = [];
				if (isset($font_details[$weight_style])) {
					// walk through possible file formats
					foreach (self::$prioritized_formats as $font_ext) {
						// details available for this specific file format?
						if (isset($font_details[$weight_style][$font_ext])) {
							// details content type
							if (isset($font_details[$weight_style]['has_css'])) {	
								// CSS
								$font_file_list[$font_ext] = self::get_font_file_info_from_css($font_details[$weight_style][$font_ext]);
							} else {
								// just file name
								$font_file_list[$font_ext] = [$font_details[$weight_style][$font_ext]];
							}
						}
					}
					// build font file info output
					foreach ($font_file_list as $format => $files) {
						// cut leading path/url from file info
						$files = str_replace(wp_get_upload_dir(),'',$files);
						// decode html entities (e.g. %20) in file path
						foreach ($files as &$file) {$file = implode('/',array_map('rawurldecode',explode('/',$file)));;}
						// convert array to html
						$font_file_list[$format] = sprintf('<span title="%2$s">%1$s</span>', strtoupper($format), implode("n",$files));
					}
					$font_file_info = '<span class="ma-customfonts-format-info">(' . implode(', ',array_values($font_file_list)) . ')</span>';
				}

				$output .= sprintf(	'<div class="ma-customfonts-font-row '.($font_file_info?'':'ma-customfonts-simulated').'">'.
										'<span class="ma-customfonts-font-info">%2$s %3$s</span>'.
										'<span class="ma-customfonts-font-sample" style="font-family:'%1$s';font-weight:%2$d;font-style:%3$s">%4$s</span>%5$s'.
									'</div>',$font_name, $weight, $style, $sample_text, $font_file_info?$font_file_info:'<span class="ma-customfonts-format-info"><em>(simulated)</em></span>');

			}
		}
		$output .= '</div>';

		$et = microtime(true);
		if (WP_DEBUG && self::$timing) {error_log(sprintf('%s::%s() Timing: %.5f sec.',__CLASS__,__FUNCTION__,$et-$st));}
		return $output;

	}
} // end of class MA_CustomFonts


endif; // end of conditional implementations


//===================================================================================================================
// Warn about incompatibilities
add_action('wp_loaded',function(){
	$GLOBALS['ma_customfonts_incompatibilities'] = [];
	if (is_admin()) {
		// Plugin "Elegant Custom Fonts"
		if (function_exists('is_plugin_active') && is_plugin_active('elegant-custom-fonts/elegant-custom-fonts.php'))
			{$GLOBALS['ma_customfonts_incompatibilities'][] = '"Elegant Custom Fonts"';}
		// Plugin "Use Any Font"
		if (function_exists('is_plugin_active') && is_plugin_active('use-any-font/use-any-font.php'))
			{$GLOBALS['ma_customfonts_incompatibilities'][] = '"Use Any Font"';}
		// Plugin "Swiss Knife" with feature "Font Manager" active
		if (function_exists('is_plugin_active') && is_plugin_active('swiss-knife/swiss-knife.php') && (get_option('swiss_font_manager')=='yes')) 														
			{$GLOBALS['ma_customfonts_incompatibilities'][] = '"Swiss Knife" with feature "Font Manager" enabled';}
		if (count($GLOBALS['ma_customfonts_incompatibilities'])) {
			add_action('admin_notices', function(){
				if (WP_DEBUG ) {error_log('MA_CustomFonts / Incompatibilities: '.print_r($GLOBALS['ma_customfonts_incompatibilities'],true));}
				echo '<div class="notice notice-warning is-dismissible">
						<p>The Code Snippet "Oxygen: Custom Fonts" is not compatible with the Plugin '.implode(' or ',$GLOBALS['ma_customfonts_incompatibilities']).'.<br/>
						Please deactivate either the Code Snippet or the Plugin (feature).</p>
					</div>';
			});
		}
	}
	if (count($GLOBALS['ma_customfonts_incompatibilities'])) return;
	

	//-------------------------------------------------------------------------------------------------------------------
	// create a primitive ECF_Plugin class if plugin "Elegant Custom Fonts" is not installed
	if (!count($GLOBALS['ma_customfonts_incompatibilities']) && !class_exists('ECF_Plugin')) {
		class ECF_Plugin {
			static function get_font_families() {
				$st = microtime(true);
				$font_family_list = MA_CustomFonts::get_font_families();
				$et = microtime(true);
				if (WP_DEBUG && MA_CustomFonts::$timing) {error_log(sprintf('MA_CustomFonts/%s::%s() Timing: %.5f sec.',__CLASS__,__FUNCTION__,$et-$st));}
				return $font_family_list;
			}
		}
	}
	

},1000); // hook late to check other plugins!


//===================================================================================================================
// Initialize
add_action('wp_loaded',function(){
	if (count($GLOBALS['ma_customfonts_incompatibilities'])) return;
	if (wp_doing_ajax()) return; 		// don't run for AJAX requests
	if (wp_doing_cron()) return; 		// don't run for CRON requests
	if (wp_is_json_request()) return; 	// don't run for JSON requests
	if (is_favicon()) return; 			// don't run for favicon request
	if (WP_DEBUG && (MA_CustomFonts::$debug || MA_CustomFonts::$timing)) {error_log(sprintf('MA_CustomFonts: Request URI="%s" action="%s"', @$_SERVER['REQUEST_URI'], @$_REQUEST['action']));}

	if (is_admin()) {
		// initialize admin menu Appearance > Custom Fonts
		MA_CustomFonts::init_admin_menu();
		// init custom font functionality only when needed
		global $pagenow;
		if ( 	($pagenow === 'themes.php' && @$_REQUEST['page'] === 'ma_customfonts') 	// custom font test 
			||	($pagenow === 'post-new.php')											// new post
			||	($pagenow === 'post.php' && @$_REQUEST['action'] === 'edit')			// edit post
		) {
			MA_CustomFonts::init();
		}
	} else {
		// frontend
		MA_CustomFonts::init();
	}
},1001); // hook after incompatibility check

Other Snippets

WPCodeBox is a WordPress Code Snippets Manager that allows you to share your WordPress Code Snippets across your sites.