feat: Fähigkeiten theme-option-read/-write und font-upload; ThemeOptionsAdapter und FontCatalog (Manifest, Nachbau customizer-typography, Signatur-Referenzen) und DesignRights::themeOptions()/fonts()

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-19 17:44:57 +02:00
co-authored by Claude Sonnet 5.5
parent 8379c55c89
commit a90cd7f5bd
14 changed files with 4765 additions and 26 deletions
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Design;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
use LIT\MCP\Integrations\Divi\Release\Manifest;
defined('ABSPATH') || exit;
/**
* Adapter "Designsystem" (spec §3) for Divi's font lists (plan 4d W5, W9): the families Divi's customizer sanitizer
* accepts (uploaded fonts + et_builder_get_fonts(), exact names), the uploaded ones, and every reserved name for an
* upload, plus Divi's role right "Eigene Schriften verwalten". et_builder_get_google_fonts() parses about 1.3 MB and
* may fetch from Google once a day, so names() is loaded once per instance – DiviIntegration builds one per request;
* uploaded() and reserved() are read fresh (an upload changes them). Not final so the tools can be tested with a mock.
*/
class FontCatalog implements DiviAdapter
{
/** @var list<string>|null */
private ?array $names = null;
public static function manifest(): Manifest
{
return new Manifest(
Manifest::DESIGN,
calls: [
new Call('et_builder_get_fonts', 0),
new Call('et_builder_get_custom_fonts', 0),
new Call('et_builder_get_google_fonts', 0),
new Call('et_builder_get_websafe_fonts', 0),
new Call('et_pb_is_allowed', 1, [DiviFeature::FontUpload]),
],
// The options behind Divi's lists; read only through the functions above.
keys: ['et_uploaded_fonts', 'et_google_fonts_cache', 'et_google_api_settings'],
// Plan 4d W1: font variables are checked against the same list, so a break locks all three on purpose.
features: [DiviFeature::ThemeOptionWrite, DiviFeature::FontUpload, DiviFeature::VariableWrite]
);
}
/** @return list<string> Exact family names Divi's sanitizer accepts: uploaded, then web-safe and Google. */
public function names(): array
{
return $this->names ??= self::namesOf(et_builder_get_custom_fonts(), et_builder_get_fonts());
}
/** @return list<string> */
public function uploaded(): array
{
return self::namesOf(et_builder_get_custom_fonts());
}
/** @return list<string> Google, web-safe and uploaded names – an upload must not reuse one (W9). */
public function reserved(): array
{
return self::namesOf(
et_builder_get_google_fonts(),
et_builder_get_websafe_fonts(),
et_builder_get_custom_fonts()
);
}
public function has(string $name): bool
{
return in_array($name, $this->names(), true);
}
/** Divi's role right "Eigene Schriften verwalten" (custom_fonts_management); fail closed without Divi. */
public function canManage(): bool
{
return function_exists('et_pb_is_allowed') && (bool) et_pb_is_allowed('custom_fonts_management');
}
/**
* @param mixed ...$lists Divi's lists (family => settings); anything else (a broken filter) is skipped.
* @return list<string>
*/
private static function namesOf(mixed ...$lists): array
{
$names = [];
foreach ($lists as $list) {
if (! is_array($list)) {
continue;
}
foreach (array_keys($list) as $name) {
$names[] = (string) $name;
}
}
return array_values(array_unique($names));
}
}