refactor: small Divi tree helpers out of DiviWriter (M3)
Block::isPlaceholder replaces the divi/placeholder literals (ledger T4); NodeIndex::parentBlock/sectionLabel replace the two parentBlock copies and DiviWriter::sectionLabel; DiviChange::idOf and Outline::summary replace the duplicated id resolution and outline text; DiviSchema holds the shared input properties. Behaviour unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Integrations\Divi\Pages;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
|
||||
|
||||
/**
|
||||
@@ -74,6 +75,17 @@ final class DiviChange
|
||||
return $this->changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Path ID of an entry in the tree after the change: a removed node keeps its ID from before
|
||||
* the removal; a surviving node is looked up in $index (null if it is not there).
|
||||
*
|
||||
* @param array{block: ?Block, id: ?string, type: string, action: string, label: string} $entry
|
||||
*/
|
||||
public static function idOf(array $entry, NodeIndex $index): ?string
|
||||
{
|
||||
return $entry['id'] ?? ($entry['block'] !== null ? $index->idOf($entry['block']) : null);
|
||||
}
|
||||
|
||||
/** @return list<Block> */
|
||||
public function touched(): array
|
||||
{
|
||||
|
||||
@@ -71,15 +71,14 @@ final class DiviContentComposer implements ContentComposer
|
||||
|
||||
$index = NodeIndex::build($layout['document']);
|
||||
$warnings = $composition->warnings()->collect($index, $layout['blocks']);
|
||||
$outline = new Outline($this->design->read()->presetNames());
|
||||
$text = $outline->text($outline->rows($index));
|
||||
if ($outline->wasTruncated()) {
|
||||
$outline = (new Outline($this->design->read()->presetNames()))->summary($index);
|
||||
if ($outline['truncated']) {
|
||||
$warnings[] = DiviWriter::OUTLINE_TRUNCATED;
|
||||
}
|
||||
|
||||
$saved = $this->saver->saveNew($postId, Serializer::document($layout['document']));
|
||||
|
||||
return ['warnings' => array_merge($warnings, $saved), 'outline' => $text];
|
||||
return ['warnings' => array_merge($warnings, $saved), 'outline' => (string) $outline['text']];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,7 +38,7 @@ final class DiviDeleteAbility extends DiviWriteAbility
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'properties' => array_merge(DiviWriter::inputProperties(), [
|
||||
'properties' => array_merge(DiviSchema::write(), [
|
||||
'ids' => [
|
||||
'type' => 'array',
|
||||
'minItems' => 1,
|
||||
|
||||
@@ -34,7 +34,7 @@ final class DiviDuplicateNodeAbility extends DiviWriteAbility
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'properties' => array_merge(DiviWriter::inputProperties(), [
|
||||
'properties' => array_merge(DiviSchema::write(), [
|
||||
'id' => ['type' => 'string', 'description' => 'Pfad-ID des zu duplizierenden Knotens.'],
|
||||
]),
|
||||
'required' => ['post_id', 'tree_rev', 'id'],
|
||||
|
||||
@@ -39,7 +39,7 @@ final class DiviInsertAbility extends DiviWriteAbility
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'properties' => array_merge(DiviWriter::inputProperties(), [
|
||||
'properties' => array_merge(DiviSchema::write(), [
|
||||
'nodes' => [
|
||||
'type' => 'array',
|
||||
'minItems' => 1,
|
||||
@@ -48,7 +48,7 @@ final class DiviInsertAbility extends DiviWriteAbility
|
||||
'description' => 'Einzufügende Knoten in Reihenfolge; Abschnitte mit rows/modules, Zeilen mit '
|
||||
. 'columns und modules, Module mit Kurzform-Feldern, items (Kind-Module) und attrs.',
|
||||
],
|
||||
], DiviWriter::positionProperties()),
|
||||
], DiviSchema::position()),
|
||||
'required' => ['post_id', 'tree_rev', 'nodes'],
|
||||
'additionalProperties' => false,
|
||||
];
|
||||
@@ -68,7 +68,7 @@ final class DiviInsertAbility extends DiviWriteAbility
|
||||
return DiviErrors::position($position);
|
||||
}
|
||||
|
||||
$context = DiviWriter::sectionLabel($page->index, $position['parent']);
|
||||
$context = $page->index->sectionLabel($position['parent']);
|
||||
$nodes = is_array($input['nodes'] ?? null) ? array_values($input['nodes']) : [];
|
||||
if ($nodes === []) {
|
||||
return Errors::invalidInput('Keine Knoten angegeben; nichts wurde gespeichert.', [
|
||||
|
||||
@@ -35,9 +35,9 @@ final class DiviMoveAbility extends DiviWriteAbility
|
||||
return [
|
||||
'type' => 'object',
|
||||
'properties' => array_merge(
|
||||
DiviWriter::inputProperties(),
|
||||
DiviSchema::write(),
|
||||
['id' => ['type' => 'string', 'description' => 'Pfad-ID des zu verschiebenden Knotens.']],
|
||||
DiviWriter::positionProperties()
|
||||
DiviSchema::position()
|
||||
),
|
||||
'required' => ['post_id', 'tree_rev', 'id'],
|
||||
'additionalProperties' => false,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Pages;
|
||||
|
||||
/** Input schema properties the Divi write tools share. */
|
||||
final class DiviSchema
|
||||
{
|
||||
/** @return array<string, array<string, mixed>> post_id and tree_rev of every Divi write. */
|
||||
public static function write(): array
|
||||
{
|
||||
return [
|
||||
'post_id' => ['type' => 'integer', 'minimum' => 1],
|
||||
'tree_rev' => [
|
||||
'type' => 'string',
|
||||
'description' => 'tree_rev aus lit-divi-outline bzw. der letzten Divi-Änderung (Pflicht).',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, array<string, mixed>> Target position of insert and move. */
|
||||
public static function position(): array
|
||||
{
|
||||
return [
|
||||
'parent' => [
|
||||
'type' => 'string',
|
||||
'description' => 'Pfad-ID des Elternknotens (z. B. Spalte "s0.r0.c1"); ohne parent/before/after: '
|
||||
. 'Ende der Seite.',
|
||||
],
|
||||
'before' => ['type' => 'string', 'description' => 'Pfad-ID: vor diesem Knoten einfügen.'],
|
||||
'after' => ['type' => 'string', 'description' => 'Pfad-ID: nach diesem Knoten einfügen.'],
|
||||
'index' => ['type' => 'integer', 'minimum' => 0, 'description' => 'Position unter parent (0 = zuerst).'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ final class DiviUpdateAbility extends DiviWriteAbility
|
||||
{
|
||||
return [
|
||||
'type' => 'object',
|
||||
'properties' => array_merge(DiviWriter::inputProperties(), [
|
||||
'properties' => array_merge(DiviSchema::write(), [
|
||||
'changes' => [
|
||||
'type' => 'array',
|
||||
'minItems' => 1,
|
||||
|
||||
@@ -9,9 +9,7 @@ use LIT\MCP\Abilities\Rev;
|
||||
use LIT\MCP\Abilities\ToolResult;
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Outline;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Serializer;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||
@@ -40,33 +38,6 @@ class DiviWriter
|
||||
) {
|
||||
}
|
||||
|
||||
/** @return array<string, array<string, mixed>> post_id and tree_rev of every Divi write. */
|
||||
public static function inputProperties(): array
|
||||
{
|
||||
return [
|
||||
'post_id' => ['type' => 'integer', 'minimum' => 1],
|
||||
'tree_rev' => [
|
||||
'type' => 'string',
|
||||
'description' => 'tree_rev aus lit-divi-outline bzw. der letzten Divi-Änderung (Pflicht).',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, array<string, mixed>> Target position of insert and move. */
|
||||
public static function positionProperties(): array
|
||||
{
|
||||
return [
|
||||
'parent' => [
|
||||
'type' => 'string',
|
||||
'description' => 'Pfad-ID des Elternknotens (z. B. Spalte "s0.r0.c1"); ohne parent/before/after: '
|
||||
. 'Ende der Seite.',
|
||||
],
|
||||
'before' => ['type' => 'string', 'description' => 'Pfad-ID: vor diesem Knoten einfügen.'],
|
||||
'after' => ['type' => 'string', 'description' => 'Pfad-ID: nach diesem Knoten einfügen.'],
|
||||
'index' => ['type' => 'integer', 'minimum' => 0, 'description' => 'Position unter parent (0 = zuerst).'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The page to change, after rights, type, layout and the tree_rev lock were checked.
|
||||
*
|
||||
@@ -104,18 +75,6 @@ class DiviWriter
|
||||
return $this->composition->forPost((int) $page->post->ID);
|
||||
}
|
||||
|
||||
/** Section label around $block, for automatic admin labels ("" when none). */
|
||||
public static function sectionLabel(NodeIndex $index, ?Block $block): string
|
||||
{
|
||||
for ($node = $block; $node !== null; $node = self::parentBlock($index, $node)) {
|
||||
if ($node->name() === 'divi/section') {
|
||||
return NodeSummary::label($node);
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the changed tree of $page and returns the ToolResult array. Call it only when the
|
||||
* whole input validated – every error before means nothing is written.
|
||||
@@ -178,15 +137,16 @@ class DiviWriter
|
||||
->with('tree_rev', Rev::of((string) $updated->post_content))
|
||||
->urls((string) get_preview_post_link($updated), $page->editUrl());
|
||||
foreach ($change->entries() as $entry) {
|
||||
$id = $entry['id'] ?? ($entry['block'] !== null ? $index->idOf($entry['block']) : null);
|
||||
$tool->changed($id ?? '?', $entry['type'], $entry['action'], $entry['label']);
|
||||
$tool->changed(DiviChange::idOf($entry, $index) ?? '?', $entry['type'], $entry['action'], $entry['label']);
|
||||
}
|
||||
|
||||
return $tool->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Outline of the sections that contain changed nodes (breadth limit of ruling S18).
|
||||
* Outline of the sections that contain changed nodes (breadth limit of ruling S18). A removed
|
||||
* node has no block any more, but its ID from before the removal still names the (surviving)
|
||||
* section.
|
||||
*
|
||||
* @return array{text: ?string, truncated: bool}
|
||||
*/
|
||||
@@ -194,9 +154,7 @@ class DiviWriter
|
||||
{
|
||||
$sections = [];
|
||||
foreach ($change->entries() as $entry) {
|
||||
// Same id resolution as the "changed" entries below: a removed node has no block
|
||||
// any more, but its id from before the removal still names the (surviving) section.
|
||||
$id = $entry['id'] ?? ($entry['block'] !== null ? $index->idOf($entry['block']) : null);
|
||||
$id = DiviChange::idOf($entry, $index);
|
||||
if ($id !== null) {
|
||||
$sections[explode('.', $id)[0]] = true;
|
||||
}
|
||||
@@ -207,20 +165,7 @@ class DiviWriter
|
||||
}
|
||||
|
||||
$outline = new Outline($this->design->read()->presetNames());
|
||||
$texts = [];
|
||||
$truncated = false;
|
||||
foreach (array_keys($sections) as $sectionId) {
|
||||
$texts[] = $outline->text($outline->rows($index, (string) $sectionId));
|
||||
$truncated = $truncated || $outline->wasTruncated();
|
||||
}
|
||||
|
||||
return ['text' => implode("\n", $texts), 'truncated' => $truncated];
|
||||
}
|
||||
|
||||
private static function parentBlock(NodeIndex $index, Block $block): ?Block
|
||||
{
|
||||
$parent = $index->parentOf($block);
|
||||
|
||||
return $parent instanceof Block && $parent->name() !== 'divi/placeholder' ? $parent : null;
|
||||
return $outline->summary($index, array_map('strval', array_keys($sections)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user