fix: divi-node-get rev + short type, divi-outline id validation
- lit/divi-node-get now also returns the post-level rev (Rev::ofPost, same
optimistic lock lit-content-update needs) next to tree_rev, and reports a
node's type as the short name ("button"), matching lit-divi-outline instead
of the namespaced "divi/button". description() notes that $variable(...)$
values shown are re-validated when written, never taken over unchecked.
- lit/divi-outline validates its optional `id` input against the same
path-ID pattern lit-divi-node-get uses (a malformed id is now a validation
error, not a "not found"), and its outputSchema's per-node `required` list
now includes info/presets/children, which are always present.
- NodeSummary::label() (the admin label shown in outlines and node-get)
whitespace-normalizes and bounds its result via Text::shorten(), like every
other summary string, instead of returning a raw, unbounded value.
- docs/superpowers/notes/.../etappe-2a-abnahme.md corrected: only a global
module is "nur lesbar" - a specialty section is a normal, editable
divi/section block; the outline just marks it "[specialty]".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,8 +54,9 @@ Staging) verbinden und prüfen, dass die Antworten mit dem tatsächlichen Aufbau
|
|||||||
Überschriften/Texte/Links/Bilder liefern, die dem Frontend entsprechen.
|
Überschriften/Texte/Links/Bilder liefern, die dem Frontend entsprechen.
|
||||||
- [ ] Bei einer der zuvor im Visual Builder gebauten Seiten (Schritt 1): stichprobenartig prüfen, dass
|
- [ ] Bei einer der zuvor im Visual Builder gebauten Seiten (Schritt 1): stichprobenartig prüfen, dass
|
||||||
globales Modul, Preset, Dynamic Content und Spezialabschnitt im Outline/Node-Get sinnvoll
|
globales Modul, Preset, Dynamic Content und Spezialabschnitt im Outline/Node-Get sinnvoll
|
||||||
wiedergegeben werden (globales Modul und Spezialabschnitt ggf. als „nur lesbar“ markiert – das ist
|
wiedergegeben werden. Nur das globale Modul ist dabei „nur lesbar“ (siehe `divi-structure`-Guide);
|
||||||
in dieser Etappe so vorgesehen, siehe `divi-structure`-Guide).
|
ein Spezialabschnitt (specialty section) ist ein normaler, bearbeitbarer `divi/section`-Block –
|
||||||
|
Outline zeigt ihn lediglich mit dem Zusatz `[specialty]`, nicht als „nur lesbar“.
|
||||||
|
|
||||||
## Ergebnis
|
## Ergebnis
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,19 @@ final class NodeSummary
|
|||||||
/** Keys of object values that carry readable text, in this order. */
|
/** Keys of object values that carry readable text, in this order. */
|
||||||
private const TEXT_KEYS = ['text', 'title', 'label', 'alt', 'address', 'linkUrl', 'url', 'src'];
|
private const TEXT_KEYS = ['text', 'title', 'label', 'alt', 'address', 'linkUrl', 'url', 'src'];
|
||||||
|
|
||||||
|
/** The admin label is a one-line title in the layers panel, never an unbounded copy. */
|
||||||
|
private const LABEL_LIMIT = 80;
|
||||||
|
|
||||||
public static function label(Block $block): string
|
public static function label(Block $block): string
|
||||||
{
|
{
|
||||||
$label = AttrMap::desktop($block->attrs(), 'module.meta.adminLabel');
|
$label = AttrMap::desktop($block->attrs(), 'module.meta.adminLabel');
|
||||||
|
if (! is_string($label) || $label === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
return is_string($label) ? $label : '';
|
$normalized = trim((string) preg_replace('/\s+/u', ' ', $label));
|
||||||
|
|
||||||
|
return Text::shorten($normalized, self::LABEL_LIMIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function excerpt(Block $block, int $max = 60): string
|
public static function excerpt(Block $block, int $max = 60): string
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace LIT\MCP\Integrations\Divi\Pages;
|
|||||||
use LIT\MCP\Abilities\Ability;
|
use LIT\MCP\Abilities\Ability;
|
||||||
use LIT\MCP\Abilities\Errors;
|
use LIT\MCP\Abilities\Errors;
|
||||||
use LIT\MCP\Abilities\JsonMap;
|
use LIT\MCP\Abilities\JsonMap;
|
||||||
|
use LIT\MCP\Abilities\Rev;
|
||||||
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
|
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
|
||||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
|
use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
|
||||||
@@ -38,7 +39,9 @@ final class DiviNodeGetAbility implements Ability
|
|||||||
{
|
{
|
||||||
return 'Liest einen Knoten der Divi-Seite vollständig: attrs als Attributpfad → Werte je Breakpoint '
|
return 'Liest einen Knoten der Divi-Seite vollständig: attrs als Attributpfad → Werte je Breakpoint '
|
||||||
. '(Vollform, z. B. "title.decoration.font.font": {"desktop": {"value": {…}}}), die Kurzform-Werte, '
|
. '(Vollform, z. B. "title.decoration.font.font": {"desktop": {"value": {…}}}), die Kurzform-Werte, '
|
||||||
. 'Presets und die IDs der Kinder. Pfad-IDs aus lit-divi-outline.';
|
. 'Presets und die IDs der Kinder. Pfad-IDs aus lit-divi-outline. Werte wie $variable(…)$ (globale '
|
||||||
|
. 'Farben, Variablen, dynamische Inhalte) werden unverändert angezeigt, beim Schreiben aber erneut '
|
||||||
|
. 'geprüft, nie ungeprüft übernommen.';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function inputSchema(): array
|
public function inputSchema(): array
|
||||||
@@ -61,6 +64,7 @@ final class DiviNodeGetAbility implements Ability
|
|||||||
'properties' => [
|
'properties' => [
|
||||||
'post_id' => ['type' => 'integer'],
|
'post_id' => ['type' => 'integer'],
|
||||||
'tree_rev' => ['type' => 'string'],
|
'tree_rev' => ['type' => 'string'],
|
||||||
|
'rev' => ['type' => 'string'],
|
||||||
'id' => ['type' => 'string'],
|
'id' => ['type' => 'string'],
|
||||||
'parent' => ['type' => ['string', 'null']],
|
'parent' => ['type' => ['string', 'null']],
|
||||||
'type' => ['type' => 'string'],
|
'type' => ['type' => 'string'],
|
||||||
@@ -79,7 +83,7 @@ final class DiviNodeGetAbility implements Ability
|
|||||||
'builder_version' => ['type' => ['string', 'null']],
|
'builder_version' => ['type' => ['string', 'null']],
|
||||||
'children' => ['type' => 'array', 'items' => ['type' => 'string']],
|
'children' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||||||
],
|
],
|
||||||
'required' => ['post_id', 'tree_rev', 'id', 'type', 'editable', 'attrs', 'shortform', 'children'],
|
'required' => ['post_id', 'tree_rev', 'rev', 'id', 'type', 'editable', 'attrs', 'shortform', 'children'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,9 +128,10 @@ final class DiviNodeGetAbility implements Ability
|
|||||||
return [
|
return [
|
||||||
'post_id' => (int) $page->post->ID,
|
'post_id' => (int) $page->post->ID,
|
||||||
'tree_rev' => $page->treeRev(),
|
'tree_rev' => $page->treeRev(),
|
||||||
|
'rev' => Rev::ofPost($page->post),
|
||||||
'id' => $id,
|
'id' => $id,
|
||||||
'parent' => $page->index->parentId($block),
|
'parent' => $page->index->parentId($block),
|
||||||
'type' => $block->name(),
|
'type' => $block->isDivi() ? $block->shortName() : $block->name(),
|
||||||
'label' => NodeSummary::label($block),
|
'label' => NodeSummary::label($block),
|
||||||
'editable' => $editable,
|
'editable' => $editable,
|
||||||
'attrs' => JsonMap::of(AttrMap::flatten($block->attrs())),
|
'attrs' => JsonMap::of(AttrMap::flatten($block->attrs())),
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ declare(strict_types=1);
|
|||||||
namespace LIT\MCP\Integrations\Divi\Pages;
|
namespace LIT\MCP\Integrations\Divi\Pages;
|
||||||
|
|
||||||
use LIT\MCP\Abilities\Ability;
|
use LIT\MCP\Abilities\Ability;
|
||||||
|
use LIT\MCP\Abilities\Errors;
|
||||||
use LIT\MCP\Abilities\Rev;
|
use LIT\MCP\Abilities\Rev;
|
||||||
|
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||||
use LIT\MCP\Integrations\Divi\BlockTree\Outline;
|
use LIT\MCP\Integrations\Divi\BlockTree\Outline;
|
||||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||||
use WP_Error;
|
use WP_Error;
|
||||||
@@ -82,7 +84,9 @@ final class DiviOutlineAbility implements Ability
|
|||||||
'editable' => ['type' => 'boolean'],
|
'editable' => ['type' => 'boolean'],
|
||||||
'depth' => ['type' => 'integer'],
|
'depth' => ['type' => 'integer'],
|
||||||
],
|
],
|
||||||
'required' => ['id', 'type', 'label', 'excerpt', 'editable', 'depth'],
|
'required' => [
|
||||||
|
'id', 'type', 'label', 'excerpt', 'info', 'presets', 'children', 'editable', 'depth',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'edit_url' => ['type' => 'string'],
|
'edit_url' => ['type' => 'string'],
|
||||||
@@ -117,6 +121,9 @@ final class DiviOutlineAbility implements Ability
|
|||||||
}
|
}
|
||||||
|
|
||||||
$rootId = isset($input['id']) && is_string($input['id']) && $input['id'] !== '' ? $input['id'] : null;
|
$rootId = isset($input['id']) && is_string($input['id']) && $input['id'] !== '' ? $input['id'] : null;
|
||||||
|
if ($rootId !== null && preg_match(NodeIndex::ID_PATTERN, $rootId) !== 1) {
|
||||||
|
return Errors::invalidInput('id ist keine Pfad-ID.', ['id' => 'z. B. "s0.r1.c0.m2"']);
|
||||||
|
}
|
||||||
if ($rootId !== null && $page->index->find($rootId) === null) {
|
if ($rootId !== null && $page->index->find($rootId) === null) {
|
||||||
return DiviPageLoader::unknownNode($rootId, $page->post);
|
return DiviPageLoader::unknownNode($rootId, $page->post);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\BlockTree;
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\BlockTree;
|
||||||
|
|
||||||
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
|
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
|
||||||
|
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||||
use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
|
use LIT\MCP\Integrations\Divi\BlockTree\NodeSummary;
|
||||||
use LIT\MCP\Integrations\Divi\BlockTree\Outline;
|
use LIT\MCP\Integrations\Divi\BlockTree\Outline;
|
||||||
@@ -69,6 +70,24 @@ final class OutlineTest extends TestCase
|
|||||||
self::assertSame('', NodeSummary::textOf(new stdClass()));
|
self::assertSame('', NodeSummary::textOf(new stdClass()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testLabelIsWhitespaceNormalizedAndBounded(): void
|
||||||
|
{
|
||||||
|
$block = self::blockWithAdminLabel("Hero \n Titel\twith space");
|
||||||
|
self::assertSame('Hero Titel with space', NodeSummary::label($block));
|
||||||
|
|
||||||
|
$long = str_repeat('a', 200);
|
||||||
|
$shortened = NodeSummary::label(self::blockWithAdminLabel($long));
|
||||||
|
self::assertLessThan(200, mb_strlen($shortened));
|
||||||
|
self::assertStringEndsWith('…', $shortened);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function blockWithAdminLabel(string $label): Block
|
||||||
|
{
|
||||||
|
return Block::create('divi/text', [
|
||||||
|
'module' => ['meta' => ['adminLabel' => ['desktop' => ['value' => $label]]]],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function testAttrMapFlattenGetSetRemove(): void
|
public function testAttrMapFlattenGetSetRemove(): void
|
||||||
{
|
{
|
||||||
$attrs = [
|
$attrs = [
|
||||||
|
|||||||
@@ -55,6 +55,17 @@ final class DiviReadAbilitiesTest extends TestCase
|
|||||||
self::assertStringContainsString('tree_rev', $missing->get_error_message());
|
self::assertStringContainsString('tree_rev', $missing->get_error_message());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testOutlineValidatesTheSubtreeId(): void
|
||||||
|
{
|
||||||
|
DiviPageLoaderTest::givenPost(DiviPageLoaderTest::fixture('synthetic-basic.html'));
|
||||||
|
$ability = new DiviOutlineAbility(new DiviPageLoader(DiviPageLoaderTest::schema()), $this->design());
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
'lit_validation_failed',
|
||||||
|
$ability->execute(['post_id' => 7, 'id' => 'hero'])->get_error_code()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function testOutlineHintsForEmptyAndReadOnlyContent(): void
|
public function testOutlineHintsForEmptyAndReadOnlyContent(): void
|
||||||
{
|
{
|
||||||
DiviPageLoaderTest::givenPost('');
|
DiviPageLoaderTest::givenPost('');
|
||||||
@@ -73,8 +84,9 @@ final class DiviReadAbilitiesTest extends TestCase
|
|||||||
|
|
||||||
$heading = $ability->execute(['post_id' => 7, 'id' => 's0.r0.c1.m0']);
|
$heading = $ability->execute(['post_id' => 7, 'id' => 's0.r0.c1.m0']);
|
||||||
|
|
||||||
self::assertSame('divi/heading', $heading['type']);
|
self::assertSame('heading', $heading['type']);
|
||||||
self::assertSame('s0.r0.c1', $heading['parent']);
|
self::assertSame('s0.r0.c1', $heading['parent']);
|
||||||
|
self::assertSame(12, strlen($heading['rev']));
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
['desktop' => ['value' => ['headingLevel' => 'h1', 'color' => '#1a1a1a']], 'phone' => ['value' => ['size' => '28px']]],
|
['desktop' => ['value' => ['headingLevel' => 'h1', 'color' => '#1a1a1a']], 'phone' => ['value' => ['size' => '28px']]],
|
||||||
$heading['attrs']['title.decoration.font.font']
|
$heading['attrs']['title.decoration.font.font']
|
||||||
|
|||||||
Reference in New Issue
Block a user