feat: {{link:ID}} placeholder via Divi's post_link_url dynamic content
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Compose;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Entries {{link:<ID>}} may point to (E13a). Pure interface – WpLinkTargets asks WordPress.
|
||||
*/
|
||||
interface LinkTargets
|
||||
{
|
||||
/** Post type of entry $id when it may be linked (exists, not trashed, readable); null otherwise. */
|
||||
public function postTypeOf(int $id): ?string;
|
||||
}
|
||||
@@ -12,10 +12,10 @@ 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>}}. 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.
|
||||
* {{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
|
||||
{
|
||||
@@ -37,9 +37,10 @@ final class Placeholders
|
||||
'post_title', 'post_excerpt', 'post_date', 'post_modified_date', 'post_author', 'post_categories', 'post_tags',
|
||||
];
|
||||
|
||||
private const PATTERN = '/^\{\{\s*(color|var|font|post|field)\s*:\s*([^{}]+?)\s*\}\}$/';
|
||||
private const PATTERN = '/^\{\{\s*(color|var|font|post|field|link)\s*:\s*([^{}]+?)\s*\}\}$/';
|
||||
|
||||
public function __construct(private DesignSystem $design)
|
||||
/** @param LinkTargets|null $links Entries {{link:…}} may point to; without them every link fails. */
|
||||
public function __construct(private DesignSystem $design, private ?LinkTargets $links = null)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -54,7 +55,7 @@ final class Placeholders
|
||||
if (str_contains($value, '{{')) {
|
||||
throw new InvalidArgumentException(
|
||||
'Platzhalter müssen allein im Wert stehen, z. B. "{{color:primary}}" (bekannt: color, var, font, '
|
||||
. 'post, field).'
|
||||
. 'post, field, link).'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,6 +69,7 @@ final class Placeholders
|
||||
'var' => $this->variable($name),
|
||||
'font' => $this->font($name),
|
||||
'post' => $this->post($name),
|
||||
'link' => $this->link($name),
|
||||
default => $this->field($name),
|
||||
};
|
||||
}
|
||||
@@ -152,6 +154,36 @@ final class Placeholders
|
||||
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) {
|
||||
|
||||
@@ -26,7 +26,8 @@ defined('ABSPATH') || exit;
|
||||
* settings hue/saturation/lightness/opacity, numbers only.
|
||||
* - type "content": DynamicContentRules – the dynamic content map (ruling S22), custom_meta_<key>
|
||||
* (S12/S29/S30), non-color global variables and theme fonts (S27); it also cleans each content
|
||||
* setting. "enable_html" is always rejected here.
|
||||
* setting. "enable_html" is always rejected here. Internal links post_link_url_<type> ({{link:…}},
|
||||
* E13a) only in URL fields.
|
||||
* A dynamic content map of the wrong shape fails in the constructor with InvalidArgumentException.
|
||||
*/
|
||||
final class VariableRef
|
||||
@@ -232,6 +233,13 @@ final class VariableRef
|
||||
*/
|
||||
private function contentSettings(string $name, array $settings, ValueSanitizer $sanitizer, bool $url): array
|
||||
{
|
||||
if (! $url && str_starts_with($name, 'post_link_url_')) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'"%s" (Link auf einen Eintrag, {{link:…}}) nur in Link- und URL-Feldern.',
|
||||
$name
|
||||
));
|
||||
}
|
||||
|
||||
$fields = $this->content->fieldsFor($name);
|
||||
$clean = [];
|
||||
foreach ($settings as $key => $value) {
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace LIT\MCP\Integrations\Divi\Pages;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Compose\AttrWriter;
|
||||
use LIT\MCP\Integrations\Divi\Compose\Composer;
|
||||
use LIT\MCP\Integrations\Divi\Compose\LinkTargets;
|
||||
use LIT\MCP\Integrations\Divi\Compose\MediaResolver;
|
||||
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
|
||||
use LIT\MCP\Integrations\Divi\Compose\ShortformWriter;
|
||||
@@ -45,7 +46,8 @@ class DiviComposition
|
||||
private DesignSystemReader $design,
|
||||
private ValueSanitizer $sanitizer = new WpValueSanitizer(),
|
||||
private MediaResolver $media = new WpMediaResolver(),
|
||||
private DynamicContentMap $dynamicContent = new DynamicContentMap()
|
||||
private DynamicContentMap $dynamicContent = new DynamicContentMap(),
|
||||
private LinkTargets $links = new WpLinkTargets()
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -83,7 +85,7 @@ class DiviComposition
|
||||
*/
|
||||
public function placeholders(): Placeholders
|
||||
{
|
||||
return $this->placeholders ??= new Placeholders($this->design->read());
|
||||
return $this->placeholders ??= new Placeholders($this->design->read(), $this->links);
|
||||
}
|
||||
|
||||
/** The one choke point of this composition (ValueCleaner, rulings S1–S3, S7, S13, S28, S32). */
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Pages;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Compose\LinkTargets;
|
||||
use WP_Post;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* {{link:<ID>}} targets from WordPress: the entry exists, lives (no trash, auto-draft or
|
||||
* revision) and the current user may read it (read_post, like select_post settings, S37).
|
||||
* Missing and unreadable entries give the same null (no existence oracle).
|
||||
*/
|
||||
final class WpLinkTargets implements LinkTargets
|
||||
{
|
||||
private const DEAD = ['trash', 'auto-draft', 'inherit'];
|
||||
|
||||
public function postTypeOf(int $id): ?string
|
||||
{
|
||||
$post = get_post($id);
|
||||
if (! $post instanceof WP_Post || in_array((string) $post->post_status, self::DEAD, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return current_user_can('read_post', $id) ? (string) $post->post_type : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user