Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
180 lines
7.6 KiB
PHP
180 lines
7.6 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Integrations\Divi\Pages;
|
||
|
||
use LIT\MCP\Abilities\Annotations;
|
||
use LIT\MCP\Abilities\Errors;
|
||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||
use LIT\MCP\Integrations\Divi\Compose\ColumnWidths;
|
||
use LIT\MCP\Integrations\Divi\Design\PresetAssignment;
|
||
use WP_Error;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
/**
|
||
* lit/divi-update: sets or removes values of existing nodes – short-form fields ("set"), full
|
||
* form ("attrs", null removes a path or breakpoint), preset and admin label (spec §5.7). Several
|
||
* nodes per call, all or nothing (ruling S39): every change is validated first and all errors
|
||
* come back together; only then are the attributes Composer::update() computed set. A node may
|
||
* appear once per call (ruling S14); set and attrs of one change must not reach the same path
|
||
* (ruling S38, checked by Composer::update()).
|
||
*/
|
||
final class DiviUpdateAbility extends DiviWriteAbility
|
||
{
|
||
public function name(): string
|
||
{
|
||
return 'lit/divi-update';
|
||
}
|
||
|
||
public function label(): string
|
||
{
|
||
return 'Divi-Knoten ändern';
|
||
}
|
||
|
||
public function description(): string
|
||
{
|
||
return 'Ändert vorhandene Divi-Knoten: changes = Liste von {id, set: {Kurzform-Felder}, attrs: {Attributpfad: '
|
||
. '{Breakpoint: Wert}}, preset, group_presets, clear_overrides, label}. null entfernt einen Wert. '
|
||
. 'preset = Name/ID oder Liste (Stapel, bis ' . PresetAssignment::MAX_STACK . '; bei gleicher Priorität '
|
||
. 'gewinnt das letzte Preset im Stapel); group_presets = {Gruppe: Preset}, z. B. '
|
||
. '{"font-body": "Fließtext"}. Überdecken lokale Werte '
|
||
. 'ein Preset, meldet die Antwort das; clear_overrides: true entfernt sie (nach set/attrs). Nicht '
|
||
. 'genannte Einstellungen bleiben unverändert. Jeder Knoten nur einmal pro Aufruf; denselben Pfad nicht '
|
||
. 'zugleich über set und attrs. tree_rev aus lit-divi-outline ist Pflicht; alles oder nichts.';
|
||
}
|
||
|
||
public function inputSchema(): array
|
||
{
|
||
return [
|
||
'type' => 'object',
|
||
'properties' => array_merge(DiviSchema::write(), [
|
||
'changes' => [
|
||
'type' => 'array',
|
||
'minItems' => 1,
|
||
'maxItems' => 50,
|
||
'items' => [
|
||
'type' => 'object',
|
||
'properties' => [
|
||
'id' => ['type' => 'string'],
|
||
'set' => ['type' => 'object', 'description' => 'Kurzform-Felder, z. B. {"text": "Neu"}.'],
|
||
'attrs' => [
|
||
'type' => 'object',
|
||
'description' => 'Vollform: Attributpfad → Breakpoint-Werte.',
|
||
],
|
||
'preset' => PresetAssignment::valueSchema(
|
||
true,
|
||
'Preset-Name oder -ID, Liste (Stapel, 1–' . PresetAssignment::MAX_STACK
|
||
. ') oder null zum Entfernen. Bei gleicher Priorität gewinnt das letzte '
|
||
. 'Preset im Stapel.'
|
||
),
|
||
'group_presets' => [
|
||
'type' => 'object',
|
||
'description' => 'Gruppen-Presets: {Gruppe: Preset-Name | Liste | null}. Gruppe = '
|
||
. 'Kurzname (font-body), voller Name (divi/font-body) oder Attributpfad; null '
|
||
. 'entfernt den Eintrag der Gruppe.',
|
||
'additionalProperties' => PresetAssignment::valueSchema(true),
|
||
],
|
||
'clear_overrides' => [
|
||
'type' => 'boolean',
|
||
'description' => 'true entfernt lokale Werte, die Werte der zugewiesenen Presets '
|
||
. 'überdecken (nach set und attrs).',
|
||
],
|
||
'label' => [
|
||
'type' => ['string', 'null'],
|
||
'description' => 'Admin-Label im Divi-Builder; null entfernt.',
|
||
],
|
||
],
|
||
'required' => ['id'],
|
||
'additionalProperties' => false,
|
||
],
|
||
],
|
||
]),
|
||
'required' => ['post_id', 'tree_rev', 'changes'],
|
||
'additionalProperties' => false,
|
||
];
|
||
}
|
||
|
||
public function annotations(): array
|
||
{
|
||
return Annotations::write(true);
|
||
}
|
||
|
||
protected function change(DiviPage $page, array $input, DiviComposition $composition): DiviChange|WP_Error
|
||
{
|
||
$changes = is_array($input['changes'] ?? null) ? array_values($input['changes']) : [];
|
||
if ($changes === []) {
|
||
return Errors::invalidInput('Keine Änderungen angegeben; nichts wurde gespeichert.', [
|
||
'changes' => 'mindestens eine Änderung',
|
||
]);
|
||
}
|
||
|
||
$composer = $composition->composer();
|
||
$errors = [];
|
||
/** @var list<string> $invalidPaths */
|
||
$invalidPaths = [];
|
||
/** @var array<string, int> $seen id => position of its first change */
|
||
$seen = [];
|
||
/** @var list<array{0: Block, 1: array<string, mixed>, 2: string}> $updates */
|
||
$updates = [];
|
||
foreach ($changes as $position => $change) {
|
||
$field = sprintf('changes[%d]', $position);
|
||
if (! is_array($change)) {
|
||
$errors[$field] = 'Objekt mit id erwartet';
|
||
continue;
|
||
}
|
||
|
||
$id = is_string($change['id'] ?? null) ? $change['id'] : '';
|
||
if ($id !== '' && isset($seen[$id])) {
|
||
// Ruling S14: a second change of the same node would overwrite the first silently.
|
||
$errors[$field . '.id'] = sprintf(
|
||
'Knoten "%s" steht schon in changes[%d]; alle Änderungen eines Knotens in einer Änderung bündeln',
|
||
$id,
|
||
$seen[$id]
|
||
);
|
||
continue;
|
||
}
|
||
|
||
$block = $id === '' ? null : $page->index->find($id);
|
||
if ($id !== '') {
|
||
$seen[$id] = $position;
|
||
}
|
||
|
||
if ($block === null) {
|
||
$errors[$field . '.id'] = sprintf('Knoten "%s" gibt es nicht (lit-divi-outline neu lesen)', $id);
|
||
continue;
|
||
}
|
||
|
||
$result = $composer->update($block, $change, $field);
|
||
$errors += $result['errors'];
|
||
array_push($invalidPaths, ...$result['invalid_paths']);
|
||
$updates[] = [$block, $result['attrs'], $field];
|
||
}
|
||
|
||
if ($errors === []) {
|
||
$errors = ColumnWidths::updateErrors($page->index, $updates);
|
||
}
|
||
|
||
if ($errors !== []) {
|
||
return DiviErrors::composed(
|
||
['errors' => $errors, 'invalid_paths' => $invalidPaths],
|
||
'Änderungen sind ungültig; nichts wurde gespeichert.'
|
||
);
|
||
}
|
||
|
||
$updates = array_values(array_filter(
|
||
$updates,
|
||
static fn (array $update): bool => $update[1] !== $update[0]->attrs()
|
||
));
|
||
$change = new DiviChange(sprintf('%d Divi-Knoten geändert', count($updates)));
|
||
foreach ($updates as [$block, $attrs]) {
|
||
// Composer::update() output: every client value already passed the choke point.
|
||
$block->setAttrs($attrs);
|
||
$change->record($block, 'updated');
|
||
}
|
||
|
||
return $change;
|
||
}
|
||
}
|