Blog

Code Snippet: Eigene Schriftarten in Gutenberg, Bricks, Oxygen integrieren

Version: 3.4.2 (15.03.2024)

Einführung

Die Builder Bricks und Oxygen erlauben die Verwendung von Google Schriften.
Auch die Einbindung von eigenen Schriftarten ist möglich, entweder direkt oder mittels Zusatz-Plugins.

Gesetzliche Einschränkungen für Google Schriften

In einigen Ländern ist die direkte Einbindung von Google Schriften aufgrund von Datenschutz-Bestimmungen (GDPR, DSGVO, ...) problematisch.
Stattdessen müssen Google Schriften auf dem eigenen Server vorgehalten werden.

Umständliche Einbindung eigener Schriften

Die meisten bestehenden Lösungen für die Einbindung eigener Schriften erfordern den Upload der einzelnen Schriftdateien mit jeweiliger Angabe von Schrift-Familie, -Stil, und -Stärke. Je nach Anzahl und Varianten der Schriftdateien ist das ein zeitaufwändiger und umständlicher Prozess.

Lösung

Dieses Snippet vereinfacht die Bereitstellung und Einbindung von eigenen Schriften:
Die Schriftdateien können einfach in einen Ordner auf dem eigenen Server hochgeladen werden. Das Snippet erkennt anhand der Dateinamen automatisch Schrift-Familie, -Stil und -Stärke. Auch Schriften mit variabler Stärke werden unterstützt.

Für die Einbindung lokal bereitgestellter Google Schriften habe ich den Web Font Loader entwickelt:
Über diesen Dienst können Google Schriften ganz einfach in modernen Formaten heruntergeladen und in den Ordner auf dem eigenen Server hochgeladen werden.
Das Snippet erkennt diese Schriften und bindet sie automatisch ein, inklusive Unterstützung für variable Schriftstärken und Unicode Ranges, soweit verfügbar.

Seit Version 3.4.0 des Snippets stehen die Schriften auch im Gutenberg Editor zur Verfügung.

Anleitung

  1. Installiere das Snippet.
    Du kannst entweder die JSON Datei herunterladen und in das Plugin Code Snippets oder Advanced Scripts importieren,
    oder den Source Code kopieren und selbst ein Snippet in Deinem anderen Snippet Plugin anlegen.
  2. Rufe eine beliebige Seite Deiner Website auf.
    Das Snippet erstellt auf Deinem Server automatisch ein neues Verzeichnis wp-content/uploads/fonts
  3. Nun kannst Du eigene Schriften und auch lokal gespeicherte Google Schriften einbinden.
    Folge dazu einfach der entsprechenden nachfolgenden Anleitung.

Google bietet über seinen Dienst Google Fonts aktuell deutlich über 1.000 Schriften an, die gemäß den Lizenzen der jeweiligen Ersteller kostenlos genutzt werden können. 

Die Schriften können in die eigene Website eingebunden werden – allerdings nur über eine direkte Verbindung zu den Google Servern. Das ist im einigen Ländern aus Datenschutz-Gründen (DSGVO, GDPR, ...) nicht so einfach zulässig. Zum Download bietet Google die Schriften nur im Format TTF an, das aufgrund der Dateigröße nicht für die Verwendung im Web geeignet ist.

Daher habe ich selbst einen Web-Dienst Web Font Loader entwickelt, der den Download der Google Schriften in den modernen und web-tauglichen Formaten WOFF2 und WOFF ermöglicht.

Web Font Loader

Suche Dir einfach die gewünschten Schriften aus und klicke den Download Button. 
Du erhältst eine ZIP Datei. Entpacke diese ZIP Datei. Du siehst nun einen Ordner mit dem Namen der Schrift:

Lade den gesamten Ordner in das Verzeichnis wp-content/uploads/fonts auf Deinem Server hoch.
Der Ordner enthält wichtige Dateien, die das Snippet zur korrekten Einbindung und Bereitstellung der Schriften benötigt.

Das Snippet wird alle verfügbaren Schrift-Familien, -Stile und -Stärken und auch die verfügbaren Zeichensätze automatisch erkennen. Die notwendigen Informationen werden aus der beiliegenden JSON Datei gelesen.


Neu ab Version 3.2.6:
Seit dem Jahr 2016 unterstützen alle wichtigen Browser das Schriftformat WOFF2.
Eine neue Option im Snippet verhindert, dass das alte Format WOFF dem Browser angeboten wird.
Dadurch wird vermieden, dass der Browser zusätzlich zu den WOFF2 Dateien ungewollt auch noch die WOFF Dateien lädt, wenn man auf der Seite Emoji oder Symbole verwendet.

Müssen ältere Browser, wie z.B. Internet Explorer oder Apple Geräte sehr alter Generationen, zwingend unterstützt werden, kann diese Option im Konfigurationsabschnitt des Snippets geändert werden:

public static $wfl_support_woff = true; // Support old style WOFF files from Web Font Loader for browsers before 2016

Diese Option betrifft nur Schriften, die über den Web Font Loader herunter geladen wurden.
Schriften aus anderer Quelle werden weiterhin in allen verfügbaren Formaten bereit gestellt.

Lade Deine Schriftdateien in das Verzeichnis wp-content/uploads/fonts auf Deinem Server hoch.
Bevorzugt sollten die Dateien moderne Formate wie WOFF2 und WOFF haben. 
Es werden aber auch ältere und proprietäre wie EOT und SVG unterstützt, und Formate, die eigentlich nicht für die Benutzung im Web gedacht sind, wie OTF und TTF. 

Du kannst die Dateien auch in Unterordnern organisieren, z.B. gruppiert nach Schrift-Familie.

Das Code Snippet findet alle Schriftdateien, stellt sie im Builder zur Auswahl zur Verfügung, und erzeugt automatisch den entsprechenden CSS Code.

Schriftdateien, -namen, -stärken und -stile

Diese Details werden anhand der Dateinamen ermittelt.

Das Snippet unterstützt die Schriftformate EOT, OTF, SVG, TTF, WOFF, WOFF2.

Als Schriftname wird der Dateiname der Schriftdatei verwendet.
Für die Schriftdatei "Matthias Altmann.woff2" wird also Im Builder der Schriftname "Matthias Altmann" angezeigt.

Enthält der Dateiname Schlüsselworte für Schriftstärken, werden diese erkannt und nicht in den Schriftnamen übernommen, sondern entsprechend im erzeugten CSS Code vermerkt.
Folgende Stärken und Stile werden aktuell erkannt:

SchriftstärkeSchlüsselworte
100Thin
200ExtraLight, UltraLight
300Light
400Normal, Regular, Book
500Medium
600Demi, DemiBold, SemiBold
700Bold
800ExtraBold, UltraBold
900Heavy

Die Groß-/Kleinschreibung spielt bei den Schlüsselwörtern keine Rolle, und es können Bindestriche zwischen den Schlüsselwortteilen enthalten sein. ExtraLight wird also ebenso erkannt wie extra-light.

Auch numerische Werte in den Dateinamen (100, 200, ...) werden als Schriftstärken erkannt.

Außerdem werden die Schlüsselwörter "italic" und "oblique" für den kursiven Schriftstil erkannt. Groß-/Kleinschreibung und Trennstrich davor sind auch hier irrelevant.

Existieren mehrere Schriftdateien mit dem gleichen Namen, aber unterschiedlichen Schriftformaten (eot, otf, svg, ttf, woff, woff2), wird im Builder die Schriftart nur ein mal angezeigt. Das ausgegebene CSS enthält jedoch alle Schriftformate, der Browser sucht sich das bevorzugte selbst aus.

Schriften mit variabler Stärke werden ebenfalls unterstützt: Werden im Dateinamen die Schlüsselworte VariableFont_wght oder [wght] (Groß-/Kleinschreibung egal) erkannt, wird das CSS automatisch für Schriften mit variabler Stärke angepasst.

Konfiguration

Im Snippet gibt es einige Variablen zur Konfiguration.
Die Variablen sind im Source Code im Abschnitt CONFIGURATION aufgeführt und beschrieben.

Test der Schriften

Im Snippet ist eine Test-Funktion implementiert, die eine Liste aller vom Snippet erkannten und registrierten Schriftarten, -stärken und -stilen ausgibt. Hier kann man die Einbindung und Darstellung der Schriften prüfen, auch in verschiedenen Schriftgrößen.

Test per Admin Funktion

In der WordPress Administration gibt es unter Design (bzw. Appearance) einen neuen Menüpunkt "MA Custom Fonts".
Hier erhält man einen schnellen Überblick über die durch das Snippet registrierten Schriften.

Test per Shortcode

Das Snippet stellt einen Shortcode [ma-customfonts-test] zur Verfügung.
Diesen Shortcode kann man z.B. auf einer neuen Seite im Gutenberg Editor eingeben. Mit dem Shortcode auf einer Seite kann man die Schriften einfach z.B. auf einem Smartphone testen.

Performance

Bei der Entwicklung des Skripts habe ich viel Wert auf Effizienz und Performance gelegt. Die Erkennung vorhandener Schriften und die Erzeugung des CSS Codes dauert auf einem durchschnittlichen Server nur Millisekunden.

Der CSS Code für die Schriftarten wird als CSS Datei verlinkt oder am Ende jeder Seite ausgegeben, egal, ob eine der Schriftarten verwendet wird oder nicht. Der Browser lädt eine Schriftart jedoch nur, wenn sie wirklich benötigt wird, und dann auch nur in dem einen bevorzugten Format.

Kompatibilität

Oxygen

Bisher sind mir einige WordPress Plugins bekannt, die auf ähnliche Weise benutzerdefinierte Schriften in Oxygen integrieren: "Elegant Custom Fonts", "Oxy Font Manager", "Swiss Knife", "Use Any Font".
Da sich die Art der Implementierung bzw. der technische Ansatz, Schriften in Oxygen zu integrieren, in diesen Plugins und meinem Code Snippet ähneln, sind diese Lösungen nicht kompatibel. Man muss sich für eine dieser Lösungen entscheiden. Also bitte vor Verwendung meines Code Snippets diese Plugins deaktivieren, bzw. bei "Swiss Knife" den "Font Manager" deaktivieren.

Oxygen bietet die Möglichkeit, Google Fonts direkt einzubetten.
Wenn Sie dieses Code-Snippet verwenden möchten, um Google Fonts lokal auf Ihrem eigenen Server zu hosten, empfehle ich, die Google Fonts-Unterstützung in Oxygen zu deaktivieren: Oxygen > Settings > Bloat Eliminator > Disable Google Fonts

Bricks

Zu Bricks sind mir keine Inkompatibilitäten bekannt.

Bricks bietet die Möglichkeit, Google Fonts direkt einzubetten.
Wenn Du dieses Code-Snippet verwenden möchtest, um Google Fonts lokal auf Ihrem eigenen Server zu hosten, empfehle ich, die Google Fonts-Unterstützung in Bricks zu deaktivieren: Bricks > Einstellungen > Performance > Google Fonts deaktivieren

FAQ - Häufig gestellte Fragen

Seit den 1990er Jahren gibt es das "True Type" Format (TTF), mit dem Schriften auf einem Computer installiert werden konnten.
1996 hat Microsoft das Format "Embedded Open Type" (EOT) entwickelt, um für den Internet Explorer komprimierte und damit kleinere Schriftdateien bereit zu stellen.
Ab den 2010er Jahren wurden dann weitere, immer optimiertere Schriftdatei-Formate für die Nutzung im Web entwickelt, nämlich OTF, SVG, WOFF und WOFF2.

WOFF2 ist das derzeit fortschrittlichste Format und wird von allen modernen Browsern unterstützt.
Nur noch wenige ältere Geräte, wie beispielsweise iPhones und iPads der ganz frühen Generationen können damit nichts anfangen und benötigen WOFF.

Nachfolgende Tabelle zeigt anhand der Schriftart "Dinot" die Datei-Größen der einzelnen Datei-Formate:

FormatDatei-Größe
TTF52 KB
EOT52 KB
OTF44 KB
WOFF24 KB
WOFF216 KB

Da eine Schriftart aus mehreren Dateien für die verschiedenen Schriftstärken und Schriftstile bestehen kann, wird schnell klar, dass bei modernen Formaten wie WOFF und WOFF2 deutlich weniger Daten übertragen werden müssen.

Wenn man also eine eigene Schriftart nur im Format TTF vorliegen hat, empfiehlt sich die Konvertierung nach WOFF oder WOFF2.
Unter dem Suchbegriff "Font Converter" findet man im Internet hierfür zahlreiche kostenlose Online-Dienste.

Ja.
Für den Download von Google Fonts in modernen Web-Formaten habe ich einen eigenen Service Web Font Loader entwickelt, der perfekt auf die Verwendung mit diesem Snippet abgestimmt ist.

Zu beachten: (Oxygen)

Mein Code Snippet sucht nach Schriftdateien und fügt sie der Oxygen Schriftenliste hinzu.
Oxygen bietet jedoch im Standard bereits einige Google Fonts in der Schriftenliste an.

Wenn wir beispielsweise die Google Schrift "Roboto" herunter laden und lokal bereitstellen, wird sie mein Code Snippet der Schriftenliste hinzufügen.
Oxygen fügt selbst die Google Schrift "Roboto" hinzu. "Roboto" ist also nun zwei mal in der Liste enthalten: Einmal als lokale Schrift (in hellblau dargestellt) und einmal als Google Schrift (in weiß dargestellt).

Erschwerend kommt noch hinzu, dass in Oxygen auch mit der aktivierten Option "Disable Google Fonts" trotzdem 20 Google Schriften in der Schriftauswahl angezeigt werden, und mit der Suche nach Schriftarten Google Fonts angeboten werden. Dies ist dem Oxygen Team bekannt, es gibt aber aktuell noch keine Lösung hierfür.

Ich empfehle, in Oxygen die Unterstützung für Google Fonts abzuschalten: Oxygen > Settings > Bloat Eliminator > Disable Google Fonts

In den Oxygen Global Settings kann man für Google Fonts festlegen, in welchen Schriftstärken sie eingebettet werden sollen. Damit kann man einschränken, welche Schriftstärken vom Google Fonts Server geladen werden, und damit bei nur wenigen benutzten Schriftstärken die Dateigröße reduzieren.

Bei den eigenen Schriftarten, die wir über dieses Code Snippet einbinden, bestimmen wir selbst, welche Schriftdateien wir im Ordner fonts ablegen. Gewöhnlich gibt es von einer Schriftart für jede Schriftstärke eine eigene Schriftdatei. All diese von uns bereit gestellten Schriftdateien und damit die verschiedenen Schriftstärken werden dem Browser angeboten. Der Browser lädt jedoch nur die Schriftdateien mit den Schriftstärken, die wirklich zur Anzeige der Seite benötigt werden.

Es gibt also gar keinen Grund, für unsere eigenen Schriftarten die Schriftstärken in den Oxygen Global Settings einzuschränken.

In Einzelfällen werden kyrillische Zeichen in bestimmten Sprachen mit bestimmten Schriften nicht korrekt dargestellt. Die folgende CSS Regel behebt dieses Problem:

* {
  -webkit-font-feature-settings: "locl" 0;
  -moz-font-feature-settings:    "locl" 0;
  -ms-font-feature-settings:     "locl" 0;
  font-feature-settings:         "locl" 0;
  -webkit-locale: auto;
}

Download

Dieser JSON-Download kann direkt in die Plugins Code Snippets oder Advanced Scripts importiert werden. Vergiss nicht, das Snippet nach dem Import zu aktivieren.
Falls Du ein anderes Snippet Plugin verwendest, kannst Du stattdessen den Source Code kopieren und damit selbst ein neues Snippet anlegen.

ma-custom-fonts.code-snippets.json
Version 3.4.2, 2024-03-15

Spenden

Es macht mir viel Freude, Code Snippets zu entwickeln und damit Anforderungen zu lösen. Die Snippets stelle ich kostenfrei zur Verfügung.

Wenn Du möchtest, kannst Du meine vielen Stunden Arbeit mit einer kleinen Kaffee-Spende über PayPal honorieren.

  Bei Klick auf den Button wird eine Verbindung zu PayPal aufgebaut.

Spenden werden selbstverständlich ordnungsgemäß durch mich versteuert.

Disclaimer

Das Code Snippet habe ich nach bestem Wissen und Gewissen getestet. (Siehe den Abschnitt "TESTED WITH" im Source Code)
Ich stelle das Code Snippet zur freien Verwendung zur Verfügung.
Eine Garantie für die Funktionalität in allen denkbaren WordPress Umgebungen kann ich nicht geben.
Download und Nutzung dieses Code Snippets erfolgen auf eigene Gefahr und Verantwortung.

Change Log

Siehe Source Code.

Source Code

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

TESTED WITH:
Product		Versions
--------------------------------------------------------------------------------------------------------------
PHP 		7.4.3, 8.1, 8.2
WordPress	6.4.2, 6.4.3
Bricks		1.9.6.1, 1.9.7
Oxygen		4.8.1
--------------------------------------------------------------------------------------------------------------

VERSION HISTORY:
Date		Version		Description
--------------------------------------------------------------------------------------------------------------
2024-03-15	3.4.2		Fixes:
						- Fixed PHP warning in admin_init() line 818: $_REQUEST['page'] might not be defined 
						  when using Bricks.
2024-03-04	3.4.1		Changes:
						- Builder fonts in Gutenberg: Call of Oxygen function ct_get_global_settings embedded
						  in call_user_func() to avoid being marked as unknown in Bricks setups.
						Fixes:
						- Fixed PHP warning in admin_init(): Variable $builder_fonts_to_gutenberg definition 
						  was inside of Oxygen block, hence undefined in Bricks block.
2024-02-25	3.4.0		Reorganization of code base.
						New Features:
						- Support for assigning fonts in Gutenberg. 
						  Can be disabled by setting $gutenberg_font_family_select = false;
						- Oxygen Builder: Hide "Top 20" Google Fonts if Google Fonts are disabled.
						- Added filter for excluding ma-customfonts.css from FlyingPress CSS minimizer
						  which causes issues with already minimized CSS containing @font-face rules.
						Changes:
						- Changed CSS file name from ma_customfonts.css to ma-customfonts.css
						- Changed test page slug from ma_customfonts to ma-customfonts
						- Removed setting $recursive for font file scan. Always scan recursive.
						- Added compatibility check for "Oxy Font Manager"
						- Oxygen Builder font lists now show "Custom" badge for injected fonts
						- Custom Fonts Test now loads CSS using URL parameter ver= to assure proper refresh.
						- Setting $fonts_in_gutenberg changed to $builder_fonts_in_gutenberg (Bricks, Oxygen)
						- Setting renamed: 
						  $cssoutput to $css_output, $cssminimize to $css_minimize, $fontdisplay to $font_display
						- $timing now allows levels 0/false (off), 1/true (basic), 2 (extended)
						- $timing now also calculates memory usage. Please note that memory usage won't simply 
						  be cummulated. Methods use memory temporarily but also free memory after use.
2022-10-17	3.3.1		Changes:
						- Migrated JavaScript from jQuery to vanilla JS (ES6) to eliminate jQuery dependency.
						- Modernized initialization to avoid errors with WPCodeBox:
						  Replaced @ error control operator with ?? null coalescing operator
						  (Thanks to Alexander van Aken for reporting)
2022-09-26	3.3.0		New Features:
						- Custom Fonts are now also available in Bricks Builder. They are listed in section
						  "Standard Fonts".
						  (Thanks to Luke Wakefield for the implementation idea and to Tom Homer for contacting
						  and informing me about Luke's solution)
						- New configuration option $fonts_in_gutenberg:
						  Allows to enable (default) or disable the assignment of custom fonts in Gutenberg. 
						Changes:
						- Renamed code snippet from "MA Oxygen Custom Fonts" to "MA Custom Fonts"
						Fixes:
						- The snippet applies display and text fonts defined in Oxygen to Gutenberg Editor. 
						  Google fonts defined in Oxygen are not loaded for Gutenberg. 
						  So we can't assign Google fonts to Gutenberg. Only custom fonts are now assigned.
						  (Thanks to Kamil Alhaijali for reporting) 
2022-09-19	3.2.7		Compatibility Fix:
						- In Oxygen 4.0.4, Pro Menu calls ECF_Plugin::get_font_families() as Ajax call. 
						  For performance reasons, MA Custom Fonts doesn't get initialized for Ajax calls and 
						  doesn't have the base font directory set, which causes an error. We now just return 
						  an empty custom fonts list to prevent this error. That doesn't impact the Pro Menu 
						  functionality. 
						  (Many thanks to Kevin Pudlo from the Oxygen development team for reporting, support 
						  and testing concerning the Pro Menu component, and Alexander van Aken for reporting 
						  a similar issue concerning the Mega Menu component.)
2022-06-02	3.2.6		New Features:
						- New configuration option $wfl_support_woff: 
						  Font packages downloaded from Web Font Loader (https://webfontloader.altmann.de) 
						  contain WOFF2 files supported by all modern browsers, and also WOFF files to support 
						  ancient browsers before 2016 like Internet Explorer, or Safari on older Apple devices.
						  The new default setting is to NOT provide old style WOFF files for ancient browsers.
						  Set this option to true if you still need to support old browsers before 2016.
						  (Many thanks to Sunny Trochaniak and Yan Kiara for reporting issues and supporting 
						  investigations with unexpected WOFF loading when using symbols/emoji)
						- Added display of timing details at end of page Appearance > MA Custom Fonts
						Fixes:
						- Corrected initialization of dummy ECF plugin to let Oxygen detect custom fonts and 
						  prevents attempts to be loaded as Google fonts
						  (Thanks to Firat Sekerli for reporting)
						Changes:
						- Removed configuration and all code for debugging.
						- For Web Font Loader fonts: Only emit CSS if related font file exists. 
						  User might have deleted e.g. the files for a specific language.
2022-03-13	3.2.5		New Features:
						- Legend and coloring for font formats in custom fonts test screen
						Changes:
						- Renamed Admin menu Appearance > "Custom Fonts" to "MA Custom Fonts"
						- Renamed test shortcode from "maltmann_custom_fonts_test" to "ma-customfonts-test"
						- Optimized handling of font_base (dir, url) by class var
						- Enhanced detection of variable weight fonts by [wght] in filename
						  (Thanks to Paul Batey for reporting)
						- Adapted min/max font weight for variable fonts from 100/900 to 1/1000 according to 
						  https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-weight#values
						  (Thanks to Paul Batey for reporting)
						- Completely re-built custom font test screen (WP Admin and Shortcode) to improve 
							- variable weight fonts (display logic)
							- responsive view (for test on smartsphones)
2022-02-11				Tested with PHP 8.0, WordPress 5.9, Oxygen 4.0 beta 1
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
						  (Thanks to Henning Wechsler for reporting)
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 version for client project
--------------------------------------------------------------------------------------------------------------
*/

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

	const TITLE			= 'MA Custom Fonts';
	const SLUG			= 'ma-customfonts';
	const VERSION		= '3.4.2';

	// ===== CONFIGURATION ==============================================================================================
	/** Define how fonts are displayed. 
	 * Corresponds to CSS font-display: 'auto', 'block', 'swap', 'fallback', 'optional' or '' (disable) 
	 * @link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display
	 */
	public $font_display					= 'block';

	/** Defines how CSS is emitted: 
	 * - 'file' emits a link to a CSS file (cacheable by browser), 
	 * - 'html' emits inline CSS in HTML header 
	 */
	public $css_output						= 'file';
	
	/** Enables (`true`) or disables (`false`) minimizing of CSS. 
	 * Caution! Some Performance/Cache plugins don't handle minimized CSS containing @font-face correctly. 
	 * For FlyingPress, there's now already a filter to skip already minimized ma_customfonts.css.  
	 */
	public $css_minimize					= true;
	
	/** Defines support of old style WOFF files from Web Font Loader for browsers before 2016. */
	public $wfl_support_woff				= false;	

	/** Sample text used on admin page Appearance > MA Custom Fonts */
	public $sample_text 					= 'The quick brown fox jumps over the lazy dog.';	

	/** Gutenberg: Enables selection of available custom font families */
	public $gutenberg_font_family_select 	= true; 
	
	/** Bricks/Oxygen Builder: 
	 * Enables use of default fonts in Gutenberg as defined in Theme Styles (Bricks) or Global Settings (Oxygen). 
	 * If $gutenberg_font_family_select is set to true, this setting is ignored since fonts need to be loaded anyway.
	 */
	public $builder_fonts_in_gutenberg		= true;	

	/** Enable runtime and memory usage info to WordPress debug.log if WP_DEBUG also enabled. 
	 * Please note that memory usage won't simply be cummulated. Methods use memory temporarily 
	 * but also free memory after use. Memory usage is thus just a snapshot.
	 * - false/0: Disabled 
	 * - true/1: Enabled 
	 * - 2: Extended.
	 */
	public $timing							= 0;

	// ===== INTERNAL. DO NOT EDIT. =====================================================================================
	private $incompatibilities 				= [];	// incompatibilities detected before initialization
	private $prioritized_formats			= ['eot','woff2','woff','ttf','otf','svg']; // font formats in prioritized CSS order
	private $var_weight_formats				= ['woff2','ttf']; // font formats allowing variable weight
	private $fonts_base						= null; // will be initialized automatically
	private $fonts_details_cache			= [];	// cache for already parsed font details
	private $fonts 							= null;	// list of fonts and related files
	private $font_families					= null;	// list of font family names
	private $font_files_cnt					= 0;	// number of font files parsed
	private $font_css						= null;	// temp storage for custom font css
	private $gutenberg_font_classes			= null; // css classes to be used with Gutenberg (.has-xyz-font-family)
	private $gutenberg_editor_font_styles	= [];	// temp storage for Gutenberg css defining oxygen fonts

	private $timing_collect_fonts			= 0;	// used to show timing on admin page
	private $timing_generate_css			= 0;	// used to show timing on admin page
	private $timing_total_runtime			= 0;	// total runtime

	private $memory_collect_fonts			= 0;	// used to show memory usage on admin page
	private $memory_generate_css			= 0;	// used to show memory usage on admin page
	private $memory_total_usage				= 0;	// total memory usage

	


	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Class constructor
	 */
	function __construct() {
		$st = microtime(true);
		$sm = memory_get_usage();
		$GLOBALS['MA_CustomFonts'] = $this;

		if (wp_doing_ajax()) 		goto DONE;	// don't run for AJAX requests
		if (wp_doing_cron()) 		goto DONE;	// don't run for CRON requests
		if (wp_is_json_request()) 	goto DONE;	// don't run for JSON requests

		if (WP_DEBUG && $this->timing>1) {error_log(sprintf('%s Initializing...',__CLASS__));}

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

		if ($this->is_incompatible()) return;

		$this->fonts_base = $this->get_fonts_base(); 
		if (!$this->fonts_base) {return;}

		// collect fonts, font families, font classes gutenberg
		$this->collect_fonts();

		// pre-fill custom font css, and optionally write file
		$this->font_css = $this->generate_font_css();

		// FlyingPress doesn't handle CSS minify correctly for already minified CSS containing @font-face
		add_filter('flying_press_exclude_from_minify:css', function ($exclude_keywords) 
			{return array_merge($exclude_keywords??[],[self::SLUG.'.css']);});
		
		// Backend: Admin menu, 
		add_action('admin_menu', [$this, 'admin_init_menu']);
		add_action('admin_init', [$this, 'admin_init']);

		// Frontend: Emit custom font css in head 
		add_action('wp_head', [$this, 'frontend_css'], 5);
		add_action('wp_footer', [$this, 'debug_info']);

		// Bricks: Add custom fonts to Bricks Builder UI.
		add_filter( 'bricks/builder/standard_fonts', function($fonts) {
			return array_merge($fonts, $this->get_font_families());
		});

		// Gutenberg
		if ($this->gutenberg_font_family_select) {
			add_filter('wp_theme_json_data_theme', [$this, 'theme_json_inject_fonts']);
			add_action('admin_head', [$this, 'blocks_config']);
		}

		// Shortcode for testing custom fonts (listing all fonts with their formats, weights, styles)
		add_shortcode('ma-customfonts-test', function( $atts, $content, $shortcode_tag ) {
			return $this->get_font_samples('shortcode');
		}); 

		DONE:
		// add a handler for logging total runtime
		add_action('shutdown', [$this, 'total_runtime_memory']);
		
		$et = microtime(true);
		$em = memory_get_usage();
		if (WP_DEBUG && $this->timing>1 && $this->fonts_base) {error_log(sprintf('%s Initialization runtime %.5f sec., memory usage: %.2f KB',__CLASS__,$et-$st, ($em-$sm)/1024));}
		$this->timing_total_runtime += $et-$st;
		$this->memory_total_usage += $em-$sm;
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Logs total runtime. (only if initialized)
	 */
	public function total_runtime_memory(){
		if (WP_DEBUG && $this->timing && $this->fonts_base) {error_log(sprintf('%s Total runtime: %.5f sec., memory usage: %.2f KB', __CLASS__, $this->timing_total_runtime, $this->memory_total_usage/1024));}
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Checks for incompatibilities. Registers admin notice.
	 * @return bool 				`true` if any incompatibilities found
	 */
	private function is_incompatible(): bool{
		if (!function_exists('is_plugin_active')) return false;
		$incomp = [];
		if (is_plugin_active('elegant-custom-fonts/elegant-custom-fonts.php')) {
			$incomp[] = '"Elegant Custom Fonts"';
		}
		if (is_plugin_active('use-any-font/use-any-font.php')) {
			$incomp[] = '"Use Any Font"';
		}
		if (is_plugin_active('swiss-knife/swiss-knife.php') && (get_option('swiss_font_manager')=='yes')) {
			$incomp[] = '"Swiss Knife" with feature "Font Manager" enabled';
		}
		if (is_plugin_active('Oxy-Font-Manager/Oxy-Font-Manager.php')) {
			$incomp[] = '"Oxy Font Manager"';
		}
		$this->incompatibilities = $incomp;
		if (count($incomp) && is_admin()) {
			add_action('admin_notices', function(){
				if (WP_DEBUG ) {error_log(self::TITLE.' Incompatibilities: '.implode(', ',$this->incompatibilities));}
				echo '<div class="notice notice-error is-dismissible">
						<p>The '.$this->get_script_details()->full.' is not compatible with the Plugin '.implode(', ',$this->incompatibilities).'.<br/>
						Please deactivate either "'.self::TITLE.'" or the named Plugin (feature).</p>
					</div>';
			});
		}	
		return count($incomp) ? true : false;	
	}

	//===================================================================================================================
	// FONTS
	//===================================================================================================================
	//-------------------------------------------------------------------------------------------------------------------
	/** 
	 * Returns base dir and url for fonts. Creates directory wp-content/uploads/fonts if necessary.
	 * @return null|object 			The object with properties `dir`, `url`. Returns `null` on error.
	 */
	private function get_fonts_base(): ?object{
		$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);

		// V3.4.0: Rename ma_customfonts.css to ma-customfonts.css
		if (file_exists($retval->dir.'/ma_customfonts.css')) {
			$status = copy($retval->dir.'/ma_customfonts.css', $retval->dir.'/'.self::SLUG.'.css');
			if ($status === true) {unlink($retval->dir.'/ma_customfonts.css');}
		}
		return $retval;
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Returns slug for a font family name.
	 * @param string $font_name		The font name
	 * @return string				The slug for the font name
	 */
	private function get_font_slug(string $font_name): string {
		$retval = preg_replace('/[^a-z0-9\-\_]/','-',strtolower($font_name));
		return $retval;
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Returns a list of font family names. 
	 * @return array				The list of font family names.
	 */
	function get_font_families(): array {
		return $this->font_families??[];
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * - Collects font files from font folder. 
	 * - Stores family names to $font_families. 
	 * - Prepares Gutenberg CSS Classes in $gutenberg_font_classes.
	 */
	private function collect_fonts() {
		$st = microtime(true);
		$sm = memory_get_usage();
		if (isset($this->fonts)) return; // Already collected.
		$this->fonts = [];
		// recursive scan for font files (including subdirectories)
		$directory_iterator = new RecursiveDirectoryIterator($this->fonts_base->dir,  RecursiveDirectoryIterator::SKIP_DOTS | RecursiveDirectoryIterator::UNIX_PATHS);
		$file_iterator = new RecursiveIteratorIterator($directory_iterator);
		// 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()), $this->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) {
							$this->font_files_cnt ++;
							unset($font_splfiles[$idx]);
						}
					}
					// get the relative path
					$font_path_relative = str_replace($this->fonts_base->dir,'',$font_path);
					// encode every single path element since we might have spaces or special chars 
					$font_path_url = implode('/',array_map('rawurlencode',explode('/',$font_path_relative)));
					
					// add CSS blocks (could be multiple unicode ranges) to fonts list
					$font_baseurl = $this->fonts_base->url . $font_path_url;
					foreach ($font_details->css as $css_ruleset) {
						// check for WOFF support. Skip if not enabled
						if ($css_ruleset->format=='woff' && !$this->wfl_support_woff) {continue;}
						// check if file exists. User might have deleted it.
						if (!file_exists($font_path . $css_ruleset->url)) {continue;}
						$this->fonts[$css_ruleset->{'font-family'}]['source'] = 'Web Font Loader';
						$this->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($this->fonts[$css_ruleset->{'font-family'}][$css_ruleset->{'font-weight'}.'/'.$css_ruleset->{'font-style'}][$css_ruleset->{'format'}])) {
							$this->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 = $this->create_css_from_ruleset($css_ruleset);
						$this->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) {
			$this->font_files_cnt ++;
			$font_ext = $font_splfile->getExtension();
			$font_details = $this->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($this->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,$this->fonts)) {$this->fonts[$font_name] = [];}
			// create entry for this font weight/style 
			if (!array_key_exists($font_weight.'/'.$font_style,$this->fonts[$font_name])) {$this->fonts[$font_name][$font_weight.'/'.$font_style] = [];}
			// store font details for this file
			$this->fonts[$font_name][$font_weight.'/'.$font_style][strtolower($font_ext)] = $this->fonts_base->url . $font_path . '/' . rawurlencode($font_splfile->getBasename());
		}
		ksort($this->fonts, SORT_NATURAL | SORT_FLAG_CASE);

		// store font family names, prepare Gutenberg CSS classes
		$this->font_families = [];
		$this->gutenberg_font_classes = [];
		foreach (array_keys($this->fonts) as $font_name) {
			$this->font_families[] = $font_name;
			$font_slug = $this->get_font_slug($font_name);
			$this->gutenberg_font_classes[] = sprintf('.has-%s-font-family{font-family:"%s";}', $font_slug, $font_name);
		}

		$et = microtime(true);
		$em = memory_get_usage();
		$this->timing_collect_fonts = $et-$st;
		$this->memory_collect_fonts = $em-$sm;
		if (WP_DEBUG && $this->timing>1) {error_log(sprintf('    %s %d font files, %d font families.', __METHOD__, $this->font_files_cnt, count($this->fonts)));}
		if (WP_DEBUG && $this->timing>1) {error_log(sprintf('  %s runtime: %.5f sec., temp memory usage: %.2f KB', __METHOD__, $et-$st, ($em-$sm)/1024));}
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Parses weight and style from a font file name (not used for Web Font Loader packages).
	 * @param string $name				The font file name
	 * @return object					Object with `name`, `weight`, `style`
	 */
	private function parse_font_name(string $name): object {
		// already in cache?
		if (array_key_exists($name,$this->fonts_details_cache)) {return $this->fonts_details_cache[$name];}
		$st = microtime(true);
		$sm = memory_get_usage();
		$retval = (object)['name'=>$name, 'weight'=>400, 'style'=>'normal'];
		$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|\[wght\])/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';
		}
		// 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;
				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); 
		}
		// store to cache
		$this->fonts_details_cache[$name] = $retval;
		$et = microtime(true);
		$em = memory_get_usage();
		if (WP_DEBUG && $this->timing>2) {error_log(sprintf('     %s runtime: %.5f sec., memory usage: %.2f KN', __METHOD__, $et-$st, ($em-$sm)/1024));}
		return $retval;
	}
	
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Returns CSS block from CSS properties stored in JSON from Web Font Loader
	 * @param object $css_ruleset		The CSS ruleset from Web Font Loader
	 * @return string					The CSS @font-face definition as string
	 */
	private function create_css_from_ruleset(object $css_ruleset): string {
		$retval = '';
		if (isset($css_ruleset)) {
			$retval .= "@font-face{\n";
			if (isset($css_ruleset->{'comment'})) {$retval .= sprintf("  /* %s */\n",$css_ruleset->{'comment'});}
			$retval .= sprintf("  font-family:'%s';\n",$css_ruleset->{'font-family'});
			$retval .= sprintf("  font-style:%s;\n",$css_ruleset->{'font-style'});
			$retval .= sprintf("  font-weight:%s;\n",$css_ruleset->{'font-weight'});
			$retval .= sprintf("  src:url('%s') format('%s');\n",$css_ruleset->{'url'}, $css_ruleset->{'format'});
			if (isset($css_ruleset->{'unicode-range'})) {$retval .= sprintf("  unicode-range:%s;\n", $css_ruleset->{'unicode-range'});}
			if ($this->font_display) {
				$retval .= sprintf("  font-display:%s;\n",$this->font_display);
			}			
			$retval .= '}';
		}
		return $retval;
	}

	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Creates CSS for custom fonts. Returned value depends on configuration `$css_output`:
	 * - `file`: `<link rel="stylesheet" ...>`
	 * - `html`: `<style>...</style>`
	 * @param bool $plaincss	`true`to return plain CSS without `<link...>` or `<style></style>` tags. Default is `false`.
	 * 							Currently not used. 
	 * @return string			The CSS as string, either `<link...>` or `<style...>`, or plain CSS
	 */
	function generate_font_css($plaincss = false): string {
		// emit CSS for fonts in footer
		$style = '';
		$st = microtime(true);
		$sm = memory_get_usage();
		foreach ($this->fonts as $font_name => $font_details) {
			ksort($font_details);
			foreach ($font_details as $weight_style => $file_list) {
				if ($weight_style=='source') {continue;}
				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($this->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 = '1 1000';
					} 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 ($this->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 ($this->font_display) {
						$style .= sprintf('  font-display:%s;'.PHP_EOL, $this->font_display);
					}
					$style .= '}'.PHP_EOL;
				}
			}		
		}

		// if $gutenberg_font_family_select is enabled, add Gutenberg font classes
		if ($this->gutenberg_font_family_select && $this->gutenberg_font_classes) {
			foreach ($this->gutenberg_font_classes as $class) {
				$style .= $class.PHP_EOL;
				// prepare overrides for Oxygen default fonts
				$this->gutenberg_editor_font_styles[] = 'body .editor-styles-wrapper :is(h1,h2,h3,h4,h5,h6,.editor-post-title)'.$class.PHP_EOL;
			}
		}

		// minimize string if configured
		if ($this->css_minimize) {
			$style = preg_replace('/\r?\n *(?![\@\.])/', '', $style); // remove line breaks, except before @, .
			$style = preg_replace('/\t/', '', $style); // remove tabs
		}

		$version = $this->get_script_details()->full;
		$retval = '';
		// Generate CSS file no matter how $css_output type is configured. 
		// Block editor in FSE/iframe mode doesn't handle inject @font-face, so we need to load the file in Gutenberg editor
		// write CSS to file
		$css_path = $this->fonts_base->dir.'/'.self::SLUG.'.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 && $this->timing>1) {error_log(sprintf('%s Writing CSS file "%s"',__METHOD__,$css_path));}
			$status = file_put_contents($css_path, $css_code);
			if ($status === false) {error_log(sprintf('%s Error writing CSS file "%s"',__METHOD__,$css_path));}
			$css_hash_file = file_exists($css_path) ? hash_file('CRC32', $css_path, false) : 0;
		}
		if ($this->css_output == 'file') {
			$css_url = str_replace($this->fonts_base->dir,$this->fonts_base->url ,$css_path);
			$retval = sprintf('<link id="%s" href="%s?ver=%s" rel="stylesheet" type="text/css"/>', self::SLUG, $css_url, $css_hash_file);
		}
		if ($this->css_output == 'html') {
			// option: write CSS to html
			$retval = sprintf('<style id="%s">/* Version: %s */%s</style>', self::SLUG, $version, PHP_EOL.$style);
		}
		if ($plaincss) {
			$retval = $style;
		}

		$et = microtime(true);
		$em = memory_get_usage();
		$this->timing_generate_css = $et-$st;
		$this->memory_generate_css = $em-$sm;
		if (WP_DEBUG && $this->timing>1) {error_log(sprintf('  %s runtime: %.5f sec., temp memory usage: %.2f KB', __METHOD__, $et-$st, ($em-$sm)/1024));}
		return $retval;
	}
	
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Returns properly quoted font name(s) from font spec; detects multiple font names and quotes separately. Only quotes 
	 * font names when necessary (e.g. include spaces)
	 * @param string $font_spec		The font name or a string with multiple font names (`Arial, Helvetica Neue`)
	 * @return string 				The quoted font name(s) quoted (`Arial, "Helvetica Neue"`)
	 */
	private function quote_font_names(string $font_spec): string {
		$fonts = preg_split('/,\s*/', $font_spec, -1, PREG_SPLIT_NO_EMPTY);
		$fonts = array_map(function($name){
			return preg_match('/[^A-Za-z0-9\-]/',$name) ? '"'.$name.'"' : $name;
		},$fonts);
		$retval = implode(', ',$fonts);
		return $retval;
	}

	//===================================================================================================================
	// ADMIN / SHORTCODE
	//===================================================================================================================
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Executes actions only needed for admin:
	 * - Loads CSS (file or inline) on pages using Gutenberg editor, and on the AM Custom Font test page
	 * - Oxygen: Retrieves global standard fonts and  
	 */
	public function admin_init(){
		$st = microtime(true);
		$sm = memory_get_usage();

		// Load CSS for calls using Gutenberg Editor, or the MA Custom Fonts test page. 
		// Requires ma-customfont.css (for font loading) and some Oxygen settings (for font assignment)
		global $pagenow;

		// MA Custom Fonts test page
		if ( ($pagenow === 'themes.php') && ($_REQUEST['page']??null === self::SLUG)) {
			// enqueue or embed ma-customfonts.css
			if ($this->css_output == 'file') {
				$css_path = $this->fonts_base->dir.'/'.self::SLUG.'.css';
				$css_hash_file = file_exists($css_path) ? hash_file('CRC32', $css_path, false) : 0;
				wp_enqueue_style(self::SLUG, $this->fonts_base->url.'/'.self::SLUG.'.css', [], $css_hash_file); 
			} else {
				add_action('admin_head', [$this, 'frontend_css'],5);
			}
		}

		// Gutenberg Editor
		if ( ($pagenow === 'post-new.php') || ($pagenow === 'post.php' && ($_REQUEST['action']??'') === 'edit')) {

			// Builder font settings in Gutenberg
			$builder_fonts_to_gutenberg = [];


			if (defined('CT_VERSION') && $this->builder_fonts_in_gutenberg && function_exists('ct_get_global_settings')) {
				// Create custom style for body, h1-h6 from Oxygen global settings
				// Google fonts are not loaded in Gutenberg. We only set styles for custom fonts and websave fonts.
				$ct_global_settings = call_user_func('ct_get_global_settings');

				// body font
				$body = [];
				if (in_array($ct_global_settings['fonts']['Text']??null, $this->font_families)) {
					$body['font-family'] = $this->quote_font_names($ct_global_settings['fonts']['Text']);
				}
				$body_size = $ct_global_settings['body_text']['font-size']??'';
				$body_size_unit = $ct_global_settings['body_text']['font-size-unit']??'';
				if ($body_size && $body_size_unit) {$body['font-size'] = $body_size . $body_size_unit;}
				$body['font-weight'] = $ct_global_settings['body_text']['font-weight']??null;
				$body['line-height'] = $ct_global_settings['body_text']['line-height']??null;
				$body_style = '';
				// TODO: check for simple numeric size/weight/line-height, since e.g. var() might not be provided
				//foreach ($body as $property => $value) {if (!empty($value)) {$body_style .= $property.':'.$value.';';}}
				// for now, we only set font family
				if (!empty($body['font-family'])) {$body_style .= 'font-family:'.$body['font-family'].';';}

				if ($body_style) {
					$builder_fonts_to_gutenberg[] = sprintf('body .editor-styles-wrapper {%s}', $body_style);
				}
				// heading font
				$head = []; // define outside loop. we'll reuse the settings for each heading
				foreach (['H1','H2','H3','H4','H5','H6'] as $h) {
					if (in_array($ct_global_settings['fonts']['Display']??null, $this->font_families)) {
						$head['font-family'] = $this->quote_font_names($ct_global_settings['fonts']['Display']);
					}
					$head_size = $ct_global_settings['headings'][$h]['font-size']??'';
					$head_size_unit = $ct_global_settings['headings'][$h]['font-size-unit']??'';
					if ($head_size && $head_size_unit) {$head['font-size'] = $head_size . $head_size_unit;}
					$head['font-weight'] = $ct_global_settings['headings'][$h]['font-weight']??null;
					$head['line-height'] = $ct_global_settings['headings'][$h]['line-height']??null;
					$head_style = '';
					// TODO: check for simple numeric size/weight/line-height, since e.g. var() might not be provided
					//foreach ($head as $property => $value) {if (!empty($value)) {$head_style .= $property.':'.$value.';';}}
					// for now, we only set font family
					if (!empty($head['font-family'])) {$head_style .= 'font-family:'.$head['font-family'].';';}
					if ($head_style) {
						$selector = $h == 'H1' ? ':is(h1,.editor-post-title)' : ':is('.strtolower($h).')';
						$builder_fonts_to_gutenberg[] = sprintf('body .editor-styles-wrapper %s {%s}', $selector, $head_style);
					}
				}
				// Prepend to Gutenberg editor font styles
				$this->gutenberg_editor_font_styles = array_merge($builder_fonts_to_gutenberg, $this->gutenberg_editor_font_styles);
			}

			if (defined('BRICKS_VERSION') && defined('BRICKS_DB_THEME_STYLES') && $this->builder_fonts_in_gutenberg) {
				// Create custom style for body, h1-h6 from Bricks Theme styles
				$styles = get_option(constant('BRICKS_DB_THEME_STYLES'), []);
				$post_id = get_the_ID();
				$found_styles = [];
				foreach ( $styles as $style_id => $style ) {
					$conditions = $style['settings']['conditions']['conditions']??false;
					if (!is_array($conditions) ) {continue;}
					if (method_exists('Bricks\Database','screen_conditions')) {
						$found_styles = call_user_func('Bricks\Database::screen_conditions', $found_styles, $style_id, $conditions, $post_id, '');
					}
				}
				$style_id = null;
				if (!empty($found_styles)) {
					ksort($found_styles, SORT_NUMERIC);
					$style_id = array_pop($found_styles);
				}
				if ($style_id) {
					$settings = $styles[$style_id]['settings']??[];
				}

				// body font
				$body = [];
				if (in_array($settings['typography']['typographyBody']['font-family']??null, $this->font_families)) {
					$body['font-family'] = $this->quote_font_names($settings['typography']['typographyBody']['font-family']??null);
				}
				$body['font-size'] = $settings['typography']['typographyBody']['font-size']??null;
				$body['font-weight'] = $settings['typography']['typographyBody']['font-weight']??null;
				$body['line-height'] = $settings['typography']['typographyBody']['line-height']??null;
				$body_style = '';
				// TODO: check for simple numeric size/weight/line-height, since e.g. var() might not be provided
				//foreach ($body as $property => $value) {if (!empty($value)) {$body_style .= $property.':'.$value.';';}}
				if (!empty($body['font-family'])) {$body_style .= 'font-family:'.$body['font-family'].';';}
				if ($body_style) {
					$builder_fonts_to_gutenberg[] = sprintf('body .editor-styles-wrapper {%s}', $body_style);
				}

				// heading font
				$head = []; // define outside loop. we'll reuse the settings for each heading
				$head_defaults = [
					'H1'	=> ['font-size'=>'2.4em', 'line-height'=>'1.4'],
					'H2'	=> ['font-size'=>'2.1em', 'line-height'=>'1.4'],
					'H3'	=> ['font-size'=>'1.8em', 'line-height'=>'1.4'],
					'H4'	=> ['font-size'=>'1.6em', 'line-height'=>'1.4'],
					'H5'	=> ['font-size'=>'1.3em', 'line-height'=>'1.4'],
					'H6'	=> ['font-size'=>'1.1em', 'line-height'=>'1.4'],
				];
				foreach (['s','H1','H2','H3','H4','H5','H6'] as $h) {
					// s = global settings
					if (in_array($settings['typography']['typographyHeading'.$h]['font-family']??null, $this->font_families)) {
						$head['font-family'] = $this->quote_font_names($settings['typography']['typographyHeading'.$h]['font-family']);
					}
					$head['font-size'] = $settings['typography']['typographyHeading'.$h]['font-size']??$head_defaults[$h]['font-size']??null;
					$head['font-weight'] = $settings['typography']['typographyHeading'.$h]['font-weight']??null;
					$head['line-height'] = $settings['typography']['typographyHeading'.$h]['line-height']??$head_defaults[$h]['line-height']??null;
					$head_style = '';
					// TODO: check for simple numeric size/weight/line-height, since e.g. var() might not be provided
					//foreach ($head as $property => $value) {if (!empty($value)) {$head_style .= $property.':'.$value.';';}}
					// for now, we only set font family
					if (!empty($head['font-family'])) {$head_style .= 'font-family:'.$head['font-family'].';';}
					if ($head_style && ($h != 's')) {
						$selector = $h == 'H1' ? ':is(h1,.editor-post-title)' : ':is('.strtolower($h).')';
						$builder_fonts_to_gutenberg[] = sprintf('body .editor-styles-wrapper %s {%s}', $selector, $head_style);
					}
				}
				// Prepend to Gutenberg editor font styles
				$this->gutenberg_editor_font_styles = array_merge($builder_fonts_to_gutenberg, $this->gutenberg_editor_font_styles);
			}

			// Custom Fonts in Gutenberg
			// Block editor in FSE/iframe mode doesn't handle @font-face rules as inline. 
			// Load external CSS no matter how $css_output is configured
			if (count($this->gutenberg_editor_font_styles)) {
				add_filter('block_editor_settings_all', function($editor_settings, $editor_context){
					// add font-size 16
					$styles = implode(PHP_EOL, $this->gutenberg_editor_font_styles);
					$styles = preg_replace('/^body \.editor\-styles\-wrapper/m','body', $styles);
					$editor_settings['styles'][] = ['css' => $styles];
					return $editor_settings;
				}, 10,2 );
				add_action('enqueue_block_assets', function(){
					$css_path = $this->fonts_base->dir.'/'.self::SLUG.'.css';
					$css_hash_file = file_exists($css_path) ? hash_file('CRC32', $css_path, false) : 0;
					// enqueue with reltive path instead of our URL since block assets in FSE iframe can't be used scheme-less
					$css_rel_path = str_replace(ABSPATH,'/',$css_path);
					wp_enqueue_style(self::SLUG, $css_rel_path, [], $css_hash_file);
				});
			}

		}
		$et = microtime(true);
		$em = memory_get_usage();
		if (WP_DEBUG && $this->timing>1) {error_log(sprintf('%s runtime: %.5f sec., memory usage: %.2f KB',__METHOD__,$et-$st, ($em-$sm)/1024));}
		$this->timing_total_runtime += $et-$st;
		$this->memory_total_usage += $em-$sm;
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Emits frontend CSS. Can be either a link or inline depending on configuration $css_output.
	 */
	public function frontend_css(){
		$st = microtime(true);
		$sm = memory_get_usage();
		// common custom font definition, optionally contains Gutenberg .has-xyz-font-family classes
		echo $this->font_css;

		// Oxygen Builder: Add custom styles for font lists
		if (defined('CT_VERSION') && defined('SHOW_CT_BUILDER') && (!defined('OXYGEN_IFRAME'))) {
			$oxygen_builder_styles = [];
			// Show custom fonts in light blue.
			$selector = '.oxygen-select-box-option.ng-binding.ng-scope[ng-repeat*="elegantCustomFonts"]'; 
			$oxygen_builder_styles[] = $selector.'{color:lightblue;justify-content:flex-start;}';
			$oxygen_builder_styles[] = $selector.'::before {content:"Custom";margin-right:5px;padding:2px 3px;background-color:lightblue;border-radius:2px;color:black;font-size:75%}';
			// Optiopnal: Hide "Top 20" Google fonts if Google fonts are disabled
			if (get_option('oxygen_vsb_disable_google_fonts')??null == 'true') {
				$selector = '.oxygen-select-box-option.ng-binding[ng-repeat^="font in iframeScope.googleFontsList"]';
				$oxygen_builder_styles[] = $selector.'{display:none;}';
			} 
			echo sprintf('<style id="%s-oxygen">%s</style>', self::SLUG, implode(PHP_EOL, $oxygen_builder_styles));
		}
		$et = microtime(true);
		$em = memory_get_usage();
		if (WP_DEBUG && $this->timing>1) {error_log(sprintf('%s runtime %.5f sec., temp memory usage: %.2f KB',__METHOD__,$et-$st, ($em-$sm)/1024));}
		$this->timing_total_runtime += $et-$st;
		$this->memory_total_usage += $em-$sm;
	}
	//-------------------------------------------------------------------------------------------------------------------
	/** @since 3.4.0 */
	public function debug_info(){
		/** @since 1.2.0 debugging info */ 
		echo sprintf('<span id="%s-info" data-nosnippet style="display:none">%s %s %s</span>', self::SLUG, $this->get_script_details()->type, self::SLUG, self::VERSION); 
	}
	

	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Registers the Admin menu Appearance > MA Custom Fonts.
	 */
	public function admin_init_menu() {
		add_submenu_page(	'themes.php', 									// parent slug of "Appearance"
							//'ct_dashboard_page',							// parent slug of "Oxygen"
							_x(self::TITLE,'page title', 'ma-customfonts'), // page title
							_x(self::TITLE,'menu title', 'ma-customfonts'), // menu title
							'manage_options',								// capabilitiy
							self::SLUG,										// menu slug
							[$this, 'admin_customfonts']					// function
		);
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Outpuths the admin page Appearance > MA Custom Fonts to display samples of all detected fonts
	 */
	public function admin_customfonts() {
		$output =	'<div class="wrap">'.
						'<h1>' . esc_html(get_admin_page_title()) . '</h1>'.
						$this->get_font_samples('admin').
					'</div>';
		echo $output;
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Returns parsed font file url from CSS block
	 * @param string|array $css			The CSS to pasre (typically a @font-face ruleset)
	 * @return array					The URLs parsed from CSS
	 */
	private 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 font file urls for var weight
	 * @param array $font_deatils		The font details (array with coollected font details)
	 * @return array					Array with relative paths to the variable weight files, or empty array if none
	 */
	private function get_font_var_weight_urls(?array $font_details): array {
		$retval = [];
		if (isset($font_details) && (is_array($font_details))) {
			// check if we have a 'var' weight array key
			foreach ($font_details as $weight_style => $details) {
				// check key (for custom fonts)
				if (strpos($weight_style,'var') === 0) {
					foreach ($this->var_weight_formats as $format) { // support var weight for specific formats
						if (isset($details[$format])) {
							$retval[] = urldecode($details[$format]);
						}
					}
					goto DONE;
				}
				// check CSS file names of woff2 files (for WebFontLoader fonts)
				if (isset($details['woff2']) && is_array($details['woff2'])) {
					foreach ($details['woff2'] as $css_rule) {
						if (preg_match('/src: url\(\'(.*?)\'\)/',$css_rule,$matches)) {
							$src = $matches[1];
							if (preg_match('/[ \-]?(VariableFont|\[wght\])/i',$src)) {
								$retval[] = $src;
							}
						}
					}
				}
			}
		}
		DONE:
		// remove duplicates
		$retval = array_unique($retval);
		// remove base url
		foreach ($retval as &$url) {
			$url = str_replace($this->fonts_base->url.'/','',$url);
		}
		return $retval;
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Returns font url (string or array) shortened and urldecoded
	 * @param ?string|array $urls		The full URL(s)
	 * @return array					The relative URLs rawdecoded
	 */
	private function get_font_short_display_url($urls) {
		$retval = [];
		if ($urls) {
			if (!is_array($urls)) {$urls = [$urls];}
			foreach ($urls as $url) {
				// cut leading path/url from file info
				$url = str_replace($this->fonts_base->url.'/','',$url);
				// decode html entities (e.g. %20) in file path
				$url = implode('/',array_map('rawurldecode',explode('/',$url)));
				$retval[] = $url;
			}
		}
		return implode(', ',$retval);
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Returns HTML to display samples of registered fonts. 
	 * @param string $mode Format of returned page:
	 * - `'admin'`: 		formatting to be displayed on WP Admin > Appearance
	 * - `'shortcode'`:	formatting to be displayed as shortcode output
	 * @return string		The HTML as string
	 */
	private function get_font_samples($mode = null): string {
		$st = microtime(true);
		$sm = memory_get_usage();
		$script_details = $this->get_script_details()->full;
		$sample_text = $this->sample_text;
		$output_style = <<<'END_OF_STYLE'
		<style>
		.ma-customfonts-wrap {font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Ubuntu,Cantarell,Helvetica,sans-serif;font-size:13px;margin:20px 0;}
		.ma-customfonts-wrap h3 {margin-top:20px;padding-bottom:0;}

		.ma-customfonts-header {border:1px solid darkgray;border-radius:10px;padding:10px;padding-top:0;}
		.ma-customfonts-header > div {display:flex;flex-direction:row;margin-top:.5em;}
		.ma-customfonts-label {flex-shrink:0;display:inline-block;width:110px;}

		#ma-customfonts-input-font-size {width:60px;text-align:center;min-height:1.5em;line-height:1em;padding:0;}
		#ma-customfonts-input-sample-text {width:100%;max-width:400px;text-align:left;}

		.ma-customfonts-legend {margin-top:3em;border:1px solid darkgray;border-radius:10px;padding:10px;padding-top:0;}
		.ma-customfonts-legend > div {display:flex;flex-direction:row;margin-top:.5em;}
		.ma-customfonts-legend > div > span:first-child {width:15ch;flex-shrink:0;}

		.ma-customfonts-font-additional-info {margin-left:1em;margin-bottom:1em;font-size:13px;font-weight:normal;}

		.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:1.2em;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-format-info.simulated {color:gray;font-style:italic;cursor:alias;} 
		.ma-customfonts-format-info :is(.eot,.ttf,.svg) {color:red;}
		.ma-customfonts-format-info :is(.ttf,.otf) {color:chocolate;}
		.ma-customfonts-format-info :is(.woff) {color:orange;}
		.ma-customfonts-format-info :is(.woff2) {color:green;}

		.ma-customfonts-simulated {display:none;}
		@media (max-width:700px) {
			.ma-customfonts-font-row {flex-wrap:wrap;}
			.ma-customfonts-font-info {order:1;}
			.ma-customfonts-font-sample {order:3; width:100%;}
			.ma-customfonts-format-info {order:2;}
		}
		</style>
END_OF_STYLE;
		$var = (object)[
			'container_style'	=> $mode=='shortcode' ? 'style="border:1px dashed darkgray;padding:10px;"' : '',
			'title'				=> $mode=='shortcode' ? '<h2>'.self::TITLE.'</h2>' : '',
			'cnt_families'		=> count($this->fonts),
			'cnt_files'			=> $this->font_files_cnt,
		];
		$output_header = <<<END_OF_HEADER
		<div class="ma-customfonts-wrap" {$var->container_style}>
			{$var->title}
			<div class="ma-customfonts-header">
				<div>
					<span class="ma-customfonts-label">Version:</span>
					<span>{$script_details}<span>
				</div>
				<div>
					<span class="ma-customfonts-label">Font Families:</span>
					<span>{$var->cnt_families}<span>
				</div>
				<div>
					<span class="ma-customfonts-label">Font Files:</span>
					<span>{$var->cnt_files}</span>
				</div>
				<div>
					<span class="ma-customfonts-label">Sample Font Size:</span>
					<span><input id="ma-customfonts-input-font-size" type="number" value="16" onchange="ma_customfonts_change_font_size();"> px</span>
				</div>
				<div>
					<span class="ma-customfonts-label">Sample Text:</span>
					<input id="ma-customfonts-input-sample-text" value="{$sample_text}" onkeyup="ma_customfonts_change_sample_text();">
				</div>
				<div>
					<span class="ma-customfonts-label">Simulated:</span>
					<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.</span>
				</div>
			</div>
END_OF_HEADER;
		$output_legend = <<<END_OF_LEGEND
		<fieldset class="ma-customfonts-legend">
			<legend><strong>Font File Formats</strong></legend>
			<div><span style="color:green">WOFF2</span><span>Modern formats are perfect for web.</span></div>
			<div><span style="color:orange">WOFF</span><span>Older formats can be used for web but have larger file sizes.</span></div>
			<div><span style="color:chocolate">OTF, TTF</span><span>Formats meant for desktop use, can be used for web but have larger to huge file sizes.</span></div>
			<div><span style="color:red">EOT, SVG</span><span>Some formats are only supported on specific browsers like EOT on IE, SVG on Safari. Deprecated!</span></div>
		</fieldset>
END_OF_LEGEND;
		// controls
		$output_script = <<<'END_OF_SCRIPT'
		<script>
		function changeCss($className, $classValue) {
			// we need invisible container to store additional css definitions
			let $cssMainContainer = document.querySelector('#ma-customfonts-css-modifier-container');
			if ($cssMainContainer === null) {
				$cssMainContainer = document.createElement('div');
				$cssMainContainer.id = 'ma-customfonts-css-modifier-container';
				$cssMainContainer.style.display = 'none';
				document.body.appendChild($cssMainContainer);
			}
			// we need one div for each class
			let $classContainer = $cssMainContainer.querySelector('div[data-class="' + $className + '"]');
			if ($classContainer === null) {
				$classContainer = document.createElement('div');
				$classContainer.dataset.class = $className;
				$cssMainContainer.appendChild($classContainer);
			}
			// set class style
			$classContainer.innerHTML = '<style type="text/css">.'+$className+' {'+$classValue+'}</style>';
		}
		function ma_customfonts_change_font_size() {
			let $val = document.querySelector('#ma-customfonts-input-font-size').value;
			changeCss('ma-customfonts-font-sample','font-size: '+$val+'px;');
		}
		function ma_customfonts_change_sample_text() {
			let $val = document.querySelector('#ma-customfonts-input-sample-text').value;
			document.querySelectorAll('.ma-customfonts-font-sample').forEach( ($elm) => {$elm.textContent=$val;} );
		}
		function ma_customfonts_toggle_simulated() {
			let $simulated = document.querySelector('#ma-customfonts-input-simulated').checked;
			document.querySelectorAll('.ma-customfonts-simulated').forEach( ($elm) => {$elm.style.display = $simulated?'flex':'none';} );
		}
		</script>
END_OF_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; } }
		// prepare data structure to display fonts in each weight/style combination
		$samples = [];
		foreach (array_keys($this->fonts) as $font_name) {
			$samples[$font_name] = [
				'has_var_weight' => false,
				'weights_styles' => [],
			];
			foreach ($weights_styles as $weight_style) {
				$samples[$font_name]['weights_styles'][$weight_style] = [];
			}
		}
		// collect available font files
		foreach ($this->fonts as $font_name => $font_details) {
			ksort($font_details);

			// check if we have a 'var' weight
			$font_var_weight_urls = $this->get_font_var_weight_urls($font_details);
			if ($font_var_weight_urls) {$samples[$font_name]['has_var_weight'] = true;}
			$samples[$font_name]['source'] = $font_details['source'] ?? null;

			// loop font details and fill available samples and formats
			foreach ($font_details as $weight_style => $formats) {
				if ($weight_style=='source') {continue;}
				list ($weight,$style) = explode('/',$weight_style);
				
				if ($weight == 'var') {
					//  var weight detected from file name; we don't know the supported weights; fill all weights for current style
					foreach ($this->var_weight_formats as $format) { // support var weight for specific formats
						if (isset($formats[$format])) {
							$url = $formats[$format];
							$url = $this->get_font_short_display_url($url);
							foreach ($weights as $var_weight) {
								$samples[$font_name]['weights_styles'][$var_weight.'/'.$style][$format] = $url;
							}
						}
					}
				} 
				// fill non var weight formats 
				foreach ($formats as $format => $url) {
					if (!in_array($format,$this->prioritized_formats)) {continue;} // skip 'has_css'
					if ($weight != 'var') {
						if (is_array($url)) { // seems to be an array of CSS rules
							$url = $this->get_font_file_info_from_css($url);
						}
						$url = $this->get_font_short_display_url($url);
						$samples[$font_name]['weights_styles'][$weight.'/'.$style][$format] = $url;
					}
				}
			}
		}
		$output_sample = '';
		foreach ($samples as $font_name => $font_details) {
			
			// output the font sample block
			$output_sample .= sprintf('<h3>%1$s</h3>',$font_name);
			// additional font info (variable fonts, source Web Font Loader, WOFF support)
			$font_additional_infos = [];
			if ($font_details['has_var_weight']) {$font_additional_infos[] = 'Variable Weight Font.';}
			if ($font_details['source'] == 'Web Font Loader') {
				$font_additional_infos[] = 'Downloaded from <a href="https://webfontloader.altmann.de/" target="_blank">Web Font Loader</a>.';
				if (!$this->wfl_support_woff) {$font_additional_infos[] = 'WOFF files are ignored according to configuration setting.';}
			}
			$output_sample .= count($font_additional_infos) ? '<div class="ma-customfonts-font-additional-info">'.implode(' ',$font_additional_infos).'</div>' : '';

			foreach ($font_details['weights_styles'] as $weight_style => $formats) {
				if ($weight_style=='source') {continue;}
				list ($weight,$style) = explode('/',$weight_style);
				// build font file info output
				$font_file_list = [];
				foreach ($formats as $format => $files) {
					if (is_array($files)) {$files = implode("\n",$files);}
					$font_file_list[$format] = sprintf('<span class="%3$s" title="%2$s">%1$s</span>', strtoupper($format), $files, $format);
				}
				$font_file_info = '<span class="ma-customfonts-format-info">(' . implode(', ',array_values($font_file_list)) . ')</span>';
				$output_sample .= sprintf(	'<div class="ma-customfonts-font-row '.($font_file_list?'':'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_list?$font_file_info:'<span class="ma-customfonts-format-info simulated">(simulated)</span>');
			}
		}
		// output timing
		$runtime = (object)[
			'collect_fonts'	=> sprintf('runtime: %.4f sec.',$this->timing_collect_fonts),
			'generate_css'	=> sprintf('runtime: %.4f sec.',$this->timing_generate_css),
			'total'			=> sprintf('runtime: %.4f sec.',$this->timing_total_runtime),
			'test_page'		=> sprintf('runtime: %.4f sec.',microtime(true)-$st),
		];
		$memory = (object)[
			'collect_fonts'	=> sprintf('temp memory usage: %.2f KB',$this->memory_collect_fonts/1024),
			'generate_css'	=> sprintf('temp memory usage: %.2f KB',$this->memory_generate_css/1024),
			'total'			=> sprintf('net memory usage: %.2f KB.',$this->memory_total_usage/1024),
			'test_page'		=> sprintf('temp memory usage: %.2f KB',(memory_get_usage()-$sm)/1024),
		];
		$output_timing = <<<END_OF_TIMING
		<fieldset class="ma-customfonts-legend">
			<legend><strong>Timing & Memory</strong></legend>
			<div><span>Collect fonts:</span><span>{$runtime->collect_fonts}, {$memory->collect_fonts}</span></div>
			<div><span>Generate CSS:</span><span>{$runtime->generate_css}, {$memory->generate_css}</span></div>
			<div><span>Total:</span><span>{$runtime->total}, {$memory->total}</span></div>
			<div><span>Test page:</span><span>{$runtime->test_page}, {$memory->test_page}</span></div>
		</fieldset>
END_OF_TIMING;



		$output =  $output_style . $output_header . $output_script . $output_sample . $output_legend . $output_timing.'</div>';

		$et = microtime(true);
		$em = memory_get_usage();
		if (WP_DEBUG && $this->timing>1) {error_log(sprintf('%s runtime: %.5f sec., temp memory usage: %.2f KB', __METHOD__, $et-$st, ($em-$sm)/1024));}
		return $output;

	}
	//===================================================================================================================
	// GUTENBERG
	//===================================================================================================================
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Injects font families into theme.json
	 * @param WP_Theme_JSON_Data $theme_json		The theme.json object
	 * @return WP_Theme_JSON_Data					The modified object
	 */
	public function theme_json_inject_fonts(WP_Theme_JSON_Data $theme_json): WP_Theme_JSON_Data {
		$st = microtime(true);
		$sm = memory_get_usage();
		$fontFamilies = [];
		foreach ($this->font_families as $name) {
			$slug = $this->get_font_slug($name);
			$fontFamilies[] = [
				'fontFamily'	=> '"'.$name.'"',
				'slug'			=> $slug,
				'name'			=> $name,
			];
		}
		$new_data = [
			'version' 	=> 2,
			'settings' 	=> [
				'typography' => [
					'fontFamilies' => $fontFamilies,
				]
			]
		];
		$retval = $theme_json->update_with($new_data);
		$et = microtime(true);
		$em = memory_get_usage();
		if (WP_DEBUG && $this->timing>1) {error_log(sprintf('%s runtime: %.5f sec., memory usage: %.2f KB', __METHOD__, $et-$st, ($em-$sm)/1024));}
		$this->timing_total_runtime += $et-$st;	
		$this->memory_total_usage += $em-$sm;
		return $retval;
	}
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Modifies Gutenberg blocks config via JS filter.
	 */
	public function blocks_config() {
		$st = microtime(true);
		$sm = memory_get_usage();
		?>
		<script>
		window.wp && wp.hooks && wp.hooks.addFilter( 'blocks.registerBlockType', 'ma/customfonts', ( settings, name ) => {
			if (settings.supports && settings.supports.typography) {
				settings.supports.typography.__experimentalDefaultControls = {
					...settings.supports.typography.__experimentalDefaultControls, 
					...{fontFamily:true} 
				};
			}

			return settings;
		} )
		</script>
		<?php
		$et = microtime(true);
		$em = memory_get_usage();
		if (WP_DEBUG && $this->timing>1) {error_log(sprintf('%s runtime: %.5f sec., memory usage: %.2f KB', __METHOD__, $et-$st, ($em-$sm)/1024));}
		$this->timing_total_runtime += $et-$st;	
		$this->memory_total_usage += $em-$sm;
	}
	//===================================================================================================================
	// UTILS
	//===================================================================================================================
	//-------------------------------------------------------------------------------------------------------------------
	/**
	 * Returns the script details as type, version, combined.
	 * @return object	The script  type and version:
	 * - `type`: The type as 'Plugin' or 'Code Snippet'.
	 * - `version`: The version.
	 * - `combined`: The combination of type and version.
	 * - `full`: The combination of type, title and version
	 */
	public function get_script_details() {
		$type = basename(__FILE__) == 'ma-customfonts.php' ? 'Plugin' : 'Code Snippet';
		$retval =(object)[
			'type'		=> $type,
			'version'	=> self::VERSION,
			'combined'	=> sprintf('%s %s', $type, self::VERSION),
			'full'		=> sprintf('%s "%s" %s', $type, self::TITLE, self::VERSION),
		];
		return $retval;;
	}

} // end of class MA_CustomFonts

// Initialization
new MA_CustomFonts();

//-------------------------------------------------------------------------------------------------------------------
// create a primitive ECF_Plugin class if plugin "Elegant Custom Fonts" is not installed
if (!class_exists('ECF_Plugin')) {
	class ECF_Plugin {
		static function get_font_families() {
			$st = microtime(true);
			$sm = memory_get_usage();
			$font_family_list = $GLOBALS['MA_CustomFonts']->get_font_families();
			$et = microtime(true);
			$em = memory_get_usage();
			if (WP_DEBUG && $GLOBALS['MA_CustomFonts']->timing) {error_log(sprintf('MA_CustomFonts/%s runtime: %.5f sec, memory usage: %.2f KB', __METHOD__, $et-$st, ($em-$sm)/1024));}
			return $font_family_list;
		}
	}
	global $ECF_Plugin;
	$ECF_Plugin = new ECF_Plugin();
}

endif;
magnifier