Files
WP-LIT-MCP/src/Integrations/Divi/Compose/Placeholders.php
T

199 lines
7.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Compose;
use InvalidArgumentException;
use LIT\MCP\Integrations\Divi\Design\DesignSystem;
defined('ABSPATH') || exit;
/**
* Translates the placeholders of spec §5.4 into Divi's $variable(…)$ syntax:
* {{color:primary|<Name>|gcid-…}}, {{var:<Name>|gvid-…}}, {{font:heading|body}},
* {{post:title|…}}, {{field:<meta_key>}}, {{link:<ID>}} (internal link, URL fields only – E13a).
* A placeholder must be the whole value (D9); unknown names fail with the list of existing ones,
* labels two entries share fail with their IDs (ruling S13a, DesignSystem::colorIdFor/variableFor).
* The result is still checked by VariableRef wherever it is written. Pure PHP.
*/
final class Placeholders
{
/** {{post:<key>}} => Divi dynamic content option. */
public const POST = [
'title' => 'post_title',
'excerpt' => 'post_excerpt',
'date' => 'post_date',
'modified' => 'post_modified_date',
'author' => 'post_author',
'url' => 'post_link_url',
'featured_image' => 'post_featured_image',
'categories' => 'post_categories',
'tags' => 'post_tags',
];
/** Dynamic content that is text and takes before/after settings. */
private const TEXT_SETTINGS = [
'post_title', 'post_excerpt', 'post_date', 'post_modified_date', 'post_author', 'post_categories', 'post_tags',
];
private const PATTERN = '/^\{\{\s*(color|var|font|post|field|link)\s*:\s*([^{}]+?)\s*\}\}$/';
/** @param LinkTargets|null $links Entries {{link:…}} may point to; without them every link fails. */
public function __construct(private DesignSystem $design, private ?LinkTargets $links = null)
{
}
/**
* Resolves $value when it is a placeholder; other strings are returned unchanged.
*
* @throws InvalidArgumentException With a German message for the client.
*/
public function resolve(string $value): string
{
if (preg_match(self::PATTERN, trim($value), $match) !== 1) {
if (str_contains($value, '{{')) {
throw new InvalidArgumentException(
'Platzhalter müssen allein im Wert stehen, z. B. "{{color:primary}}" (bekannt: color, var, font, '
. 'post, field, link).'
);
}
return $value;
}
[, $kind, $name] = $match;
return match ($kind) {
'color' => $this->color($name),
'var' => $this->variable($name),
'font' => $this->font($name),
'post' => $this->post($name),
'link' => $this->link($name),
default => $this->field($name),
};
}
/**
* Applies resolve() to every string inside $value (full-form values are nested).
*
* @throws InvalidArgumentException
*/
public function resolveDeep(mixed $value): mixed
{
if (is_string($value)) {
return $this->resolve($value);
}
if (is_array($value)) {
return array_map([$this, 'resolveDeep'], $value);
}
return $value;
}
private function color(string $name): string
{
$id = $this->design->colorIdFor($name);
if ($id === null) {
$colors = $this->design->colors();
$known = array_merge(
array_keys(array_intersect(DesignSystem::COLOR_ALIASES, array_column($colors, 'id'))),
array_column($colors, 'label')
);
throw new InvalidArgumentException(sprintf(
'Unbekannte globale Farbe "%s". Vorhanden: %s.',
$name,
implode(', ', array_unique($known))
));
}
return VariableRef::serialize('color', $id, []);
}
private function variable(string $name): string
{
$id = $this->design->variableFor($name);
if ($id === null) {
$labels = array_column($this->design->variables(), 'label');
throw new InvalidArgumentException(sprintf(
'Unbekannte globale Variable "%s". Vorhanden: %s.',
$name,
$labels === [] ? '(keine)' : implode(', ', $labels)
));
}
return VariableRef::serialize($this->design->isColorVariable($id) ? 'color' : 'content', $id, []);
}
private function font(string $name): string
{
$id = DesignSystem::FONT_VARIABLES[strtolower($name)] ?? null;
if ($id === null) {
throw new InvalidArgumentException(
sprintf('Unbekannte Theme-Schrift "%s". Vorhanden: heading, body.', $name)
);
}
return VariableRef::serialize('content', $id, []);
}
private function post(string $name): string
{
$option = self::POST[strtolower($name)] ?? null;
if ($option === null) {
throw new InvalidArgumentException(sprintf(
'Unbekannter Beitragswert "%s". Vorhanden: %s.',
$name,
implode(', ', array_keys(self::POST))
));
}
$settings = in_array($option, self::TEXT_SETTINGS, true) ? ['before' => '', 'after' => ''] : [];
return VariableRef::serialize('content', $option, $settings);
}
/**
* {{link:<ID>}} → Divi's own internal link post_link_url_<type> with settings.post_id (F27):
* rendered as get_permalink() at view time, so a draft target gets its pretty URL once published.
* Whether Divi offers that option (public post types only) VariableRef checks against the registry;
* that the value goes into a URL field and the target is readable (S37) the choke point checks again.
*
* @throws InvalidArgumentException
*/
private function link(string $name): string
{
// FILTER_VALIDATE_INT: no id beyond PHP_INT_MAX that would saturate to another entry.
$id = preg_match('/^[1-9][0-9]{0,18}$/D', $name) === 1 ? filter_var($name, FILTER_VALIDATE_INT) : false;
if (! is_int($id)) {
throw new InvalidArgumentException(sprintf(
'Ungültige Beitrags-ID "%s" in {{link:…}} (positive Zahl erwartet, z. B. {{link:42}}).',
$name
));
}
$type = $this->links?->postTypeOf($id);
if ($type === null) {
throw new InvalidArgumentException(sprintf(
'Eintrag %s für {{link:…}} gibt es nicht, er liegt im Papierkorb oder ist nicht lesbar.',
$name
));
}
return VariableRef::serialize('content', 'post_link_url_' . $type, ['post_id' => $name]);
}
private function field(string $name): string
{
if (preg_match(DynamicContentRules::META_KEY, $name) !== 1) {
throw new InvalidArgumentException(sprintf(
'Ungültiger Feldschlüssel "%s" (erlaubt: Buchstaben, Ziffern, "_" und "-", nicht mit "_" am Anfang).',
$name
));
}
return VariableRef::serialize('content', 'custom_meta_' . $name, ['before' => '', 'after' => '']);
}
}