v1-pre
This commit is contained in:
parent
572552ff13
commit
847ed92c23
10 changed files with 1232 additions and 591 deletions
|
|
@ -6,7 +6,17 @@
|
|||
|
||||
/* Container */
|
||||
.mlf-wrap {
|
||||
max-width: 900px;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.mlf-wrap > h1 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mlf-description {
|
||||
color: #646970;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mlf-container {
|
||||
|
|
@ -18,14 +28,16 @@
|
|||
background: #fff;
|
||||
border: 1px solid #c3c4c7;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
padding: 20px 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mlf-section h2 {
|
||||
margin-top: 0;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #c3c4c7;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/* Form */
|
||||
|
|
@ -33,7 +45,11 @@
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mlf-form-row label {
|
||||
.mlf-form-row:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mlf-form-row > label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
|
|
@ -43,126 +59,212 @@
|
|||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mlf-form-row .description {
|
||||
margin-top: 8px;
|
||||
color: #646970;
|
||||
font-style: italic;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Checkbox Grid */
|
||||
.mlf-checkbox-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 10px;
|
||||
/* Search Input */
|
||||
.mlf-search-wrapper {
|
||||
position: relative;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.mlf-checkbox-grid-small {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
max-width: 300px;
|
||||
.mlf-search-icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #646970;
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.mlf-checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #f6f7f7;
|
||||
border: 1px solid #dcdcde;
|
||||
.mlf-search-input {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
padding-left: 36px !important;
|
||||
padding-right: 36px !important;
|
||||
}
|
||||
|
||||
.mlf-search-spinner {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
float: none !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Search Results */
|
||||
.mlf-search-results {
|
||||
margin-top: 12px;
|
||||
border: 1px solid #c3c4c7;
|
||||
border-radius: 4px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.mlf-results-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mlf-result-item {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.mlf-checkbox-label:hover {
|
||||
background: #f0f0f1;
|
||||
.mlf-result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.mlf-checkbox-label input[type="checkbox"] {
|
||||
margin: 0;
|
||||
.mlf-result-item:hover {
|
||||
background: #f0f6fc;
|
||||
}
|
||||
|
||||
.mlf-checkbox-label input[type="checkbox"]:checked + span {
|
||||
.mlf-result-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mlf-result-name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: #1d2327;
|
||||
}
|
||||
|
||||
/* File Count */
|
||||
.mlf-form-row-info {
|
||||
padding: 12px 16px;
|
||||
.mlf-result-category {
|
||||
font-size: 12px;
|
||||
color: #646970;
|
||||
padding: 2px 8px;
|
||||
background: #f0f0f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mlf-result-badges {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mlf-badge {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mlf-badge-variable {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.mlf-result-preview {
|
||||
font-size: 22px;
|
||||
color: #50575e;
|
||||
line-height: 1.3;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.mlf-no-results {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: #646970;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Selected Font */
|
||||
.mlf-selected-font {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
background: #f0f6fc;
|
||||
border: 1px solid #c5d9ed;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mlf-file-count {
|
||||
.mlf-selected-font-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #c5d9ed;
|
||||
}
|
||||
|
||||
.mlf-selected-label {
|
||||
color: #646970;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mlf-selected-name {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: #1d2327;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mlf-change-font {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #2271b1;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.mlf-file-count strong {
|
||||
font-size: 1.1em;
|
||||
.mlf-change-font:hover {
|
||||
color: #135e96;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Font Preview */
|
||||
.mlf-preview-section {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #dcdcde;
|
||||
.mlf-italic-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mlf-preview-box {
|
||||
/* Italic Toggle */
|
||||
.mlf-italic-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #dcdcde;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
min-height: 100px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
font-weight: normal !important;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.mlf-preview-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.mlf-italic-toggle:hover {
|
||||
background: #f6f7f7;
|
||||
}
|
||||
|
||||
.mlf-preview-sample {
|
||||
display: block;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.mlf-preview-heading {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.mlf-preview-paragraph {
|
||||
font-size: 16px;
|
||||
color: #50575e;
|
||||
}
|
||||
|
||||
.mlf-preview-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #646970;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.mlf-preview-loading .spinner {
|
||||
float: none;
|
||||
.mlf-italic-toggle input[type="checkbox"] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mlf-preview-error {
|
||||
color: #b32d2e;
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Submit Row */
|
||||
.mlf-form-row-submit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.mlf-selected-font .mlf-form-row-submit {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mlf-form-row-submit .spinner {
|
||||
|
|
@ -189,6 +291,28 @@
|
|||
color: #721c24;
|
||||
}
|
||||
|
||||
/* Info Note */
|
||||
.mlf-info-note {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-top: 20px;
|
||||
padding: 12px 16px;
|
||||
background: #f0f6fc;
|
||||
border: 1px solid #c5d9ed;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: #1d2327;
|
||||
}
|
||||
|
||||
.mlf-info-note .dashicons {
|
||||
color: #2271b1;
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
/* Font List */
|
||||
.mlf-font-list {
|
||||
display: flex;
|
||||
|
|
@ -245,10 +369,11 @@
|
|||
.mlf-no-fonts {
|
||||
color: #646970;
|
||||
font-style: italic;
|
||||
padding: 20px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
background: #f6f7f7;
|
||||
border-radius: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Info Box */
|
||||
|
|
@ -264,12 +389,18 @@
|
|||
|
||||
.mlf-info-box .dashicons {
|
||||
color: #2271b1;
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.mlf-info-box p {
|
||||
margin: 0;
|
||||
color: #1d2327;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.mlf-info-box p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mlf-info-box a {
|
||||
|
|
@ -332,10 +463,6 @@
|
|||
|
||||
/* Responsive */
|
||||
@media screen and (max-width: 782px) {
|
||||
.mlf-checkbox-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.mlf-font-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
|
@ -345,10 +472,12 @@
|
|||
.mlf-font-actions {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.mlf-checkbox-grid {
|
||||
grid-template-columns: 1fr;
|
||||
.mlf-result-info {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mlf-selected-font-header {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,182 +9,304 @@
|
|||
|
||||
var MLF = {
|
||||
/**
|
||||
* Preview debounce timer.
|
||||
* Search debounce timer.
|
||||
*/
|
||||
previewTimer: null,
|
||||
searchTimer: null,
|
||||
|
||||
/**
|
||||
* Currently loaded preview font.
|
||||
* Currently selected font.
|
||||
*/
|
||||
currentPreviewFont: null,
|
||||
selectedFont: null,
|
||||
|
||||
/**
|
||||
* Loaded font preview stylesheets.
|
||||
*/
|
||||
loadedFonts: {},
|
||||
|
||||
/**
|
||||
* Initialize the admin functionality.
|
||||
*/
|
||||
init: function() {
|
||||
this.bindEvents();
|
||||
this.updateFileCount();
|
||||
},
|
||||
|
||||
/**
|
||||
* Bind event handlers.
|
||||
*/
|
||||
bindEvents: function() {
|
||||
// Search input
|
||||
$('#mlf-font-search').on('input', this.handleSearchInput.bind(this));
|
||||
|
||||
// Click outside to close search results
|
||||
$(document).on('click', this.handleDocumentClick.bind(this));
|
||||
|
||||
// Prevent closing when clicking inside search area
|
||||
$('.mlf-import-section').on('click', function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// Font selection from results
|
||||
$(document).on('click', '.mlf-result-item', this.handleFontSelect.bind(this));
|
||||
|
||||
// Change font button
|
||||
$('#mlf-change-font').on('click', this.handleChangeFont.bind(this));
|
||||
|
||||
// Form submission
|
||||
$('#mlf-import-form').on('submit', this.handleDownload.bind(this));
|
||||
|
||||
// Delete button clicks
|
||||
$(document).on('click', '.mlf-delete-btn', this.handleDelete.bind(this));
|
||||
|
||||
// Update file count on checkbox change
|
||||
$('#mlf-import-form').on('change', 'input[type="checkbox"]', this.updateFileCount.bind(this));
|
||||
|
||||
// Font name input for preview (debounced)
|
||||
$('#mlf-font-name').on('input', this.handleFontNameInput.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle font name input for preview (debounced).
|
||||
* Handle search input (debounced).
|
||||
*
|
||||
* @param {Event} e Input event.
|
||||
*/
|
||||
handleFontNameInput: function(e) {
|
||||
var fontName = $(e.target).val().trim();
|
||||
handleSearchInput: function(e) {
|
||||
var query = $(e.target).val().trim();
|
||||
|
||||
// Clear previous timer
|
||||
if (this.previewTimer) {
|
||||
clearTimeout(this.previewTimer);
|
||||
if (this.searchTimer) {
|
||||
clearTimeout(this.searchTimer);
|
||||
}
|
||||
|
||||
// Hide preview if empty
|
||||
if (!fontName) {
|
||||
this.hidePreview();
|
||||
// Hide results if query too short
|
||||
if (query.length < 2) {
|
||||
this.hideSearchResults();
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate font name (only allowed characters)
|
||||
if (!/^[a-zA-Z0-9\s\-]+$/.test(fontName)) {
|
||||
this.hidePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
// Debounce: wait 500ms before loading preview
|
||||
this.previewTimer = setTimeout(function() {
|
||||
MLF.loadFontPreview(fontName);
|
||||
}, 500);
|
||||
// Debounce: wait 300ms before searching
|
||||
this.searchTimer = setTimeout(function() {
|
||||
MLF.performSearch(query);
|
||||
}, 300);
|
||||
},
|
||||
|
||||
/**
|
||||
* Load font preview from Google Fonts.
|
||||
* Perform the search.
|
||||
*
|
||||
* @param {string} fontName Font family name.
|
||||
* @param {string} query Search query.
|
||||
*/
|
||||
loadFontPreview: function(fontName) {
|
||||
var $section = $('#mlf-preview-section');
|
||||
var $text = $('#mlf-preview-text');
|
||||
var $loading = $('#mlf-preview-loading');
|
||||
var $error = $('#mlf-preview-error');
|
||||
performSearch: function(query) {
|
||||
var $spinner = $('#mlf-search-spinner');
|
||||
var $results = $('#mlf-search-results');
|
||||
var $list = $('#mlf-results-list');
|
||||
|
||||
// Skip if same font already loaded
|
||||
if (this.currentPreviewFont === fontName) {
|
||||
// Show spinner
|
||||
$spinner.addClass('is-active');
|
||||
|
||||
// Send AJAX request
|
||||
$.ajax({
|
||||
url: mapleLocalFontsData.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'mlf_search_fonts',
|
||||
nonce: mapleLocalFontsData.searchNonce,
|
||||
query: query
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success && response.data.fonts) {
|
||||
MLF.displaySearchResults(response.data.fonts);
|
||||
} else {
|
||||
MLF.displayNoResults();
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
MLF.displayNoResults();
|
||||
},
|
||||
complete: function() {
|
||||
$spinner.removeClass('is-active');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Display search results.
|
||||
*
|
||||
* @param {Array} fonts Array of font objects.
|
||||
*/
|
||||
displaySearchResults: function(fonts) {
|
||||
var $results = $('#mlf-search-results');
|
||||
var $list = $('#mlf-results-list');
|
||||
|
||||
if (fonts.length === 0) {
|
||||
this.displayNoResults();
|
||||
return;
|
||||
}
|
||||
|
||||
// Show section with loading state
|
||||
$section.show();
|
||||
$text.hide();
|
||||
$loading.show();
|
||||
$error.hide();
|
||||
var html = '';
|
||||
var previewText = mapleLocalFontsData.strings.previewText || 'Maple Fonts Preview';
|
||||
|
||||
// Remove previous preview font link
|
||||
$('#mlf-preview-font-link').remove();
|
||||
fonts.forEach(function(font) {
|
||||
var fontFamily = font.family;
|
||||
var category = font.category || 'sans-serif';
|
||||
var categoryLabel = MLF.getCategoryLabel(category);
|
||||
var badges = [];
|
||||
|
||||
// Build Google Fonts URL for preview (just 400 weight for preview)
|
||||
var fontFamily = fontName.replace(/\s+/g, '+');
|
||||
var previewUrl = 'https://fonts.googleapis.com/css2?family=' + encodeURIComponent(fontFamily) + ':wght@400&display=swap';
|
||||
if (font.has_variable) {
|
||||
badges.push('<span class="mlf-badge mlf-badge-variable">Variable</span>');
|
||||
}
|
||||
|
||||
// Create link element
|
||||
html += '<div class="mlf-result-item" data-font-family="' + MLF.escapeHtml(fontFamily) + '">';
|
||||
html += ' <div class="mlf-result-info">';
|
||||
html += ' <span class="mlf-result-name">' + MLF.escapeHtml(fontFamily) + '</span>';
|
||||
html += ' <span class="mlf-result-category">' + MLF.escapeHtml(categoryLabel) + '</span>';
|
||||
if (badges.length > 0) {
|
||||
html += ' <span class="mlf-result-badges">' + badges.join('') + '</span>';
|
||||
}
|
||||
html += ' </div>';
|
||||
html += ' <div class="mlf-result-preview" style="font-family: \'' + MLF.escapeHtml(fontFamily) + '\', ' + category + ';">';
|
||||
html += MLF.escapeHtml(previewText);
|
||||
html += ' </div>';
|
||||
html += '</div>';
|
||||
|
||||
// Load font for preview
|
||||
MLF.loadFontPreview(fontFamily);
|
||||
});
|
||||
|
||||
$list.html(html);
|
||||
$results.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Display no results message.
|
||||
*/
|
||||
displayNoResults: function() {
|
||||
var $results = $('#mlf-search-results');
|
||||
var $list = $('#mlf-results-list');
|
||||
var message = mapleLocalFontsData.strings.noResults || 'No fonts found.';
|
||||
|
||||
$list.html('<div class="mlf-no-results">' + MLF.escapeHtml(message) + '</div>');
|
||||
$results.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide search results.
|
||||
*/
|
||||
hideSearchResults: function() {
|
||||
$('#mlf-search-results').hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Maximum number of font previews to load.
|
||||
*/
|
||||
maxLoadedFonts: 50,
|
||||
|
||||
/**
|
||||
* Load a font for preview from Google Fonts.
|
||||
*
|
||||
* @param {string} fontFamily Font family name.
|
||||
*/
|
||||
loadFontPreview: function(fontFamily) {
|
||||
// Skip if already loaded
|
||||
if (this.loadedFonts[fontFamily]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Limit number of loaded fonts to prevent memory accumulation
|
||||
if (Object.keys(this.loadedFonts).length >= this.maxLoadedFonts) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark as loading
|
||||
this.loadedFonts[fontFamily] = true;
|
||||
|
||||
// Create Google Fonts link
|
||||
var fontUrl = 'https://fonts.googleapis.com/css2?family=' +
|
||||
encodeURIComponent(fontFamily.replace(/ /g, '+')) +
|
||||
':wght@400&display=swap';
|
||||
|
||||
// Create and append link element
|
||||
var $link = $('<link>', {
|
||||
id: 'mlf-preview-font-link',
|
||||
rel: 'stylesheet',
|
||||
href: previewUrl
|
||||
href: fontUrl
|
||||
});
|
||||
|
||||
// Handle load success
|
||||
$link.on('load', function() {
|
||||
MLF.currentPreviewFont = fontName;
|
||||
$text.css('font-family', '"' + fontName + '", sans-serif');
|
||||
$loading.hide();
|
||||
$text.show();
|
||||
});
|
||||
|
||||
// Handle load error
|
||||
$link.on('error', function() {
|
||||
MLF.currentPreviewFont = null;
|
||||
$loading.hide();
|
||||
$error.show();
|
||||
});
|
||||
|
||||
// Append to head
|
||||
$('head').append($link);
|
||||
|
||||
// Fallback timeout (5 seconds)
|
||||
setTimeout(function() {
|
||||
if ($loading.is(':visible')) {
|
||||
// Check if font actually loaded by measuring text width
|
||||
var testSpan = $('<span>').text('test').css({
|
||||
'font-family': '"' + fontName + '", monospace',
|
||||
'position': 'absolute',
|
||||
'visibility': 'hidden'
|
||||
}).appendTo('body');
|
||||
|
||||
var testWidth = testSpan.width();
|
||||
|
||||
var fallbackSpan = $('<span>').text('test').css({
|
||||
'font-family': 'monospace',
|
||||
'position': 'absolute',
|
||||
'visibility': 'hidden'
|
||||
}).appendTo('body');
|
||||
|
||||
var fallbackWidth = fallbackSpan.width();
|
||||
|
||||
testSpan.remove();
|
||||
fallbackSpan.remove();
|
||||
|
||||
if (testWidth !== fallbackWidth) {
|
||||
// Font loaded successfully
|
||||
MLF.currentPreviewFont = fontName;
|
||||
$text.css('font-family', '"' + fontName + '", sans-serif');
|
||||
$loading.hide();
|
||||
$text.show();
|
||||
} else {
|
||||
// Font failed to load
|
||||
MLF.currentPreviewFont = null;
|
||||
$loading.hide();
|
||||
$error.show();
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide the font preview section.
|
||||
* Handle font selection.
|
||||
*
|
||||
* @param {Event} e Click event.
|
||||
*/
|
||||
hidePreview: function() {
|
||||
this.currentPreviewFont = null;
|
||||
$('#mlf-preview-section').hide();
|
||||
$('#mlf-preview-font-link').remove();
|
||||
handleFontSelect: function(e) {
|
||||
var $item = $(e.currentTarget);
|
||||
var fontFamily = $item.data('font-family');
|
||||
|
||||
// Store selected font
|
||||
this.selectedFont = fontFamily;
|
||||
|
||||
// Update UI
|
||||
$('#mlf-font-name').val(fontFamily);
|
||||
$('#mlf-selected-name').text(fontFamily);
|
||||
|
||||
// Hide search, show selected font panel
|
||||
this.hideSearchResults();
|
||||
$('#mlf-font-search').closest('.mlf-form-row').hide();
|
||||
$('#mlf-selected-font').show();
|
||||
|
||||
// Clear search input
|
||||
$('#mlf-font-search').val('');
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the file count display.
|
||||
* Handle change font button.
|
||||
*
|
||||
* @param {Event} e Click event.
|
||||
*/
|
||||
updateFileCount: function() {
|
||||
var weights = $('input[name="weights[]"]:checked').length;
|
||||
var styles = $('input[name="styles[]"]:checked').length;
|
||||
var count = weights * styles;
|
||||
handleChangeFont: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
$('#mlf-file-count').text(count);
|
||||
// Clear selection
|
||||
this.selectedFont = null;
|
||||
$('#mlf-font-name').val('');
|
||||
|
||||
// Show search, hide selected font panel
|
||||
$('#mlf-selected-font').hide();
|
||||
$('#mlf-font-search').closest('.mlf-form-row').show();
|
||||
$('#mlf-font-search').focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle document click (close search results).
|
||||
*
|
||||
* @param {Event} e Click event.
|
||||
*/
|
||||
handleDocumentClick: function(e) {
|
||||
if (!$(e.target).closest('.mlf-import-section').length) {
|
||||
this.hideSearchResults();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get human-readable category label.
|
||||
*
|
||||
* @param {string} category Category slug.
|
||||
* @return {string} Category label.
|
||||
*/
|
||||
getCategoryLabel: function(category) {
|
||||
var labels = {
|
||||
'sans-serif': 'Sans Serif',
|
||||
'serif': 'Serif',
|
||||
'display': 'Display',
|
||||
'handwriting': 'Handwriting',
|
||||
'monospace': 'Monospace'
|
||||
};
|
||||
return labels[category] || category;
|
||||
},
|
||||
|
||||
/**
|
||||
* Escape HTML entities.
|
||||
*
|
||||
* @param {string} str String to escape.
|
||||
* @return {string} Escaped string.
|
||||
*/
|
||||
escapeHtml: function(str) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -202,31 +324,17 @@
|
|||
|
||||
// Get form values
|
||||
var fontName = $('#mlf-font-name').val().trim();
|
||||
var weights = [];
|
||||
var styles = [];
|
||||
|
||||
$('input[name="weights[]"]:checked').each(function() {
|
||||
weights.push($(this).val());
|
||||
});
|
||||
|
||||
$('input[name="styles[]"]:checked').each(function() {
|
||||
styles.push($(this).val());
|
||||
});
|
||||
var includeItalic = $('#mlf-include-italic').is(':checked') ? '1' : '0';
|
||||
|
||||
// Validate
|
||||
if (!fontName) {
|
||||
this.showMessage($message, mapleLocalFontsData.strings.enterFontName, 'error');
|
||||
this.showMessage($message, mapleLocalFontsData.strings.selectFont || 'Please select a font.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (weights.length === 0) {
|
||||
this.showMessage($message, mapleLocalFontsData.strings.selectWeight, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (styles.length === 0) {
|
||||
this.showMessage($message, mapleLocalFontsData.strings.selectStyle, 'error');
|
||||
return;
|
||||
// Store original button text
|
||||
if (!$button.data('original-text')) {
|
||||
$button.data('original-text', $button.text());
|
||||
}
|
||||
|
||||
// Disable form
|
||||
|
|
@ -243,8 +351,7 @@
|
|||
action: 'mlf_download_font',
|
||||
nonce: mapleLocalFontsData.downloadNonce,
|
||||
font_name: fontName,
|
||||
weights: weights,
|
||||
styles: styles
|
||||
include_italic: includeItalic
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
|
|
@ -257,20 +364,19 @@
|
|||
MLF.showMessage($message, response.data.message || mapleLocalFontsData.strings.error, 'error');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
MLF.showMessage($message, mapleLocalFontsData.strings.error, 'error');
|
||||
error: function(xhr) {
|
||||
var message = mapleLocalFontsData.strings.error;
|
||||
if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
|
||||
message = xhr.responseJSON.data.message;
|
||||
}
|
||||
MLF.showMessage($message, message, 'error');
|
||||
},
|
||||
complete: function() {
|
||||
$form.removeClass('mlf-loading');
|
||||
$button.prop('disabled', false).text($button.data('original-text') || 'Download & Install');
|
||||
$button.prop('disabled', false).text($button.data('original-text'));
|
||||
$spinner.removeClass('is-active');
|
||||
}
|
||||
});
|
||||
|
||||
// Store original button text
|
||||
if (!$button.data('original-text')) {
|
||||
$button.data('original-text', $button.text());
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -290,6 +396,9 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// Store original button text
|
||||
var originalText = $button.text();
|
||||
|
||||
// Disable button
|
||||
$button.prop('disabled', true).text(mapleLocalFontsData.strings.deleting);
|
||||
$fontItem.addClass('mlf-loading');
|
||||
|
|
@ -311,20 +420,24 @@
|
|||
|
||||
// Check if any fonts remain
|
||||
if ($('.mlf-font-item').length === 0) {
|
||||
$('#mlf-font-list').html(
|
||||
'<p class="mlf-no-fonts">No fonts installed yet.</p>'
|
||||
$('#mlf-font-list').replaceWith(
|
||||
'<p class="mlf-no-fonts">No fonts installed yet. Search and select a font above to get started.</p>'
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alert(response.data.message || mapleLocalFontsData.strings.error);
|
||||
$button.prop('disabled', false).text('Delete');
|
||||
$button.prop('disabled', false).text(originalText);
|
||||
$fontItem.removeClass('mlf-loading');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert(mapleLocalFontsData.strings.error);
|
||||
$button.prop('disabled', false).text('Delete');
|
||||
error: function(xhr) {
|
||||
var message = mapleLocalFontsData.strings.error;
|
||||
if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
|
||||
message = xhr.responseJSON.data.message;
|
||||
}
|
||||
alert(message);
|
||||
$button.prop('disabled', false).text(originalText);
|
||||
$fontItem.removeClass('mlf-loading');
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,23 +16,6 @@ if (!defined('ABSPATH')) {
|
|||
*/
|
||||
class MLF_Admin_Page {
|
||||
|
||||
/**
|
||||
* Available font weights.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $weights = [
|
||||
100 => 'Thin',
|
||||
200 => 'Extra Light',
|
||||
300 => 'Light',
|
||||
400 => 'Regular',
|
||||
500 => 'Medium',
|
||||
600 => 'Semi Bold',
|
||||
700 => 'Bold',
|
||||
800 => 'Extra Bold',
|
||||
900 => 'Black',
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
|
|
@ -53,7 +36,8 @@ class MLF_Admin_Page {
|
|||
$installed_fonts = $registry->get_imported_fonts();
|
||||
?>
|
||||
<div class="wrap mlf-wrap">
|
||||
<h1><?php esc_html_e('Maple Local Fonts', 'maple-local-fonts'); ?></h1>
|
||||
<h1><?php esc_html_e('Maple Fonts', 'maple-local-fonts'); ?></h1>
|
||||
<p class="mlf-description"><?php esc_html_e('Import Google Fonts to your local server for privacy-friendly, GDPR-compliant typography.', 'maple-local-fonts'); ?></p>
|
||||
|
||||
<div class="mlf-container">
|
||||
<!-- Import Section -->
|
||||
|
|
@ -61,62 +45,45 @@ class MLF_Admin_Page {
|
|||
<h2><?php esc_html_e('Import from Google Fonts', 'maple-local-fonts'); ?></h2>
|
||||
|
||||
<form id="mlf-import-form" class="mlf-form">
|
||||
<!-- Search Input -->
|
||||
<div class="mlf-form-row">
|
||||
<label for="mlf-font-name"><?php esc_html_e('Font Name', 'maple-local-fonts'); ?></label>
|
||||
<input type="text" id="mlf-font-name" name="font_name" placeholder="<?php esc_attr_e('e.g., Open Sans', 'maple-local-fonts'); ?>" required />
|
||||
<p class="description"><?php esc_html_e('Enter the exact font name as it appears on Google Fonts.', 'maple-local-fonts'); ?></p>
|
||||
<label for="mlf-font-search"><?php esc_html_e('Search Fonts', 'maple-local-fonts'); ?></label>
|
||||
<div class="mlf-search-wrapper">
|
||||
<span class="dashicons dashicons-search mlf-search-icon"></span>
|
||||
<input type="text"
|
||||
id="mlf-font-search"
|
||||
class="mlf-search-input"
|
||||
placeholder="<?php esc_attr_e('Search Google Fonts...', 'maple-local-fonts'); ?>"
|
||||
autocomplete="off" />
|
||||
<span class="spinner mlf-search-spinner" id="mlf-search-spinner"></span>
|
||||
</div>
|
||||
<p class="description"><?php esc_html_e('Type at least 2 characters to search.', 'maple-local-fonts'); ?></p>
|
||||
</div>
|
||||
|
||||
<div class="mlf-form-row">
|
||||
<label><?php esc_html_e('Weights', 'maple-local-fonts'); ?></label>
|
||||
<div class="mlf-checkbox-grid">
|
||||
<?php foreach ($this->weights as $weight => $label) : ?>
|
||||
<label class="mlf-checkbox-label">
|
||||
<input type="checkbox" name="weights[]" value="<?php echo esc_attr($weight); ?>" <?php checked(in_array($weight, [400, 700], true)); ?> />
|
||||
<span><?php echo esc_html($weight); ?> (<?php echo esc_html($label); ?>)</span>
|
||||
<!-- Search Results -->
|
||||
<div class="mlf-search-results" id="mlf-search-results" style="display: none;">
|
||||
<div class="mlf-results-list" id="mlf-results-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Selected Font (hidden until a font is selected) -->
|
||||
<div class="mlf-selected-font" id="mlf-selected-font" style="display: none;">
|
||||
<div class="mlf-selected-font-header">
|
||||
<span class="mlf-selected-label"><?php esc_html_e('Selected Font:', 'maple-local-fonts'); ?></span>
|
||||
<span class="mlf-selected-name" id="mlf-selected-name"></span>
|
||||
<button type="button" class="mlf-change-font" id="mlf-change-font">
|
||||
<?php esc_html_e('Change', 'maple-local-fonts'); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Hidden input for font name -->
|
||||
<input type="hidden" id="mlf-font-name" name="font_name" value="" />
|
||||
|
||||
<div class="mlf-form-row mlf-italic-row">
|
||||
<label class="mlf-checkbox-label mlf-italic-toggle">
|
||||
<input type="checkbox" name="include_italic" id="mlf-include-italic" value="1" checked />
|
||||
<span><?php esc_html_e('Include Italic styles', 'maple-local-fonts'); ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mlf-form-row">
|
||||
<label><?php esc_html_e('Styles', 'maple-local-fonts'); ?></label>
|
||||
<div class="mlf-checkbox-grid mlf-checkbox-grid-small">
|
||||
<label class="mlf-checkbox-label">
|
||||
<input type="checkbox" name="styles[]" value="normal" checked />
|
||||
<span><?php esc_html_e('Normal', 'maple-local-fonts'); ?></span>
|
||||
</label>
|
||||
<label class="mlf-checkbox-label">
|
||||
<input type="checkbox" name="styles[]" value="italic" />
|
||||
<span><?php esc_html_e('Italic', 'maple-local-fonts'); ?></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mlf-form-row mlf-form-row-info">
|
||||
<span class="mlf-file-count">
|
||||
<?php esc_html_e('Files to download:', 'maple-local-fonts'); ?>
|
||||
<strong id="mlf-file-count">2</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Font Preview Section -->
|
||||
<div class="mlf-form-row mlf-preview-section" id="mlf-preview-section" style="display: none;">
|
||||
<label><?php esc_html_e('Preview', 'maple-local-fonts'); ?></label>
|
||||
<div class="mlf-preview-box" id="mlf-preview-box">
|
||||
<div class="mlf-preview-text" id="mlf-preview-text">
|
||||
<span class="mlf-preview-sample mlf-preview-heading"><?php esc_html_e('The quick brown fox jumps over the lazy dog', 'maple-local-fonts'); ?></span>
|
||||
<span class="mlf-preview-sample mlf-preview-paragraph"><?php esc_html_e('ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789', 'maple-local-fonts'); ?></span>
|
||||
</div>
|
||||
<div class="mlf-preview-loading" id="mlf-preview-loading" style="display: none;">
|
||||
<span class="spinner is-active"></span>
|
||||
<span><?php esc_html_e('Loading preview...', 'maple-local-fonts'); ?></span>
|
||||
</div>
|
||||
<div class="mlf-preview-error" id="mlf-preview-error" style="display: none;">
|
||||
<?php esc_html_e('Could not load font preview. The font may not exist on Google Fonts.', 'maple-local-fonts'); ?>
|
||||
</div>
|
||||
</div>
|
||||
<p class="description"><?php esc_html_e('Preview is loaded directly from Google Fonts. After installation, the font will be served locally.', 'maple-local-fonts'); ?></p>
|
||||
<p class="description"><?php esc_html_e('Italic styles are useful for emphasized text. Uncheck to reduce download size.', 'maple-local-fonts'); ?></p>
|
||||
</div>
|
||||
|
||||
<div class="mlf-form-row mlf-form-row-submit">
|
||||
|
|
@ -125,9 +92,15 @@ class MLF_Admin_Page {
|
|||
</button>
|
||||
<span class="spinner" id="mlf-spinner"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mlf-message" class="mlf-message" style="display: none;"></div>
|
||||
</form>
|
||||
|
||||
<div class="mlf-info-note">
|
||||
<span class="dashicons dashicons-info-outline"></span>
|
||||
<span><?php esc_html_e('All font weights (100-900) will be downloaded automatically. Variable fonts are used when available for better performance.', 'maple-local-fonts'); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Installed Fonts Section -->
|
||||
|
|
@ -135,7 +108,7 @@ class MLF_Admin_Page {
|
|||
<h2><?php esc_html_e('Installed Fonts', 'maple-local-fonts'); ?></h2>
|
||||
|
||||
<?php if (empty($installed_fonts)) : ?>
|
||||
<p class="mlf-no-fonts"><?php esc_html_e('No fonts installed yet.', 'maple-local-fonts'); ?></p>
|
||||
<p class="mlf-no-fonts"><?php esc_html_e('No fonts installed yet. Search and select a font above to get started.', 'maple-local-fonts'); ?></p>
|
||||
<?php else : ?>
|
||||
<div id="mlf-font-list" class="mlf-font-list">
|
||||
<?php foreach ($installed_fonts as $font) : ?>
|
||||
|
|
@ -167,20 +140,23 @@ class MLF_Admin_Page {
|
|||
<div class="mlf-section mlf-info-section">
|
||||
<?php if (wp_is_block_theme()) : ?>
|
||||
<div class="mlf-info-box">
|
||||
<span class="dashicons dashicons-info"></span>
|
||||
<span class="dashicons dashicons-editor-textcolor"></span>
|
||||
<div>
|
||||
<p><strong><?php esc_html_e('How to use your fonts', 'maple-local-fonts'); ?></strong></p>
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: link to WordPress Editor */
|
||||
esc_html__('Use %s to apply fonts to your site.', 'maple-local-fonts'),
|
||||
esc_html__('Go to %s to apply fonts to your site.', 'maple-local-fonts'),
|
||||
'<a href="' . esc_url(admin_url('site-editor.php?path=%2Fwp_global_styles')) . '">' . esc_html__('Appearance → Editor → Styles → Typography', 'maple-local-fonts') . '</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="mlf-info-box mlf-info-box-classic">
|
||||
<span class="dashicons dashicons-info"></span>
|
||||
<span class="dashicons dashicons-editor-textcolor"></span>
|
||||
<div class="mlf-classic-theme-info">
|
||||
<p><strong><?php esc_html_e('Classic Theme Detected', 'maple-local-fonts'); ?></strong></p>
|
||||
<p><?php esc_html_e('Your theme does not support the Full Site Editor. To use imported fonts, add custom CSS to your theme:', 'maple-local-fonts'); ?></p>
|
||||
|
|
|
|||
|
|
@ -74,36 +74,13 @@ class MLF_Ajax_Handler {
|
|||
wp_send_json_error(['message' => __('Font name is too long.', 'maple-local-fonts')]);
|
||||
}
|
||||
|
||||
// Validate weights
|
||||
$weights = isset($_POST['weights']) ? array_map('absint', (array) $_POST['weights']) : [];
|
||||
$allowed_weights = [100, 200, 300, 400, 500, 600, 700, 800, 900];
|
||||
$weights = array_intersect($weights, $allowed_weights);
|
||||
|
||||
if (empty($weights)) {
|
||||
wp_send_json_error(['message' => __('At least one weight is required.', 'maple-local-fonts')]);
|
||||
}
|
||||
|
||||
if (count($weights) > MLF_MAX_WEIGHTS_PER_FONT) {
|
||||
wp_send_json_error(['message' => __('Too many weights selected.', 'maple-local-fonts')]);
|
||||
}
|
||||
|
||||
// Validate styles
|
||||
$styles = isset($_POST['styles']) ? (array) $_POST['styles'] : [];
|
||||
$allowed_styles = ['normal', 'italic'];
|
||||
// Sanitize each style value before filtering
|
||||
$styles = array_map('sanitize_text_field', $styles);
|
||||
$styles = array_filter($styles, function($style) use ($allowed_styles) {
|
||||
return in_array($style, $allowed_styles, true);
|
||||
});
|
||||
|
||||
if (empty($styles)) {
|
||||
wp_send_json_error(['message' => __('At least one style is required.', 'maple-local-fonts')]);
|
||||
}
|
||||
// Validate include_italic (boolean)
|
||||
$include_italic = isset($_POST['include_italic']) && $_POST['include_italic'] === '1';
|
||||
|
||||
// 5. PROCESS REQUEST
|
||||
try {
|
||||
$downloader = new MLF_Font_Downloader();
|
||||
$download_result = $downloader->download($font_name, $weights, $styles);
|
||||
$download_result = $downloader->download($font_name, $include_italic);
|
||||
|
||||
if (is_wp_error($download_result)) {
|
||||
wp_send_json_error(['message' => $this->get_user_error_message($download_result)]);
|
||||
|
|
@ -214,12 +191,12 @@ class MLF_Ajax_Handler {
|
|||
'invalid_path' => __('Invalid file path.', 'maple-local-fonts'),
|
||||
'invalid_url' => __('Invalid font URL.', 'maple-local-fonts'),
|
||||
'invalid_name' => __('Invalid font name.', 'maple-local-fonts'),
|
||||
'invalid_weights' => __('No valid weights specified.', 'maple-local-fonts'),
|
||||
'invalid_styles' => __('No valid styles specified.', 'maple-local-fonts'),
|
||||
'not_found' => __('Font not found.', 'maple-local-fonts'),
|
||||
'not_ours' => __('Cannot delete fonts not imported by this plugin.', 'maple-local-fonts'),
|
||||
'response_too_large' => __('The font data is too large to process. Please try selecting fewer weights.', 'maple-local-fonts'),
|
||||
'response_too_large' => __('The font data is too large to process.', 'maple-local-fonts'),
|
||||
'file_too_large' => __('The font file is too large to download.', 'maple-local-fonts'),
|
||||
'no_variable' => __('Variable font not available, trying static fonts...', 'maple-local-fonts'),
|
||||
'no_fonts' => __('No font files found. The font may not support the requested styles.', 'maple-local-fonts'),
|
||||
];
|
||||
|
||||
return $messages[$code] ?? __('An unexpected error occurred. Please try again.', 'maple-local-fonts');
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ if (!defined('ABSPATH')) {
|
|||
* Class MLF_Font_Downloader
|
||||
*
|
||||
* Handles downloading fonts from Google Fonts CSS2 API.
|
||||
* Attempts variable fonts first, falls back to static fonts.
|
||||
*/
|
||||
class MLF_Font_Downloader {
|
||||
|
||||
|
|
@ -23,38 +24,150 @@ class MLF_Font_Downloader {
|
|||
*/
|
||||
private $user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
||||
|
||||
/**
|
||||
* All available font weights.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $all_weights = [100, 200, 300, 400, 500, 600, 700, 800, 900];
|
||||
|
||||
/**
|
||||
* Download a font from Google Fonts.
|
||||
*
|
||||
* Attempts variable font first, falls back to static if not available.
|
||||
*
|
||||
* @param string $font_name Font family name.
|
||||
* @param array $weights Weights to download.
|
||||
* @param array $styles Styles to download.
|
||||
* @param bool $include_italic Whether to include italic styles.
|
||||
* @return array|WP_Error Download result or error.
|
||||
*/
|
||||
public function download($font_name, $weights, $styles) {
|
||||
// Validate inputs
|
||||
public function download($font_name, $include_italic = true) {
|
||||
// Validate font name
|
||||
if (empty($font_name) || !preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) {
|
||||
return new WP_Error('invalid_name', 'Invalid font name');
|
||||
}
|
||||
|
||||
$weights = array_intersect(array_map('absint', $weights), [100, 200, 300, 400, 500, 600, 700, 800, 900]);
|
||||
if (empty($weights)) {
|
||||
return new WP_Error('invalid_weights', 'No valid weights specified');
|
||||
if (strlen($font_name) > 100) {
|
||||
return new WP_Error('invalid_name', 'Font name too long');
|
||||
}
|
||||
|
||||
$styles = array_intersect($styles, ['normal', 'italic']);
|
||||
if (empty($styles)) {
|
||||
return new WP_Error('invalid_styles', 'No valid styles specified');
|
||||
// Try variable font first
|
||||
$result = $this->try_variable_font($font_name, $include_italic);
|
||||
|
||||
if (!is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Fall back to static fonts
|
||||
return $this->download_static_fonts($font_name, $include_italic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to download variable font.
|
||||
*
|
||||
* @param string $font_name Font family name.
|
||||
* @param bool $include_italic Whether to include italic styles.
|
||||
* @return array|WP_Error Download result or error.
|
||||
*/
|
||||
private function try_variable_font($font_name, $include_italic) {
|
||||
$font_slug = sanitize_title($font_name);
|
||||
$downloaded = [];
|
||||
|
||||
// Try to fetch variable font CSS (roman/upright)
|
||||
$css = $this->fetch_variable_css($font_name, false);
|
||||
|
||||
if (is_wp_error($css)) {
|
||||
return $css;
|
||||
}
|
||||
|
||||
// Parse and download roman variable font
|
||||
$roman_faces = $this->parse_variable_css($css, $font_name);
|
||||
|
||||
if (is_wp_error($roman_faces) || empty($roman_faces)) {
|
||||
return new WP_Error('no_variable', 'Variable font not available');
|
||||
}
|
||||
|
||||
// Download roman variable font file(s)
|
||||
foreach ($roman_faces as $face) {
|
||||
$result = $this->download_single_file(
|
||||
$face['url'],
|
||||
$font_slug,
|
||||
$face['weight'],
|
||||
'normal',
|
||||
true // is_variable
|
||||
);
|
||||
|
||||
if (!is_wp_error($result)) {
|
||||
$downloaded[] = [
|
||||
'path' => $result,
|
||||
'weight' => $face['weight'],
|
||||
'style' => 'normal',
|
||||
'is_variable' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Try italic variable font if requested
|
||||
if ($include_italic) {
|
||||
$italic_css = $this->fetch_variable_css($font_name, true);
|
||||
|
||||
if (!is_wp_error($italic_css)) {
|
||||
$italic_faces = $this->parse_variable_css($italic_css, $font_name);
|
||||
|
||||
if (!is_wp_error($italic_faces) && !empty($italic_faces)) {
|
||||
foreach ($italic_faces as $face) {
|
||||
$result = $this->download_single_file(
|
||||
$face['url'],
|
||||
$font_slug,
|
||||
$face['weight'],
|
||||
'italic',
|
||||
true
|
||||
);
|
||||
|
||||
if (!is_wp_error($result)) {
|
||||
$downloaded[] = [
|
||||
'path' => $result,
|
||||
'weight' => $face['weight'],
|
||||
'style' => 'italic',
|
||||
'is_variable' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($downloaded)) {
|
||||
return new WP_Error('download_failed', 'Could not download variable font files');
|
||||
}
|
||||
|
||||
return [
|
||||
'font_name' => $font_name,
|
||||
'font_slug' => $font_slug,
|
||||
'files' => $downloaded,
|
||||
'is_variable' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Download static fonts (fallback when variable not available).
|
||||
*
|
||||
* @param string $font_name Font family name.
|
||||
* @param bool $include_italic Whether to include italic styles.
|
||||
* @return array|WP_Error Download result or error.
|
||||
*/
|
||||
private function download_static_fonts($font_name, $include_italic) {
|
||||
$styles = $include_italic ? ['normal', 'italic'] : ['normal'];
|
||||
|
||||
// Fetch CSS from Google
|
||||
$css = $this->fetch_css($font_name, $weights, $styles);
|
||||
$css = $this->fetch_static_css($font_name, $this->all_weights, $styles);
|
||||
|
||||
if (is_wp_error($css)) {
|
||||
return $css;
|
||||
}
|
||||
|
||||
// Parse CSS to get font face data
|
||||
$font_faces = $this->parse_css($css, $font_name);
|
||||
$font_faces = $this->parse_static_css($css, $font_name);
|
||||
|
||||
if (is_wp_error($font_faces)) {
|
||||
return $font_faces;
|
||||
}
|
||||
|
|
@ -62,6 +175,7 @@ class MLF_Font_Downloader {
|
|||
// Download each font file
|
||||
$font_slug = sanitize_title($font_name);
|
||||
$downloaded = $this->download_files($font_faces, $font_slug);
|
||||
|
||||
if (is_wp_error($downloaded)) {
|
||||
return $downloaded;
|
||||
}
|
||||
|
|
@ -70,34 +184,116 @@ class MLF_Font_Downloader {
|
|||
'font_name' => $font_name,
|
||||
'font_slug' => $font_slug,
|
||||
'files' => $downloaded,
|
||||
'is_variable' => false,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Google Fonts CSS2 API URL.
|
||||
* Fetch variable font CSS from Google Fonts API.
|
||||
*
|
||||
* @param string $font_name Font family name.
|
||||
* @param bool $italic Whether to fetch italic variant.
|
||||
* @return string|WP_Error CSS content or error.
|
||||
*/
|
||||
private function fetch_variable_css($font_name, $italic = false) {
|
||||
$family = str_replace(' ', '+', $font_name);
|
||||
|
||||
if ($italic) {
|
||||
// Request italic variable font
|
||||
$url = "https://fonts.googleapis.com/css2?family={$family}:ital,wght@1,100..900&display=swap";
|
||||
} else {
|
||||
// Request roman variable font
|
||||
$url = "https://fonts.googleapis.com/css2?family={$family}:wght@100..900&display=swap";
|
||||
}
|
||||
|
||||
return $this->fetch_css($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch static font CSS from Google Fonts API.
|
||||
*
|
||||
* @param string $font_name Font family name.
|
||||
* @param array $weights Weights to fetch.
|
||||
* @param array $styles Styles to fetch.
|
||||
* @return string|WP_Error CSS content or error.
|
||||
*/
|
||||
private function fetch_static_css($font_name, $weights, $styles) {
|
||||
$url = $this->build_static_url($font_name, $weights, $styles);
|
||||
return $this->fetch_css($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch CSS from a Google Fonts URL.
|
||||
*
|
||||
* @param string $url Google Fonts CSS URL.
|
||||
* @return string|WP_Error CSS content or error.
|
||||
*/
|
||||
private function fetch_css($url) {
|
||||
// Validate URL
|
||||
if (!$this->is_valid_google_fonts_url($url)) {
|
||||
return new WP_Error('invalid_url', 'Invalid Google Fonts URL');
|
||||
}
|
||||
|
||||
$response = wp_remote_get($url, [
|
||||
'timeout' => MLF_REQUEST_TIMEOUT,
|
||||
'sslverify' => true,
|
||||
'user-agent' => $this->user_agent,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return new WP_Error('request_failed', $response->get_error_message());
|
||||
}
|
||||
|
||||
$status = wp_remote_retrieve_response_code($response);
|
||||
|
||||
if ($status === 400) {
|
||||
return new WP_Error('font_not_found', 'Font not found');
|
||||
}
|
||||
|
||||
if ($status !== 200) {
|
||||
return new WP_Error('http_error', 'HTTP ' . $status);
|
||||
}
|
||||
|
||||
$css = wp_remote_retrieve_body($response);
|
||||
|
||||
if (empty($css)) {
|
||||
return new WP_Error('empty_response', 'Empty response from Google Fonts');
|
||||
}
|
||||
|
||||
// Check CSS response size
|
||||
$max_size = defined('MLF_MAX_CSS_SIZE') ? MLF_MAX_CSS_SIZE : 512 * 1024;
|
||||
if (strlen($css) > $max_size) {
|
||||
return new WP_Error('response_too_large', 'CSS response exceeds maximum size limit');
|
||||
}
|
||||
|
||||
// Verify we got WOFF2
|
||||
if (strpos($css, '.woff2)') === false) {
|
||||
return new WP_Error('wrong_format', 'Did not receive WOFF2 format');
|
||||
}
|
||||
|
||||
return $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build static font URL.
|
||||
*
|
||||
* @param string $font_name Font family name.
|
||||
* @param array $weights Array of weights.
|
||||
* @param array $styles Array of styles.
|
||||
* @return string Google Fonts CSS2 URL.
|
||||
*/
|
||||
private function build_url($font_name, $weights, $styles) {
|
||||
// URL-encode font name (spaces become +)
|
||||
private function build_static_url($font_name, $weights, $styles) {
|
||||
$family = str_replace(' ', '+', $font_name);
|
||||
|
||||
// Sort for consistent URLs
|
||||
sort($weights);
|
||||
|
||||
$has_italic = in_array('italic', $styles, true);
|
||||
$has_normal = in_array('normal', $styles, true);
|
||||
|
||||
// If only normal styles, simpler format
|
||||
if ($has_normal && !$has_italic) {
|
||||
$wght = implode(';', $weights);
|
||||
return "https://fonts.googleapis.com/css2?family={$family}:wght@{$wght}&display=swap";
|
||||
}
|
||||
|
||||
// Full format with ital axis
|
||||
$variations = [];
|
||||
foreach ($weights as $weight) {
|
||||
if ($has_normal) {
|
||||
|
|
@ -113,122 +309,102 @@ class MLF_Font_Downloader {
|
|||
}
|
||||
|
||||
/**
|
||||
* Fetch CSS from Google Fonts API.
|
||||
* Parse variable font CSS.
|
||||
*
|
||||
* @param string $font_name Font family name.
|
||||
* @param array $weights Weights to fetch.
|
||||
* @param array $styles Styles to fetch.
|
||||
* @return string|WP_Error CSS content or error.
|
||||
*/
|
||||
private function fetch_css($font_name, $weights, $styles) {
|
||||
$url = $this->build_url($font_name, $weights, $styles);
|
||||
|
||||
// Validate URL
|
||||
if (!$this->is_valid_google_fonts_url($url)) {
|
||||
return new WP_Error('invalid_url', 'Invalid Google Fonts URL');
|
||||
}
|
||||
|
||||
// CRITICAL: Must use modern browser user-agent to get WOFF2
|
||||
$response = wp_remote_get($url, [
|
||||
'timeout' => MLF_REQUEST_TIMEOUT,
|
||||
'sslverify' => true,
|
||||
'user-agent' => $this->user_agent,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return new WP_Error('request_failed', $response->get_error_message());
|
||||
}
|
||||
|
||||
$status = wp_remote_retrieve_response_code($response);
|
||||
if ($status === 400) {
|
||||
return new WP_Error('font_not_found', 'Font not found');
|
||||
}
|
||||
if ($status !== 200) {
|
||||
return new WP_Error('http_error', 'HTTP ' . $status);
|
||||
}
|
||||
|
||||
$css = wp_remote_retrieve_body($response);
|
||||
if (empty($css)) {
|
||||
return new WP_Error('empty_response', 'Empty response from Google Fonts');
|
||||
}
|
||||
|
||||
// Check CSS response size to prevent memory issues
|
||||
$max_size = defined('MLF_MAX_CSS_SIZE') ? MLF_MAX_CSS_SIZE : 512 * 1024;
|
||||
if (strlen($css) > $max_size) {
|
||||
return new WP_Error('response_too_large', 'CSS response exceeds maximum size limit');
|
||||
}
|
||||
|
||||
// Verify we got WOFF2 (sanity check)
|
||||
if (strpos($css, '.woff2)') === false) {
|
||||
return new WP_Error('wrong_format', 'Did not receive WOFF2 format');
|
||||
}
|
||||
|
||||
return $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Google Fonts CSS and extract font face data.
|
||||
*
|
||||
* @param string $css CSS content from Google Fonts.
|
||||
* @param string $css CSS content.
|
||||
* @param string $font_name Expected font family name.
|
||||
* @return array|WP_Error Array of font face data or error.
|
||||
*/
|
||||
private function parse_css($css, $font_name) {
|
||||
private function parse_variable_css($css, $font_name) {
|
||||
$font_faces = [];
|
||||
|
||||
// Match all @font-face blocks
|
||||
$pattern = '/@font-face\s*\{([^}]+)\}/s';
|
||||
if (!preg_match_all($pattern, $css, $matches)) {
|
||||
return new WP_Error('parse_failed', 'Could not parse CSS - no @font-face rules found');
|
||||
return new WP_Error('parse_failed', 'No @font-face rules found');
|
||||
}
|
||||
|
||||
foreach ($matches[1] as $block) {
|
||||
$face_data = $this->parse_font_face_block($block);
|
||||
$face_data = $this->parse_font_face_block($block, true);
|
||||
|
||||
if (is_wp_error($face_data)) {
|
||||
continue; // Skip malformed blocks
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify font family matches
|
||||
if (strcasecmp($face_data['family'], $font_name) !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For variable fonts, prefer latin subset
|
||||
$key = $face_data['weight'] . '-' . $face_data['style'];
|
||||
$is_latin = $this->is_latin_subset($face_data['unicode_range']);
|
||||
|
||||
if (!isset($font_faces[$key]) || $is_latin) {
|
||||
$font_faces[$key] = $face_data;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($font_faces);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse static font CSS.
|
||||
*
|
||||
* @param string $css CSS content.
|
||||
* @param string $font_name Expected font family name.
|
||||
* @return array|WP_Error Array of font face data or error.
|
||||
*/
|
||||
private function parse_static_css($css, $font_name) {
|
||||
$font_faces = [];
|
||||
|
||||
$pattern = '/@font-face\s*\{([^}]+)\}/s';
|
||||
if (!preg_match_all($pattern, $css, $matches)) {
|
||||
return new WP_Error('parse_failed', 'No @font-face rules found');
|
||||
}
|
||||
|
||||
foreach ($matches[1] as $block) {
|
||||
$face_data = $this->parse_font_face_block($block, false);
|
||||
|
||||
if (is_wp_error($face_data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify font family matches (security)
|
||||
if (strcasecmp($face_data['family'], $font_name) !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create unique key for weight+style combo
|
||||
$key = $face_data['weight'] . '-' . $face_data['style'];
|
||||
|
||||
// Prefer latin subset (usually comes after latin-ext)
|
||||
$is_latin = $this->is_latin_subset($face_data['unicode_range']);
|
||||
|
||||
// Only store if:
|
||||
// 1. We don't have this weight/style yet, OR
|
||||
// 2. This is latin and replaces non-latin
|
||||
if (!isset($font_faces[$key]) || $is_latin) {
|
||||
$font_faces[$key] = $face_data;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($font_faces)) {
|
||||
return new WP_Error('no_fonts', 'No valid font faces found in CSS');
|
||||
return new WP_Error('no_fonts', 'No valid font faces found');
|
||||
}
|
||||
|
||||
// Limit number of font faces to prevent excessive downloads
|
||||
// Limit number of font faces
|
||||
$max_faces = defined('MLF_MAX_FONT_FACES') ? MLF_MAX_FONT_FACES : 20;
|
||||
$font_faces_array = array_values($font_faces);
|
||||
if (count($font_faces_array) > $max_faces) {
|
||||
$font_faces_array = array_slice($font_faces_array, 0, $max_faces);
|
||||
$result = array_values($font_faces);
|
||||
|
||||
if (count($result) > $max_faces) {
|
||||
$result = array_slice($result, 0, $max_faces);
|
||||
}
|
||||
|
||||
return $font_faces_array;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single @font-face block.
|
||||
*
|
||||
* @param string $block Content inside @font-face { }.
|
||||
* @param bool $is_variable Whether this is a variable font.
|
||||
* @return array|WP_Error Parsed data or error.
|
||||
*/
|
||||
private function parse_font_face_block($block) {
|
||||
private function parse_font_face_block($block, $is_variable = false) {
|
||||
$data = [];
|
||||
|
||||
// Extract font-family
|
||||
|
|
@ -238,9 +414,9 @@ class MLF_Font_Downloader {
|
|||
return new WP_Error('missing_family', 'Missing font-family');
|
||||
}
|
||||
|
||||
// Extract font-weight
|
||||
if (preg_match('/font-weight:\s*(\d+);/i', $block, $m)) {
|
||||
$data['weight'] = $m[1];
|
||||
// Extract font-weight (can be single value or range for variable)
|
||||
if (preg_match('/font-weight:\s*(\d+(?:\s+\d+)?);/i', $block, $m)) {
|
||||
$data['weight'] = trim($m[1]);
|
||||
} else {
|
||||
return new WP_Error('missing_weight', 'Missing font-weight');
|
||||
}
|
||||
|
|
@ -249,7 +425,7 @@ class MLF_Font_Downloader {
|
|||
if (preg_match('/font-style:\s*(\w+);/i', $block, $m)) {
|
||||
$data['style'] = $m[1];
|
||||
} else {
|
||||
$data['style'] = 'normal'; // Default
|
||||
$data['style'] = 'normal';
|
||||
}
|
||||
|
||||
// Extract src URL - MUST be fonts.gstatic.com
|
||||
|
|
@ -259,7 +435,7 @@ class MLF_Font_Downloader {
|
|||
return new WP_Error('missing_src', 'Missing or invalid src URL');
|
||||
}
|
||||
|
||||
// Extract unicode-range (optional, for subset detection)
|
||||
// Extract unicode-range
|
||||
if (preg_match('/unicode-range:\s*([^;]+);/i', $block, $m)) {
|
||||
$data['unicode_range'] = trim($m[1]);
|
||||
} else {
|
||||
|
|
@ -277,11 +453,9 @@ class MLF_Font_Downloader {
|
|||
*/
|
||||
private function is_latin_subset($range) {
|
||||
if (empty($range)) {
|
||||
return true; // Assume latin if no range specified
|
||||
return true;
|
||||
}
|
||||
|
||||
// Latin subset typically includes basic ASCII range
|
||||
// and does NOT include extended Latin (U+0100+) as primary
|
||||
if (preg_match('/U\+0000/', $range) && !preg_match('/^U\+0100/', $range)) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -305,7 +479,8 @@ class MLF_Font_Downloader {
|
|||
$face['url'],
|
||||
$font_slug,
|
||||
$face['weight'],
|
||||
$face['style']
|
||||
$face['style'],
|
||||
false
|
||||
);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
|
|
@ -317,10 +492,10 @@ class MLF_Font_Downloader {
|
|||
'path' => $result,
|
||||
'weight' => $face['weight'],
|
||||
'style' => $face['style'],
|
||||
'is_variable' => false,
|
||||
];
|
||||
}
|
||||
|
||||
// If no files downloaded, return error
|
||||
if (empty($downloaded)) {
|
||||
return new WP_Error(
|
||||
'download_failed',
|
||||
|
|
@ -332,45 +507,45 @@ class MLF_Font_Downloader {
|
|||
}
|
||||
|
||||
/**
|
||||
* Download a single WOFF2 file from Google Fonts.
|
||||
* Download a single WOFF2 file.
|
||||
*
|
||||
* @param string $url Google Fonts static URL.
|
||||
* @param string $font_slug Font slug for filename.
|
||||
* @param string $weight Font weight.
|
||||
* @param string $weight Font weight (single or range).
|
||||
* @param string $style Font style.
|
||||
* @param bool $is_variable Whether this is a variable font.
|
||||
* @return string|WP_Error Local file path or error.
|
||||
*/
|
||||
private function download_single_file($url, $font_slug, $weight, $style) {
|
||||
// Validate URL is from Google
|
||||
private function download_single_file($url, $font_slug, $weight, $style, $is_variable = false) {
|
||||
if (!$this->is_valid_google_fonts_url($url)) {
|
||||
return new WP_Error('invalid_url', 'URL is not from Google Fonts');
|
||||
}
|
||||
|
||||
// Build local filename
|
||||
$filename = sprintf('%s_%s_%s.woff2', $font_slug, $style, $weight);
|
||||
// Build filename
|
||||
$weight_slug = str_replace(' ', '-', $weight);
|
||||
if ($is_variable) {
|
||||
$filename = sprintf('%s_%s_variable.woff2', $font_slug, $style);
|
||||
} else {
|
||||
$filename = sprintf('%s_%s_%s.woff2', $font_slug, $style, $weight_slug);
|
||||
}
|
||||
$filename = sanitize_file_name($filename);
|
||||
|
||||
// Validate filename
|
||||
$filename = $this->sanitize_font_filename($filename);
|
||||
if ($filename === false) {
|
||||
return new WP_Error('invalid_filename', 'Invalid filename');
|
||||
}
|
||||
|
||||
// Get destination path
|
||||
$font_dir = wp_get_font_dir();
|
||||
$destination = trailingslashit($font_dir['path']) . $filename;
|
||||
|
||||
// Validate destination path before any file operations
|
||||
if (!$this->validate_font_path($destination)) {
|
||||
return new WP_Error('invalid_path', 'Invalid destination path');
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
if (!wp_mkdir_p($font_dir['path'])) {
|
||||
return new WP_Error('mkdir_failed', 'Could not create fonts directory');
|
||||
}
|
||||
|
||||
// Download file
|
||||
$response = wp_remote_get($url, [
|
||||
'timeout' => MLF_REQUEST_TIMEOUT,
|
||||
'sslverify' => true,
|
||||
|
|
@ -378,31 +553,29 @@ class MLF_Font_Downloader {
|
|||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return new WP_Error('download_failed', 'Failed to download font file: ' . $response->get_error_message());
|
||||
return new WP_Error('download_failed', 'Failed to download: ' . $response->get_error_message());
|
||||
}
|
||||
|
||||
$status = wp_remote_retrieve_response_code($response);
|
||||
if ($status !== 200) {
|
||||
return new WP_Error('http_error', 'Font download returned HTTP ' . $status);
|
||||
return new WP_Error('http_error', 'Download returned HTTP ' . $status);
|
||||
}
|
||||
|
||||
$content = wp_remote_retrieve_body($response);
|
||||
if (empty($content)) {
|
||||
return new WP_Error('empty_file', 'Downloaded font file is empty');
|
||||
return new WP_Error('empty_file', 'Downloaded file is empty');
|
||||
}
|
||||
|
||||
// Check font file size to prevent memory issues
|
||||
$max_size = defined('MLF_MAX_FONT_FILE_SIZE') ? MLF_MAX_FONT_FILE_SIZE : 5 * 1024 * 1024;
|
||||
if (strlen($content) > $max_size) {
|
||||
return new WP_Error('file_too_large', 'Font file exceeds maximum size limit');
|
||||
return new WP_Error('file_too_large', 'Font file exceeds maximum size');
|
||||
}
|
||||
|
||||
// Verify it looks like a WOFF2 file (magic bytes: wOF2)
|
||||
// Verify WOFF2 magic bytes
|
||||
if (substr($content, 0, 4) !== 'wOF2') {
|
||||
return new WP_Error('invalid_format', 'Downloaded file is not valid WOFF2');
|
||||
}
|
||||
|
||||
// Write file using WP Filesystem
|
||||
global $wp_filesystem;
|
||||
if (empty($wp_filesystem)) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
|
|
@ -417,10 +590,10 @@ class MLF_Font_Downloader {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validate that a URL is a legitimate Google Fonts URL.
|
||||
* Validate Google Fonts URL.
|
||||
*
|
||||
* @param string $url URL to validate.
|
||||
* @return bool True if valid Google Fonts URL.
|
||||
* @return bool True if valid.
|
||||
*/
|
||||
private function is_valid_google_fonts_url($url) {
|
||||
$parsed = wp_parse_url($url);
|
||||
|
|
@ -429,7 +602,6 @@ class MLF_Font_Downloader {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Only allow Google Fonts domains
|
||||
$allowed_hosts = [
|
||||
'fonts.googleapis.com',
|
||||
'fonts.gstatic.com',
|
||||
|
|
@ -439,26 +611,22 @@ class MLF_Font_Downloader {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sanitize and validate a font filename.
|
||||
* Sanitize font filename.
|
||||
*
|
||||
* @param string $filename The filename to validate.
|
||||
* @return string|false Sanitized filename or false if invalid.
|
||||
* @param string $filename Filename to sanitize.
|
||||
* @return string|false Sanitized filename or false.
|
||||
*/
|
||||
private function sanitize_font_filename($filename) {
|
||||
// WordPress sanitization first
|
||||
$filename = sanitize_file_name($filename);
|
||||
|
||||
// Must have .woff2 extension
|
||||
if (pathinfo($filename, PATHINFO_EXTENSION) !== 'woff2') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// No path components
|
||||
if ($filename !== basename($filename)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reasonable length
|
||||
if (strlen($filename) > 200) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -467,24 +635,21 @@ class MLF_Font_Downloader {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validate that a path is within the WordPress fonts directory.
|
||||
* Validate font path is within fonts directory.
|
||||
*
|
||||
* @param string $path Full path to validate.
|
||||
* @return bool True if path is safe, false otherwise.
|
||||
* @param string $path Path to validate.
|
||||
* @return bool True if valid.
|
||||
*/
|
||||
private function validate_font_path($path) {
|
||||
$font_dir = wp_get_font_dir();
|
||||
$fonts_path = wp_normalize_path(trailingslashit($font_dir['path']));
|
||||
|
||||
// Resolve to real path (handles ../ etc)
|
||||
$real_path = realpath($path);
|
||||
|
||||
// If realpath fails, file doesn't exist yet - validate the directory
|
||||
if ($real_path === false) {
|
||||
$dir = dirname($path);
|
||||
$real_dir = realpath($dir);
|
||||
if ($real_dir === false) {
|
||||
// Directory doesn't exist yet, check parent
|
||||
$parent_dir = dirname($dir);
|
||||
$real_parent = realpath($parent_dir);
|
||||
if ($real_parent === false) {
|
||||
|
|
@ -498,7 +663,6 @@ class MLF_Font_Downloader {
|
|||
$real_path = wp_normalize_path($real_path);
|
||||
}
|
||||
|
||||
// Must be within fonts directory
|
||||
return strpos($real_path, $fonts_path) === 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,33 +30,44 @@ class MLF_Font_Registry {
|
|||
'post_type' => 'wp_font_family',
|
||||
'name' => $font_slug,
|
||||
'posts_per_page' => 1,
|
||||
'post_status' => 'publish',
|
||||
'post_status' => 'any',
|
||||
]);
|
||||
|
||||
if (!empty($existing)) {
|
||||
return new WP_Error('font_exists', 'Font family already installed');
|
||||
}
|
||||
|
||||
// Get font directory
|
||||
// Get font directory info
|
||||
$font_dir = wp_get_font_dir();
|
||||
|
||||
// Build font face array for WordPress
|
||||
$font_faces = [];
|
||||
foreach ($files as $file) {
|
||||
$filename = basename($file['path']);
|
||||
$font_faces[] = [
|
||||
|
||||
// Determine if this is a variable font (weight is a range like "100 900")
|
||||
$is_variable = isset($file['is_variable']) && $file['is_variable'];
|
||||
$weight = $file['weight'];
|
||||
|
||||
$face_data = [
|
||||
'fontFamily' => $font_name,
|
||||
'fontWeight' => $file['weight'],
|
||||
'fontStyle' => $file['style'],
|
||||
'fontWeight' => $weight,
|
||||
'src' => 'file:./' . $filename,
|
||||
];
|
||||
|
||||
$font_faces[] = $face_data;
|
||||
}
|
||||
|
||||
// Build font family settings
|
||||
// Determine font category for fallback
|
||||
// Default to sans-serif, but could be enhanced to detect from Google Fonts metadata
|
||||
$fallback = 'sans-serif';
|
||||
|
||||
// Build font family settings (this is what Gutenberg reads)
|
||||
$font_family_settings = [
|
||||
'name' => $font_name,
|
||||
'slug' => $font_slug,
|
||||
'fontFamily' => sprintf('"%s", sans-serif', $font_name),
|
||||
'fontFamily' => "'{$font_name}', {$fallback}",
|
||||
'fontFace' => $font_faces,
|
||||
];
|
||||
|
||||
|
|
@ -67,7 +78,7 @@ class MLF_Font_Registry {
|
|||
'post_name' => $font_slug,
|
||||
'post_status' => 'publish',
|
||||
'post_content' => wp_json_encode($font_family_settings),
|
||||
]);
|
||||
], true);
|
||||
|
||||
if (is_wp_error($family_id)) {
|
||||
return $family_id;
|
||||
|
|
@ -77,13 +88,14 @@ class MLF_Font_Registry {
|
|||
update_post_meta($family_id, '_mlf_imported', '1');
|
||||
update_post_meta($family_id, '_mlf_import_date', current_time('mysql'));
|
||||
|
||||
// Create font face posts (children)
|
||||
// Create font face posts (children) - WordPress also reads these
|
||||
foreach ($files as $file) {
|
||||
$filename = basename($file['path']);
|
||||
$weight = $file['weight'];
|
||||
|
||||
$face_settings = [
|
||||
'fontFamily' => $font_name,
|
||||
'fontWeight' => $file['weight'],
|
||||
'fontWeight' => $weight,
|
||||
'fontStyle' => $file['style'],
|
||||
'src' => 'file:./' . $filename,
|
||||
];
|
||||
|
|
@ -91,15 +103,15 @@ class MLF_Font_Registry {
|
|||
wp_insert_post([
|
||||
'post_type' => 'wp_font_face',
|
||||
'post_parent' => $family_id,
|
||||
'post_title' => sprintf('%s %s %s', $font_name, $file['weight'], $file['style']),
|
||||
'post_title' => sprintf('%s %s %s', $font_name, $weight, $file['style']),
|
||||
'post_name' => sanitize_title(sprintf('%s-%s-%s', $font_slug, $weight, $file['style'])),
|
||||
'post_status' => 'publish',
|
||||
'post_content' => wp_json_encode($face_settings),
|
||||
]);
|
||||
}
|
||||
|
||||
// Clear font caches
|
||||
delete_transient('wp_font_library_fonts');
|
||||
delete_transient('mlf_imported_fonts_list');
|
||||
// Clear all font-related caches
|
||||
$this->clear_font_caches();
|
||||
|
||||
return $family_id;
|
||||
}
|
||||
|
|
@ -154,13 +166,40 @@ class MLF_Font_Registry {
|
|||
// Delete family post
|
||||
wp_delete_post($family_id, true);
|
||||
|
||||
// Clear caches
|
||||
delete_transient('wp_font_library_fonts');
|
||||
delete_transient('mlf_imported_fonts_list');
|
||||
// Clear all font-related caches
|
||||
$this->clear_font_caches();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all font-related caches.
|
||||
*/
|
||||
private function clear_font_caches() {
|
||||
// Clear WordPress Font Library cache
|
||||
delete_transient('wp_font_library_fonts');
|
||||
|
||||
// Clear our plugin's cache
|
||||
delete_transient('mlf_imported_fonts_list');
|
||||
|
||||
// Clear global settings cache (used by Gutenberg)
|
||||
wp_cache_delete('wp_get_global_settings', 'theme_json');
|
||||
wp_cache_delete('wp_get_global_stylesheet', 'theme_json');
|
||||
|
||||
// Clear theme.json related caches
|
||||
delete_transient('global_styles');
|
||||
delete_transient('global_styles_' . get_stylesheet());
|
||||
|
||||
// Clear object cache for post queries
|
||||
wp_cache_flush_group('posts');
|
||||
|
||||
// Clear any theme mods cache
|
||||
delete_transient('theme_mods_' . get_stylesheet());
|
||||
|
||||
// Trigger action for other plugins/themes that might cache fonts
|
||||
do_action('mlf_fonts_cache_cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all fonts imported by this plugin.
|
||||
*
|
||||
|
|
@ -194,7 +233,7 @@ class MLF_Font_Registry {
|
|||
// Single query to get ALL font faces for ALL fonts (fixes N+1)
|
||||
$all_faces = get_posts([
|
||||
'post_type' => 'wp_font_face',
|
||||
'posts_per_page' => 1000, // Max 100 fonts × 9 weights × 2 styles = 1800, but limit reasonably
|
||||
'posts_per_page' => 1000,
|
||||
'post_status' => 'publish',
|
||||
'post_parent__in' => $font_ids,
|
||||
]);
|
||||
|
|
@ -234,7 +273,11 @@ class MLF_Font_Registry {
|
|||
|
||||
// Sort variants by weight then style
|
||||
usort($variants, function($a, $b) {
|
||||
$weight_cmp = intval($a['weight']) - intval($b['weight']);
|
||||
// Handle weight ranges (variable fonts)
|
||||
$a_weight = is_numeric($a['weight']) ? intval($a['weight']) : intval(explode(' ', $a['weight'])[0]);
|
||||
$b_weight = is_numeric($b['weight']) ? intval($b['weight']) : intval(explode(' ', $b['weight'])[0]);
|
||||
|
||||
$weight_cmp = $a_weight - $b_weight;
|
||||
if ($weight_cmp !== 0) {
|
||||
return $weight_cmp;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,265 @@
|
|||
<?php
|
||||
/**
|
||||
* Font Search for Maple Local Fonts.
|
||||
*
|
||||
* @package Maple_Local_Fonts
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class MLF_Font_Search
|
||||
*
|
||||
* Handles searching Google Fonts metadata.
|
||||
*/
|
||||
class MLF_Font_Search {
|
||||
|
||||
/**
|
||||
* Cache key for fonts metadata.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CACHE_KEY = 'mlf_google_fonts_metadata';
|
||||
|
||||
/**
|
||||
* Cache duration in seconds (24 hours).
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const CACHE_DURATION = DAY_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* Google Fonts metadata URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const METADATA_URL = 'https://fonts.google.com/metadata/fonts';
|
||||
|
||||
/**
|
||||
* Maximum metadata response size (2MB).
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const MAX_METADATA_SIZE = 2 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Rate limiter instance.
|
||||
*
|
||||
* @var MLF_Rate_Limiter
|
||||
*/
|
||||
private $rate_limiter;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action('wp_ajax_mlf_search_fonts', [$this, 'handle_search']);
|
||||
// No nopriv - admin only
|
||||
|
||||
// Initialize rate limiter: 30 requests per minute (search can be rapid while typing)
|
||||
$this->rate_limiter = new MLF_Rate_Limiter(30, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle font search AJAX request.
|
||||
*/
|
||||
public function handle_search() {
|
||||
// 1. NONCE CHECK
|
||||
if (!check_ajax_referer('mlf_search_fonts', 'nonce', false)) {
|
||||
wp_send_json_error(['message' => __('Security check failed.', 'maple-local-fonts')], 403);
|
||||
}
|
||||
|
||||
// 2. CAPABILITY CHECK
|
||||
$capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options';
|
||||
if (!current_user_can($capability)) {
|
||||
wp_send_json_error(['message' => __('Unauthorized.', 'maple-local-fonts')], 403);
|
||||
}
|
||||
|
||||
// 3. RATE LIMIT CHECK
|
||||
if (!$this->rate_limiter->check_and_record('search')) {
|
||||
wp_send_json_error([
|
||||
'message' => __('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'),
|
||||
], 429);
|
||||
}
|
||||
|
||||
// 4. INPUT VALIDATION
|
||||
$query = isset($_POST['query']) ? sanitize_text_field(wp_unslash($_POST['query'])) : '';
|
||||
|
||||
if (strlen($query) < 2) {
|
||||
wp_send_json_success(['fonts' => []]);
|
||||
}
|
||||
|
||||
// Limit query length
|
||||
if (strlen($query) > 100) {
|
||||
wp_send_json_error(['message' => __('Search query too long.', 'maple-local-fonts')]);
|
||||
}
|
||||
|
||||
// 5. PROCESS REQUEST
|
||||
$fonts = $this->get_fonts_metadata();
|
||||
|
||||
if (is_wp_error($fonts)) {
|
||||
wp_send_json_error(['message' => $fonts->get_error_message()]);
|
||||
}
|
||||
|
||||
// Search fonts
|
||||
$results = $this->search_fonts($fonts, $query);
|
||||
|
||||
wp_send_json_success(['fonts' => $results]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Google Fonts metadata (cached).
|
||||
*
|
||||
* @return array|WP_Error Array of fonts or error.
|
||||
*/
|
||||
public function get_fonts_metadata() {
|
||||
// Check cache first
|
||||
$cached = get_transient(self::CACHE_KEY);
|
||||
if ($cached !== false) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
// Fetch from Google
|
||||
$response = wp_remote_get(self::METADATA_URL, [
|
||||
'timeout' => 30,
|
||||
'sslverify' => true,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return new WP_Error('fetch_failed', __('Could not fetch fonts list from Google.', 'maple-local-fonts'));
|
||||
}
|
||||
|
||||
$status = wp_remote_retrieve_response_code($response);
|
||||
if ($status !== 200) {
|
||||
return new WP_Error('fetch_failed', __('Google Fonts returned an error.', 'maple-local-fonts'));
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
|
||||
// Validate response size
|
||||
if (strlen($body) > self::MAX_METADATA_SIZE) {
|
||||
return new WP_Error('response_too_large', __('Font metadata response too large.', 'maple-local-fonts'));
|
||||
}
|
||||
|
||||
// Validate response is not empty
|
||||
if (empty($body)) {
|
||||
return new WP_Error('empty_response', __('Empty response from Google Fonts.', 'maple-local-fonts'));
|
||||
}
|
||||
|
||||
// Google's metadata has a )]}' prefix for security, remove it
|
||||
$body = preg_replace('/^\)\]\}\'\s*/', '', $body);
|
||||
|
||||
$data = json_decode($body, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return new WP_Error('parse_failed', __('Could not parse fonts data.', 'maple-local-fonts'));
|
||||
}
|
||||
|
||||
if (!$data || !isset($data['familyMetadataList'])) {
|
||||
return new WP_Error('parse_failed', __('Could not parse fonts data.', 'maple-local-fonts'));
|
||||
}
|
||||
|
||||
// Process and simplify the data
|
||||
$fonts = [];
|
||||
foreach ($data['familyMetadataList'] as $font) {
|
||||
$fonts[] = [
|
||||
'family' => $font['family'],
|
||||
'category' => $font['category'] ?? 'sans-serif',
|
||||
'variants' => $font['fonts'] ?? [],
|
||||
'subsets' => $font['subsets'] ?? ['latin'],
|
||||
];
|
||||
}
|
||||
|
||||
// Cache for 24 hours
|
||||
set_transient(self::CACHE_KEY, $fonts, self::CACHE_DURATION);
|
||||
|
||||
return $fonts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search fonts by query.
|
||||
*
|
||||
* @param array $fonts All fonts.
|
||||
* @param string $query Search query.
|
||||
* @return array Matching fonts (max 20).
|
||||
*/
|
||||
private function search_fonts($fonts, $query) {
|
||||
$query_lower = strtolower($query);
|
||||
$exact_matches = [];
|
||||
$starts_with = [];
|
||||
$contains = [];
|
||||
|
||||
foreach ($fonts as $font) {
|
||||
$family_lower = strtolower($font['family']);
|
||||
|
||||
// Exact match
|
||||
if ($family_lower === $query_lower) {
|
||||
$exact_matches[] = $this->format_font_result($font);
|
||||
}
|
||||
// Starts with query
|
||||
elseif (strpos($family_lower, $query_lower) === 0) {
|
||||
$starts_with[] = $this->format_font_result($font);
|
||||
}
|
||||
// Contains query
|
||||
elseif (strpos($family_lower, $query_lower) !== false) {
|
||||
$contains[] = $this->format_font_result($font);
|
||||
}
|
||||
}
|
||||
|
||||
// Combine results: exact matches first, then starts with, then contains
|
||||
$results = array_merge($exact_matches, $starts_with, $contains);
|
||||
|
||||
// Limit to 20 results
|
||||
return array_slice($results, 0, 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a font for the search results.
|
||||
*
|
||||
* @param array $font Font data.
|
||||
* @return array Formatted font.
|
||||
*/
|
||||
private function format_font_result($font) {
|
||||
// Check if font has variable version
|
||||
$has_variable = false;
|
||||
$has_italic = false;
|
||||
$weights = [];
|
||||
|
||||
if (!empty($font['variants'])) {
|
||||
foreach ($font['variants'] as $variant => $data) {
|
||||
// Variable fonts have ranges like "100..900"
|
||||
if (strpos($variant, '..') !== false) {
|
||||
$has_variable = true;
|
||||
}
|
||||
if (strpos($variant, 'i') !== false || strpos($variant, 'italic') !== false) {
|
||||
$has_italic = true;
|
||||
}
|
||||
// Extract weight
|
||||
$weight = preg_replace('/[^0-9]/', '', $variant);
|
||||
if ($weight && !in_array($weight, $weights, true)) {
|
||||
$weights[] = $weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort weights
|
||||
sort($weights, SORT_NUMERIC);
|
||||
|
||||
return [
|
||||
'family' => $font['family'],
|
||||
'category' => $font['category'],
|
||||
'has_variable' => $has_variable,
|
||||
'has_italic' => $has_italic,
|
||||
'weights' => $weights,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the fonts metadata cache.
|
||||
*/
|
||||
public function clear_cache() {
|
||||
delete_transient(self::CACHE_KEY);
|
||||
}
|
||||
}
|
||||
|
|
@ -258,8 +258,7 @@ class MLF_Rest_Controller extends WP_REST_Controller {
|
|||
*/
|
||||
public function create_item($request) {
|
||||
$font_name = sanitize_text_field($request->get_param('font_name'));
|
||||
$weights = array_map('absint', (array) $request->get_param('weights'));
|
||||
$styles = array_map('sanitize_text_field', (array) $request->get_param('styles'));
|
||||
$include_italic = (bool) $request->get_param('include_italic');
|
||||
|
||||
// Validate font name
|
||||
if (!preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) {
|
||||
|
|
@ -278,43 +277,9 @@ class MLF_Rest_Controller extends WP_REST_Controller {
|
|||
);
|
||||
}
|
||||
|
||||
// Validate weights
|
||||
$allowed_weights = [100, 200, 300, 400, 500, 600, 700, 800, 900];
|
||||
$weights = array_intersect($weights, $allowed_weights);
|
||||
|
||||
if (empty($weights)) {
|
||||
return new WP_Error(
|
||||
'rest_invalid_param',
|
||||
__('At least one valid weight is required.', 'maple-local-fonts'),
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
if (count($weights) > MLF_MAX_WEIGHTS_PER_FONT) {
|
||||
return new WP_Error(
|
||||
'rest_invalid_param',
|
||||
__('Too many weights selected.', 'maple-local-fonts'),
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
// Validate styles
|
||||
$allowed_styles = ['normal', 'italic'];
|
||||
$styles = array_filter($styles, function($style) use ($allowed_styles) {
|
||||
return in_array($style, $allowed_styles, true);
|
||||
});
|
||||
|
||||
if (empty($styles)) {
|
||||
return new WP_Error(
|
||||
'rest_invalid_param',
|
||||
__('At least one valid style is required.', 'maple-local-fonts'),
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$downloader = new MLF_Font_Downloader();
|
||||
$download_result = $downloader->download($font_name, $weights, $styles);
|
||||
$download_result = $downloader->download($font_name, $include_italic);
|
||||
|
||||
if (is_wp_error($download_result)) {
|
||||
return new WP_Error(
|
||||
|
|
@ -455,23 +420,10 @@ class MLF_Rest_Controller extends WP_REST_Controller {
|
|||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
],
|
||||
'weights' => [
|
||||
'description' => __('Array of font weights to download.', 'maple-local-fonts'),
|
||||
'type' => 'array',
|
||||
'required' => true,
|
||||
'items' => [
|
||||
'type' => 'integer',
|
||||
'enum' => [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
],
|
||||
],
|
||||
'styles' => [
|
||||
'description' => __('Array of font styles to download.', 'maple-local-fonts'),
|
||||
'type' => 'array',
|
||||
'required' => true,
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
'enum' => ['normal', 'italic'],
|
||||
],
|
||||
'include_italic' => [
|
||||
'description' => __('Whether to include italic styles.', 'maple-local-fonts'),
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ function mlf_init() {
|
|||
if (is_admin()) {
|
||||
new MLF_Admin_Page();
|
||||
new MLF_Ajax_Handler();
|
||||
new MLF_Font_Search();
|
||||
}
|
||||
}
|
||||
add_action('plugins_loaded', 'mlf_init', 20);
|
||||
|
|
@ -155,9 +156,9 @@ function mlf_get_capability() {
|
|||
*/
|
||||
function mlf_register_menu() {
|
||||
add_submenu_page(
|
||||
'themes.php',
|
||||
__('Maple Local Fonts', 'maple-local-fonts'),
|
||||
__('Local Fonts', 'maple-local-fonts'),
|
||||
'options-general.php',
|
||||
__('Maple Fonts', 'maple-local-fonts'),
|
||||
__('Maple Fonts', 'maple-local-fonts'),
|
||||
mlf_get_capability(),
|
||||
'maple-local-fonts',
|
||||
'mlf_render_admin_page'
|
||||
|
|
@ -165,6 +166,23 @@ function mlf_register_menu() {
|
|||
}
|
||||
add_action('admin_menu', 'mlf_register_menu');
|
||||
|
||||
/**
|
||||
* Add settings link to plugin action links.
|
||||
*
|
||||
* @param array $links Existing plugin action links.
|
||||
* @return array Modified plugin action links.
|
||||
*/
|
||||
function mlf_plugin_action_links($links) {
|
||||
$settings_link = sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
esc_url(admin_url('options-general.php?page=maple-local-fonts')),
|
||||
esc_html__('Settings', 'maple-local-fonts')
|
||||
);
|
||||
array_unshift($links, $settings_link);
|
||||
return $links;
|
||||
}
|
||||
add_filter('plugin_action_links_' . MLF_PLUGIN_BASENAME, 'mlf_plugin_action_links');
|
||||
|
||||
/**
|
||||
* Render admin page (delegates to MLF_Admin_Page).
|
||||
*/
|
||||
|
|
@ -179,7 +197,7 @@ function mlf_render_admin_page() {
|
|||
* @param string $hook The current admin page hook.
|
||||
*/
|
||||
function mlf_enqueue_admin_assets($hook) {
|
||||
if ($hook !== 'appearance_page_maple-local-fonts') {
|
||||
if ($hook !== 'settings_page_maple-local-fonts') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -202,14 +220,17 @@ function mlf_enqueue_admin_assets($hook) {
|
|||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
'downloadNonce' => wp_create_nonce('mlf_download_font'),
|
||||
'deleteNonce' => wp_create_nonce('mlf_delete_font'),
|
||||
'searchNonce' => wp_create_nonce('mlf_search_fonts'),
|
||||
'strings' => [
|
||||
'downloading' => __('Downloading...', 'maple-local-fonts'),
|
||||
'deleting' => __('Deleting...', 'maple-local-fonts'),
|
||||
'confirmDelete' => __('Are you sure you want to delete this font?', 'maple-local-fonts'),
|
||||
'error' => __('An error occurred. Please try again.', 'maple-local-fonts'),
|
||||
'selectWeight' => __('Please select at least one weight.', 'maple-local-fonts'),
|
||||
'selectStyle' => __('Please select at least one style.', 'maple-local-fonts'),
|
||||
'enterFontName' => __('Please enter a font name.', 'maple-local-fonts'),
|
||||
'searchPlaceholder' => __('Search Google Fonts...', 'maple-local-fonts'),
|
||||
'searching' => __('Searching...', 'maple-local-fonts'),
|
||||
'noResults' => __('No fonts found. Try a different search term.', 'maple-local-fonts'),
|
||||
'selectFont' => __('Select a font from the search results above.', 'maple-local-fonts'),
|
||||
'previewText' => __('Maple Fonts Preview', 'maple-local-fonts'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,13 +74,14 @@ function mlf_uninstall() {
|
|||
wp_delete_post($font_id, true);
|
||||
}
|
||||
|
||||
$processed += count($fonts);
|
||||
$fonts_count = count($fonts);
|
||||
$processed += $fonts_count;
|
||||
|
||||
// Free memory
|
||||
unset($fonts, $all_faces);
|
||||
|
||||
// If we got fewer than batch_size, we're done
|
||||
if (count($fonts) < $batch_size) {
|
||||
if ($fonts_count < $batch_size) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue