Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
140 lines
5.1 KiB
PHP
140 lines
5.1 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Integrations\WordPress\Render;
|
||
|
||
use LIT\MCP\Abilities\Ability;
|
||
use LIT\MCP\Abilities\Annotations;
|
||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||
use WP_Error;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
/**
|
||
* lit/page-render-summary: what a visitor sees in the content area – headings, texts, links,
|
||
* images with alt text and obvious problems (spec §5.7, §8.1). Works for every managed entry,
|
||
* Divi or not; the content is rendered in-process, header/footer/Theme Builder are not included.
|
||
*/
|
||
final class PageRenderSummaryAbility implements Ability
|
||
{
|
||
/** At most this many global-module notes per summary. */
|
||
private const GLOBAL_NOTES = 20;
|
||
|
||
public function __construct(private ContentSchema $schema, private PageRenderer $renderer)
|
||
{
|
||
}
|
||
|
||
public function name(): string
|
||
{
|
||
return 'lit/page-render-summary';
|
||
}
|
||
|
||
public function label(): string
|
||
{
|
||
return 'Gerenderte Seite (Auszug)';
|
||
}
|
||
|
||
public function description(): string
|
||
{
|
||
return 'Rendert den Inhaltsbereich eines Eintrags wie im Frontend und liefert einen strukturierten Auszug: '
|
||
. 'Überschriften, Texte, Links, Bilder mit Alt-Text und Warnungen (Hierarchie-Sprünge, fehlende '
|
||
. 'Alt-Texte). Header, Footer und Theme-Builder-Vorlagen sind nicht enthalten. Das Rendern führt die '
|
||
. 'üblichen Inhalts-Filter aus (Shortcodes, Divi, andere Plugins) und kann dabei Netzwerkanfragen '
|
||
. 'auslösen, z. B. oEmbed-Abrufe, die von WordPress oder anderen Plugins zwischengespeichert werden. '
|
||
. 'Gut zum Prüfen nach Änderungen. Globale Module (Divi-Bibliothek) fehlen im Auszug und werden '
|
||
. 'unter notes genannt.';
|
||
}
|
||
|
||
public function inputSchema(): array
|
||
{
|
||
return [
|
||
'type' => 'object',
|
||
'properties' => ['post_id' => ['type' => 'integer', 'minimum' => 1]],
|
||
'required' => ['post_id'],
|
||
'additionalProperties' => false,
|
||
];
|
||
}
|
||
|
||
public function outputSchema(): ?array
|
||
{
|
||
return [
|
||
'type' => 'object',
|
||
'properties' => [
|
||
'post_id' => ['type' => 'integer'],
|
||
'url' => ['type' => 'string'],
|
||
'headings' => ['type' => 'array', 'items' => ['type' => 'object']],
|
||
'texts' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||
'links' => ['type' => 'array', 'items' => ['type' => 'object']],
|
||
'images' => ['type' => 'array', 'items' => ['type' => 'object']],
|
||
'warnings' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||
'notes' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||
],
|
||
'required' => ['post_id', 'headings', 'texts', 'links', 'images', 'warnings', 'notes'],
|
||
];
|
||
}
|
||
|
||
public function annotations(): array
|
||
{
|
||
return Annotations::read();
|
||
}
|
||
|
||
public function isDirect(): bool
|
||
{
|
||
return true;
|
||
}
|
||
|
||
public function permission(array $input): bool|WP_Error
|
||
{
|
||
$post = PostGuard::editable($input['post_id'] ?? null, $this->schema, 'post_id', true);
|
||
|
||
return $post instanceof WP_Error ? $post : true;
|
||
}
|
||
|
||
public function execute(array $input): mixed
|
||
{
|
||
$post = PostGuard::editable($input['post_id'] ?? null, $this->schema, 'post_id', true);
|
||
if ($post instanceof WP_Error) {
|
||
return $post;
|
||
}
|
||
|
||
$notes = [
|
||
'Nur der Inhaltsbereich; Header, Footer und Theme-Builder-Vorlagen fehlen.',
|
||
'Das Rendern führt Plugin-/Theme-Filter aus (Shortcodes, oEmbed u. Ä.) und kann dabei '
|
||
. 'Netzwerkanfragen auslösen, die von WordPress oder anderen Plugins zwischengespeichert werden.',
|
||
];
|
||
if ((string) $post->post_password !== '') {
|
||
$notes[] = 'Der Eintrag ist passwortgeschützt; Besucher sehen zuerst das Passwortformular.';
|
||
}
|
||
|
||
foreach (array_slice(self::globalModuleIds((string) $post->post_content), 0, self::GLOBAL_NOTES) as $id) {
|
||
$notes[] = sprintf(
|
||
'Globales Modul %s ist im Auszug nicht enthalten (beim Aufruf über die Schnittstelle rendert Divi '
|
||
. 'globale Module nicht); Inhalt mit lit-divi-outline mit post_id %s lesen.',
|
||
$id,
|
||
$id
|
||
);
|
||
}
|
||
|
||
return array_merge(
|
||
['post_id' => (int) $post->ID, 'url' => (string) get_permalink($post)],
|
||
RenderSummary::fromHtml($this->renderer->render($post)),
|
||
['notes' => $notes]
|
||
);
|
||
}
|
||
|
||
/**
|
||
* IDs of the global modules in $content, found the way Divi finds them (a string scan for
|
||
* "globalModule", BlockParserStore::get_global_layout_content – parse_blocks() is unreliable there).
|
||
*
|
||
* @return list<string>
|
||
*/
|
||
private static function globalModuleIds(string $content): array
|
||
{
|
||
preg_match_all('/"globalModule":"?(\d+)"?/', $content, $matches);
|
||
|
||
return array_values(array_unique($matches[1]));
|
||
}
|
||
}
|